import { readFile } from 'fs/promises'; import { resolve } from 'path'; import { SchemaValidationError } from '../../errors/SchemaValidationError'; import { JsonValidationService } from '../../JsonValidationService'; const NAME_PATTERN = '^[a-z][a-z0-9_\\-]+$'; async function fetchTestManifest(filename: string) { const contents = await readFile(resolve(__dirname, '__test__', 'schemas', filename), { encoding: 'utf-8', }); return JSON.parse(contents); } // eslint-disable-next-line @typescript-eslint/ban-types function expectToThrowErrorMatchingTypeAndMessage(received: Function, errorType: Function, message: string) { let error: null | Error = null; try { received(); } catch (e: any) { error = e; } expect(error).toBeDefined(); expect(error?.message).toEqual(message); expect(error).toBeInstanceOf(errorType); } describe('manifest/JobV1', () => { let validationService: JsonValidationService; beforeAll(() => { validationService = new JsonValidationService(); }); it('succeeds on a valid manifest', async () => { const manifest = await fetchTestManifest('validJob.json'); expect(validationService.validateManifest(manifest, 'JobV1')).toBeTruthy(); }); it('succeeds when an optional `image` field is provided', async () => { const manifest = await fetchTestManifest('validJob.json'); expect(validationService.validateManifest({ ...manifest, image: 'node:24' }, 'JobV1')).toBeTruthy(); }); it('errors when `image` is provided but is not a string', async () => { const manifest = await fetchTestManifest('validJob.json'); expectToThrowErrorMatchingTypeAndMessage( () => { validationService.validateManifest({ ...manifest, image: 20 }, 'JobV1'); }, SchemaValidationError, 'failed validation: Expected `20` (number) in `#/image` to be of type `string`', ); }); it('succeeds when an optional `autoRetry` field is provided', async () => { const manifest = await fetchTestManifest('validJob.json'); expect(validationService.validateManifest({ ...manifest, autoRetry: true }, 'JobV1')).toBeTruthy(); expect(validationService.validateManifest({ ...manifest, autoRetry: false }, 'JobV1')).toBeTruthy(); }); it('errors when `autoRetry` is provided but is not a boolean', async () => { const manifest = await fetchTestManifest('validJob.json'); expectToThrowErrorMatchingTypeAndMessage( () => { validationService.validateManifest({ ...manifest, autoRetry: 'true' }, 'JobV1'); }, SchemaValidationError, 'failed validation: Expected `true` (string) in `#/autoRetry` to be of type `boolean`', ); }); it('errors on invalid property types in function input', async () => { const manifest = await fetchTestManifest('badFunctionInputJob.json'); expectToThrowErrorMatchingTypeAndMessage( () => { validationService.validateManifest(manifest, 'JobV1'); }, SchemaValidationError, 'failed validation: Value `wrongType` at `#/functions/0/input/properties/something/type` does not match any schema', ); }); it('errors on non-object top level input', async () => { const manifest = await fetchTestManifest('nonObjectFunctionInputJob.json'); expectToThrowErrorMatchingTypeAndMessage( () => { validationService.validateManifest(manifest, 'JobV1'); }, SchemaValidationError, 'failed validation: Expected value at `#/functions/0/input/type` to be `object`, but value given is `string`', ); }); describe.each(['_my-name', '-my-name', 'MyName', 'myName', '0my-name'])( 'fails name-pattern validation for %s', (propertyValue) => { it.each(['name'])(`fails validation for manifests with %s of ${propertyValue}`, async (propertyName) => { const manifest = await fetchTestManifest('validJob.json'); expectToThrowErrorMatchingTypeAndMessage( () => { validationService.validateManifest( { ...manifest, [propertyName]: propertyValue, }, 'JobV1', ); }, SchemaValidationError, `failed validation: Value in \`#/${propertyName}\` should match \`${NAME_PATTERN}\`, but received \`${propertyValue}\``, ); }); it('fails validation for manifests with function names of %s', async () => { const manifest = await fetchTestManifest('validJob.json'); expectToThrowErrorMatchingTypeAndMessage( () => { validationService.validateManifest( { ...manifest, functions: [ { name: propertyValue, entry: 'main.js', input: {}, }, ], }, 'JobV1', ); }, SchemaValidationError, `failed validation: Value in \`#/functions/0/name\` should match \`${NAME_PATTERN}\`, but received \`${propertyValue}\``, ); }); }, ); });