diff --git a/packages/core/src/utils/schemaValidator.test.ts b/packages/core/src/utils/schemaValidator.test.ts new file mode 100644 index 0000000000..ecd10321d2 --- /dev/null +++ b/packages/core/src/utils/schemaValidator.test.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { SchemaValidator } from './schemaValidator.js'; + +describe('SchemaValidator', () => { + it('should allow any params if schema is undefined', () => { + const params = { + foo: 'bar', + }; + expect(SchemaValidator.validate(undefined, params)).toBeNull(); + }); + + it('rejects null params', () => { + const schema = { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + }; + expect(SchemaValidator.validate(schema, null)).toBe( + 'Value of params must be an object', + ); + }); + + it('rejects params that are not objects', () => { + const schema = { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + }; + expect(SchemaValidator.validate(schema, 'not an object')).toBe( + 'Value of params must be an object', + ); + }); + + it('allows schema with extra properties', () => { + const schema = { + type: 'object', + properties: { + example_enum: { + type: 'string', + enum: ['FOO', 'BAR'], + // enum-descriptions is not part of the JSON schema spec. + // This test verifies that the SchemaValidator allows the + // use of extra keywords, like this one, in the schema. + 'enum-descriptions': ['a foo', 'a bar'], + }, + }, + }; + const params = { + example_enum: 'BAR', + }; + + expect(SchemaValidator.validate(schema, params)).toBeNull(); + }); + + it('allows custom format values', () => { + const schema = { + type: 'object', + properties: { + duration: { + type: 'string', + // See: https://cloud.google.com/docs/discovery/type-format + format: 'google-duration', + }, + mask: { + type: 'string', + format: 'google-fieldmask', + }, + foo: { + type: 'string', + format: 'something-totally-custom', + }, + }, + }; + const params = { + duration: '10s', + mask: 'foo.bar,biz.baz', + foo: 'some value', + }; + expect(SchemaValidator.validate(schema, params)).toBeNull(); + }); + + it('allows valid values for known formats', () => { + const schema = { + type: 'object', + properties: { + today: { + type: 'string', + format: 'date', + }, + }, + }; + const params = { + today: '2025-04-08', + }; + expect(SchemaValidator.validate(schema, params)).toBeNull(); + }); + + it('rejects invalid values for known formats', () => { + const schema = { + type: 'object', + properties: { + today: { + type: 'string', + format: 'date', + }, + }, + }; + const params = { + today: 'this is not a date', + }; + expect(SchemaValidator.validate(schema, params)).not.toBeNull(); + }); +}); diff --git a/packages/core/src/utils/schemaValidator.ts b/packages/core/src/utils/schemaValidator.ts index 9465896ec9..9c7376e4d9 100644 --- a/packages/core/src/utils/schemaValidator.ts +++ b/packages/core/src/utils/schemaValidator.ts @@ -9,7 +9,18 @@ import * as addFormats from 'ajv-formats'; // Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs // eslint-disable-next-line @typescript-eslint/no-explicit-any const AjvClass = (AjvPkg as any).default || AjvPkg; -const ajValidator = new AjvClass(); +const ajValidator = new AjvClass( + // See: https://ajv.js.org/options.html#strict-mode-options + { + // strictSchema defaults to true and prevents use of JSON schemas that + // include unrecognized keywords. The JSON schema spec specifically allows + // for the use of non-standard keywords and the spec-compliant behavior + // is to ignore those keywords. Note that setting this to false also + // allows use of non-standard or custom formats (the unknown format value + // will be logged but the schema will still be considered valid). + strictSchema: false, + }, +); // eslint-disable-next-line @typescript-eslint/no-explicit-any const addFormatsFunc = (addFormats as any).default || addFormats; addFormatsFunc(ajValidator);