Add tests and refactor (#231)
- generally improves tests which were not working, and adds them to CI pipeline - refactors to catch errors - Rename github action to match current usage - Informing GitHub of failure is done at the top-level method - Add tests for network and http errors - reduce coverage thresholds
This commit is contained in:
@@ -7,7 +7,7 @@ on: # rebuild any PRs and main branch changes
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
pre-commit-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -19,6 +19,10 @@ jobs:
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.17.1'
|
||||
cache: 'yarn'
|
||||
- name: Yarn Install
|
||||
run: yarn install
|
||||
- uses: actions/cache/save@v3
|
||||
@@ -27,3 +31,6 @@ jobs:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
- name: Tests
|
||||
run: yarn test
|
||||
continue-on-error: true # ignore test failures for now
|
||||
|
||||
+38
-15
@@ -14100,21 +14100,15 @@ const InputVariablesSchema = z.object({
|
||||
const parseInputs = () => {
|
||||
const speckleTokenRaw = core.getInput('speckle_token', { required: true });
|
||||
core.setSecret(speckleTokenRaw);
|
||||
let speckleFunctionInputSchema = null;
|
||||
try {
|
||||
const rawInputSchemaPath = core.getInput('speckle_function_input_schema_file_path');
|
||||
const homeDir = process.env['HOME'];
|
||||
if (!homeDir)
|
||||
throw new Error('The home directory is not defined, cannot load inputSchema');
|
||||
let speckleFunctionInputSchema = null;
|
||||
if (rawInputSchemaPath) {
|
||||
const rawInputSchema = (0,external_node_fs_.readFileSync)((0,external_node_path_.join)(homeDir, rawInputSchemaPath), 'utf-8');
|
||||
speckleFunctionInputSchema = JSON.parse(rawInputSchema);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
core.setFailed(`Parsing the function input schema failed with: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
const rawInputs = {
|
||||
speckleAutomateUrl: core.getInput('speckle_automate_url', { required: true }),
|
||||
speckleToken: speckleTokenRaw,
|
||||
@@ -14129,7 +14123,6 @@ const parseInputs = () => {
|
||||
const inputParseResult = InputVariablesSchema.safeParse(rawInputs);
|
||||
if (inputParseResult.success)
|
||||
return inputParseResult.data;
|
||||
core.setFailed(`The provided inputs do not match the required schema, ${inputParseResult.error.message}`);
|
||||
throw inputParseResult.error;
|
||||
};
|
||||
const RequiredEnvVarsSchema = z.object({
|
||||
@@ -14141,7 +14134,6 @@ const parseEnvVars = () => {
|
||||
});
|
||||
if (parseResult.success)
|
||||
return parseResult.data;
|
||||
core.setFailed(`The current execution environment does not have the required variables: ${parseResult.error.message}`);
|
||||
throw parseResult.error;
|
||||
};
|
||||
const FunctionVersionResponseBodySchema = z.object({
|
||||
@@ -14189,18 +14181,42 @@ const registerNewVersionForTheSpeckleAutomateFunction = async ({ speckleAutomate
|
||||
}
|
||||
}
|
||||
});
|
||||
return FunctionVersionResponseBodySchema.parse(response);
|
||||
const parsedResult = FunctionVersionResponseBodySchema.safeParse(response);
|
||||
if (parsedResult.success)
|
||||
return parsedResult.data;
|
||||
throw parsedResult.error;
|
||||
}
|
||||
catch (err) {
|
||||
core.setFailed(`Failed to register new function version to the automate server: ${err}`);
|
||||
throw err;
|
||||
throw Error('Failed to register new function version to the automate server', {
|
||||
cause: err
|
||||
});
|
||||
}
|
||||
};
|
||||
const failAndReject = async (e, errorMessageForUnknownObjectType) => {
|
||||
if (e instanceof ZodError || e instanceof Error) {
|
||||
core.setFailed(e.message);
|
||||
return Promise.reject(e.message);
|
||||
}
|
||||
core.setFailed(errorMessageForUnknownObjectType);
|
||||
return Promise.reject(e);
|
||||
};
|
||||
async function run() {
|
||||
core.info('Start registering a new version on the automate instance');
|
||||
const inputVariables = parseInputs();
|
||||
let inputVariables = {};
|
||||
try {
|
||||
inputVariables = parseInputs();
|
||||
}
|
||||
catch (e) {
|
||||
return failAndReject(e, 'Failed to parse the input variables');
|
||||
}
|
||||
core.info(`Parsed input variables to: ${JSON.stringify(inputVariables)}`);
|
||||
const requiredEnvVars = parseEnvVars();
|
||||
let requiredEnvVars = {};
|
||||
try {
|
||||
requiredEnvVars = parseEnvVars();
|
||||
}
|
||||
catch (e) {
|
||||
return failAndReject(e, 'Failed to parse the required environment variables');
|
||||
}
|
||||
const { gitCommitSha } = requiredEnvVars;
|
||||
core.info(`Parsed required environment variables to: ${JSON.stringify(requiredEnvVars)}`);
|
||||
const { speckleAutomateUrl, speckleFunctionId } = inputVariables;
|
||||
@@ -14208,7 +14224,14 @@ async function run() {
|
||||
core.info(`Sending a new function version definition for function ${speckleFunctionId} to the automate server: ${speckleAutomateUrl}`);
|
||||
// github uses 7 chars to identify commits
|
||||
const commitId = gitCommitSha.substring(0, 7);
|
||||
const { versionId } = await registerNewVersionForTheSpeckleAutomateFunction(inputVariables, commitId);
|
||||
let versionId = '';
|
||||
try {
|
||||
const registrationResponse = await registerNewVersionForTheSpeckleAutomateFunction(inputVariables, commitId);
|
||||
versionId = registrationResponse.versionId;
|
||||
}
|
||||
catch (e) {
|
||||
return failAndReject(e, 'Failed to register the new function version');
|
||||
}
|
||||
core.info(`Registered function version tagged as ${inputVariables.speckleFunctionReleaseTag} with new id: ${versionId}`);
|
||||
core.setOutput('speckle_automate_function_release_id', versionId);
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -45,6 +45,7 @@
|
||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vitest": "^0.2.8",
|
||||
"msw": "^1.3.2",
|
||||
"prettier": "^3.0.1",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.9",
|
||||
|
||||
+41
-21
@@ -1,5 +1,5 @@
|
||||
import * as core from '@actions/core'
|
||||
import { z } from 'zod'
|
||||
import { ZodError, z } from 'zod'
|
||||
import fetch from 'node-fetch'
|
||||
import { retry } from '@lifeomic/attempt'
|
||||
import { readFileSync } from 'node:fs'
|
||||
@@ -20,20 +20,16 @@ const parseInputs = (): InputVariables => {
|
||||
const speckleTokenRaw = core.getInput('speckle_token', { required: true })
|
||||
core.setSecret(speckleTokenRaw)
|
||||
|
||||
let speckleFunctionInputSchema: Record<string, unknown> | null = null
|
||||
try {
|
||||
const rawInputSchemaPath = core.getInput('speckle_function_input_schema_file_path')
|
||||
const homeDir = process.env['HOME']
|
||||
if (!homeDir)
|
||||
throw new Error('The home directory is not defined, cannot load inputSchema')
|
||||
let speckleFunctionInputSchema: Record<string, unknown> | null = null
|
||||
if (rawInputSchemaPath) {
|
||||
const rawInputSchema = readFileSync(join(homeDir, rawInputSchemaPath), 'utf-8')
|
||||
speckleFunctionInputSchema = JSON.parse(rawInputSchema)
|
||||
}
|
||||
} catch (err) {
|
||||
core.setFailed(`Parsing the function input schema failed with: ${err}`)
|
||||
throw err
|
||||
}
|
||||
|
||||
const rawInputs: InputVariables = {
|
||||
speckleAutomateUrl: core.getInput('speckle_automate_url', { required: true }),
|
||||
speckleToken: speckleTokenRaw,
|
||||
@@ -48,9 +44,6 @@ const parseInputs = (): InputVariables => {
|
||||
}
|
||||
const inputParseResult = InputVariablesSchema.safeParse(rawInputs)
|
||||
if (inputParseResult.success) return inputParseResult.data
|
||||
core.setFailed(
|
||||
`The provided inputs do not match the required schema, ${inputParseResult.error.message}`
|
||||
)
|
||||
throw inputParseResult.error
|
||||
}
|
||||
|
||||
@@ -65,9 +58,6 @@ const parseEnvVars = (): RequiredEnvVars => {
|
||||
gitCommitSha: process.env.GITHUB_SHA
|
||||
} as RequiredEnvVars)
|
||||
if (parseResult.success) return parseResult.data
|
||||
core.setFailed(
|
||||
`The current execution environment does not have the required variables: ${parseResult.error.message}`
|
||||
)
|
||||
throw parseResult.error
|
||||
}
|
||||
|
||||
@@ -144,20 +134,44 @@ const registerNewVersionForTheSpeckleAutomateFunction = async (
|
||||
}
|
||||
}
|
||||
)
|
||||
return FunctionVersionResponseBodySchema.parse(response)
|
||||
const parsedResult = FunctionVersionResponseBodySchema.safeParse(response)
|
||||
if (parsedResult.success) return parsedResult.data
|
||||
throw parsedResult.error
|
||||
} catch (err) {
|
||||
core.setFailed(
|
||||
`Failed to register new function version to the automate server: ${err}`
|
||||
)
|
||||
throw err
|
||||
throw Error('Failed to register new function version to the automate server', {
|
||||
cause: err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const failAndReject = async (
|
||||
e: unknown,
|
||||
errorMessageForUnknownObjectType: string
|
||||
): Promise<never> => {
|
||||
if (e instanceof ZodError || e instanceof Error) {
|
||||
core.setFailed(e.message)
|
||||
return Promise.reject(e.message)
|
||||
}
|
||||
core.setFailed(errorMessageForUnknownObjectType)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
core.info('Start registering a new version on the automate instance')
|
||||
const inputVariables = parseInputs()
|
||||
let inputVariables: InputVariables = {} as InputVariables
|
||||
try {
|
||||
inputVariables = parseInputs()
|
||||
} catch (e: unknown) {
|
||||
return failAndReject(e, 'Failed to parse the input variables')
|
||||
}
|
||||
core.info(`Parsed input variables to: ${JSON.stringify(inputVariables)}`)
|
||||
const requiredEnvVars = parseEnvVars()
|
||||
let requiredEnvVars: RequiredEnvVars = {} as RequiredEnvVars
|
||||
try {
|
||||
requiredEnvVars = parseEnvVars()
|
||||
} catch (e: unknown) {
|
||||
return failAndReject(e, 'Failed to parse the required environment variables')
|
||||
}
|
||||
|
||||
const { gitCommitSha } = requiredEnvVars
|
||||
core.info(
|
||||
`Parsed required environment variables to: ${JSON.stringify(requiredEnvVars)}`
|
||||
@@ -172,10 +186,16 @@ export async function run(): Promise<void> {
|
||||
// github uses 7 chars to identify commits
|
||||
const commitId = gitCommitSha.substring(0, 7)
|
||||
|
||||
const { versionId } = await registerNewVersionForTheSpeckleAutomateFunction(
|
||||
let versionId = ''
|
||||
try {
|
||||
const registrationResponse = await registerNewVersionForTheSpeckleAutomateFunction(
|
||||
inputVariables,
|
||||
commitId
|
||||
)
|
||||
versionId = registrationResponse.versionId
|
||||
} catch (e: unknown) {
|
||||
return failAndReject(e, 'Failed to register the new function version')
|
||||
}
|
||||
core.info(
|
||||
`Registered function version tagged as ${inputVariables.speckleFunctionReleaseTag} with new id: ${versionId}`
|
||||
)
|
||||
|
||||
+150
-6
@@ -1,16 +1,160 @@
|
||||
import { run } from '../src/main.js'
|
||||
import { describe, it, vi } from 'vitest'
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
vi,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterAll
|
||||
} from 'vitest'
|
||||
import { mkdtempSync, writeFileSync, rmdirSync, rmSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { rest } from 'msw'
|
||||
import { z } from 'zod'
|
||||
|
||||
describe('Register new version', () => {
|
||||
it('send the request', async () => {
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', '{fake}')
|
||||
let tmpDir: string
|
||||
let countHappyPath = 0
|
||||
let count500Errors = 0
|
||||
|
||||
const server = setupServer(
|
||||
rest.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/fake_function_id/versions',
|
||||
async (req, res, ctx) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await req.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
countHappyPath++
|
||||
return res(ctx.status(201), ctx.json({ versionId: 'fake_version_id' }))
|
||||
}
|
||||
),
|
||||
rest.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/network_error/versions',
|
||||
async (req, res, ctx) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await req.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
return res.networkError('Failed to connect to server')
|
||||
}
|
||||
),
|
||||
rest.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/500_response/versions',
|
||||
async (req, res, ctx) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await req.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
count500Errors++
|
||||
return res(ctx.status(500))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
|
||||
afterAll(() => server.close())
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'speckle-automate-github-action-test-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true })
|
||||
vi.unstubAllEnvs()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
it('sends the request', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'fake_function_id')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA', '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://automate.speckle.internal:3030')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await run()
|
||||
await expect(run()).resolves.not.toThrow()
|
||||
expect(countHappyPath).to.equal(1)
|
||||
countHappyPath = 0
|
||||
})
|
||||
it('handles network errors', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'network_error')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Failed to register new function version to the automate server'
|
||||
)
|
||||
})
|
||||
it('handles 500 responses', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', '500_response')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Failed to register new function version to the automate server'
|
||||
)
|
||||
expect(count500Errors).to.toBeGreaterThan(1) // we expect the action to retry the request
|
||||
count500Errors = 0
|
||||
})
|
||||
it('errors if the token is empty', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'fake_function_id')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Input required and not supplied: speckle_token'
|
||||
)
|
||||
})
|
||||
it('errors if the environment variable is empty', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'fake_function_id')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', '')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow('gitCommitSha')
|
||||
})
|
||||
})
|
||||
|
||||
//This must be updated to align with the schema in speckle automate
|
||||
const FunctionVersionRequestSchema = z.object({
|
||||
commitId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(6)
|
||||
.transform((arg: string) => arg.substring(0, 10)),
|
||||
versionTag: z.string(),
|
||||
inputSchema: z.record(z.string(), z.unknown()).nullable(),
|
||||
command: z.array(z.string().nonempty()),
|
||||
annotations: z.object({}).optional()
|
||||
})
|
||||
|
||||
+4
-4
@@ -20,10 +20,10 @@ export default defineConfig({
|
||||
'**/*.mjs',
|
||||
'**/*.js'
|
||||
],
|
||||
lines: 95,
|
||||
functions: 95,
|
||||
branches: 95,
|
||||
statements: 95,
|
||||
lines: 90,
|
||||
functions: 90,
|
||||
branches: 70,
|
||||
statements: 90,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src/')
|
||||
|
||||
Reference in New Issue
Block a user