From 83fad073adc50056b87478b9d294afac5a7dbb5b Mon Sep 17 00:00:00 2001 From: Denis Rossati Date: Thu, 14 Nov 2024 11:34:02 -0300 Subject: [PATCH] Rework evaluation attribute schema validation (#441) --- src/schema/evaluatorSchemas.ts | 45 +++++- test/schemas/evaluationSchemas.test.ts | 45 ------ test/schemas/evaluatorSchemas.test.ts | 183 +++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 47 deletions(-) delete mode 100644 test/schemas/evaluationSchemas.test.ts create mode 100644 test/schemas/evaluatorSchemas.test.ts diff --git a/src/schema/evaluatorSchemas.ts b/src/schema/evaluatorSchemas.ts index 844463c7..bbaab769 100644 --- a/src/schema/evaluatorSchemas.ts +++ b/src/schema/evaluatorSchemas.ts @@ -1,4 +1,39 @@ -import {ObjectType, NumberType, JsonObjectType} from '../validation'; +import { + ObjectType, + NumberType, + JsonObjectType, + UnionType, + StringType, + NullType, + BooleanType, + TypeSchema, + ArrayType, +} from '../validation'; + +function createJsonSchema(maximumDepth: number): TypeSchema { + return new UnionType( + new NullType(), + new NumberType(), + new BooleanType(), + new StringType({maxLength: 255}), + ...(maximumDepth > 1 + ? [ + new JsonObjectType({ + propertyNames: new UnionType( + new NumberType(), + new StringType({ + minLength: 1, + maxLength: 50, + }), + ), + properties: createJsonSchema(maximumDepth - 1), + }), + new ArrayType({items: createJsonSchema(maximumDepth - 1)}), + ] + : [] + ), + ); +} export const evaluationOptionsSchema = new ObjectType({ properties: { @@ -6,6 +41,12 @@ export const evaluationOptionsSchema = new ObjectType({ integer: true, minimum: 0, }), - attributes: new JsonObjectType(), + attributes: new JsonObjectType({ + propertyNames: new StringType({ + minLength: 1, + maxLength: 50, + }), + properties: createJsonSchema(5), + }), }, }); diff --git a/test/schemas/evaluationSchemas.test.ts b/test/schemas/evaluationSchemas.test.ts deleted file mode 100644 index 0bd760c8..00000000 --- a/test/schemas/evaluationSchemas.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {evaluationOptionsSchema} from '../../src/schema'; -import {EvaluationOptions} from '../../src/facade/evaluatorFacade'; - -describe('The evaluation option schema', () => { - it.each([ - [{}], - [{ - timeout: 1, - }], - [{ - attributes: {foo: 'bar'}, - }], - [{ - timeout: 1, - attributes: {foo: 'bar'}, - }], - ])('should allow %s', value => { - function validate(): void { - evaluationOptionsSchema.validate(value); - } - - expect(validate).not.toThrow(); - }); - - it.each([ - [ - {timeout: -1}, - 'Expected a value greater than or equal to 0 at path \'/timeout\', actual -1.', - ], - [ - {timeout: 1.2}, - 'Expected value of type integer at path \'/timeout\', actual number.', - ], - [ - {attributes: 0}, - 'Expected a JSON object at path \'/attributes\', actual integer.', - ], - ])('should not allow %s', (value: Record, message: string) => { - function validate(): void { - evaluationOptionsSchema.validate(value); - } - - expect(validate).toThrowWithMessage(Error, message); - }); -}); diff --git a/test/schemas/evaluatorSchemas.test.ts b/test/schemas/evaluatorSchemas.test.ts new file mode 100644 index 00000000..310a056e --- /dev/null +++ b/test/schemas/evaluatorSchemas.test.ts @@ -0,0 +1,183 @@ +import {evaluationOptionsSchema} from '../../src/schema'; +import {EvaluationOptions} from '../../src/facade/evaluatorFacade'; + +describe('The evaluator options schema', () => { + it.each([ + [{}], + [{ + timeout: 1, + }], + [{ + attributes: {foo: 'bar'}, + }], + [{ + timeout: 1, + attributes: {foo: 'bar'}, + }], + [{ + timeout: 1, + attributes: {}, + }], + [{ + timeout: 1, + attributes: { + ['x'.repeat(50)]: 'x'.repeat(255), + 'multi-byte character': '♥'.repeat(255), + arr: [ + null, + 123, + 'x'.repeat(255), + true, + false, + ], + first: { + second: { + third: { + 'nested array': [ + null, + 123, + 'x'.repeat(255), + true, + false, + ], + fourth: { + fifth: '', + }, + }, + }, + }, + }, + }], + ])('should allow %s', value => { + function validate(): void { + evaluationOptionsSchema.validate(value); + } + + expect(validate).not.toThrow(); + }); + + it.each([ + [ + {timeout: -1}, + 'Expected a value greater than or equal to 0 at path \'/timeout\', actual -1.', + ], + [ + {timeout: 1.2}, + 'Expected value of type integer at path \'/timeout\', actual number.', + ], + [ + {attributes: 0}, + 'Expected a JSON object at path \'/attributes\', actual integer.', + ], + [ + { + attributes: { + first: { + second: { + third: { + fourth: { + fifth: {}, + }, + }, + }, + }, + }, + }, + 'Expected value of type null, number, boolean or string' + + ' at path \'/attributes/first/second/third/fourth/fifth\', actual Object.', + ], + [ + { + attributes: { + first: [ + [ + [ + [ + ['fifth level'], + ], + ], + ], + ], + }, + }, + 'Expected value of type null, number, boolean or string' + + ' at path \'/attributes/first/0/0/0/0\', actual array.', + ], + [ + { + attributes: { + foo: undefined, + }, + }, + 'Expected a JSON object at path \'/attributes\', actual Object.', + ], + [ + { + attributes: { + '': 'foo', + }, + }, + 'Expected at least 1 character at path \'/attributes/\', actual 0.', + ], + [ + { + attributes: { + ['x'.repeat(51)]: 'foo', + }, + }, + `Expected at most 50 characters at path '/attributes/${'x'.repeat(51)}', actual 51.`, + ], + [ + { + attributes: 'foo', + }, + 'Expected a JSON object at path \'/attributes\', actual string.', + ], + [ + { + attributes: { + string: 'x'.repeat(256), + }, + }, + 'Expected at most 255 characters at path \'/attributes/string\', actual 256.', + ], + [ + { + attributes: { + first: { + second: { + third: { + fourth: { + fifth: 'x'.repeat(256), + }, + }, + }, + }, + }, + }, + 'Expected at most 255 characters at path \'/attributes/first/second/third/fourth/fifth\', actual 256.', + ], + [ + { + attributes: { + first: [ + [ + [ + [ + 'x'.repeat(256), + ], + ], + ], + ], + }, + }, + 'Expected at most 255 characters at path \'/attributes/first/0/0/0/0\', actual 256.', + ], + ])('should not allow %s', (value: Record, message: string) => { + function validate(): void { + evaluationOptionsSchema.validate(value); + } + + expect(validate).toThrowWithMessage(Error, message); + }); +});