diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ce2dd4f..79ed5ae8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added support for `application/yaml` responses ([#363](https://github.com/opensearch-project/opensearch-api-specification/pull/363)) - Added test for search with seq_no_primary_term ([#367](https://github.com/opensearch-project/opensearch-api-specification/pull/367)) - Added a linter for parameter sorting ([#369](https://github.com/opensearch-project/opensearch-api-specification/pull/369)) +- Added AjvErrorsParser to print more informative error messages ([#364](https://github.com/opensearch-project/opensearch-api-specification/issues/364)) +- Added JsonSchemaValidator, a wrapper for AJV ([#364](https://github.com/opensearch-project/opensearch-api-specification/issues/364)) - Added support for `application/cbor` responses ([#371](https://github.com/opensearch-project/opensearch-api-specification/pull/371)) - Added support for `application/smile` responses ([#386](https://github.com/opensearch-project/opensearch-api-specification/pull/386)) - Added `doc_status`, `remote_store`, `segment_replication` and `unreferenced_file_cleanups_performed` to `SegmentStats` ([#395](https://github.com/opensearch-project/opensearch-api-specification/pull/395)) diff --git a/json_schemas/_info.schema.yaml b/json_schemas/_info.schema.yaml index 77a2257b7..6b1ce5383 100644 --- a/json_schemas/_info.schema.yaml +++ b/json_schemas/_info.schema.yaml @@ -40,5 +40,4 @@ properties: type: string required: - title - - version - - $schema \ No newline at end of file + - version \ No newline at end of file diff --git a/json_schemas/_superseded_operations.schema.yaml b/json_schemas/_superseded_operations.schema.yaml index a49f6671e..6f11df33f 100644 --- a/json_schemas/_superseded_operations.schema.yaml +++ b/json_schemas/_superseded_operations.schema.yaml @@ -1,9 +1,10 @@ $schema: http://json-schema.org/draft-07/schema# type: object -patternProperties: - ^\$schema$: +properties: + $schema: type: string +patternProperties: ^/: type: object properties: @@ -17,5 +18,4 @@ patternProperties: enum: [GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH] required: [superseded_by, operations] additionalProperties: false -required: [$schema] additionalProperties: false \ No newline at end of file diff --git a/json_schemas/test_story.schema.yaml b/json_schemas/test_story.schema.yaml index a4cddf37e..3c3e28f53 100644 --- a/json_schemas/test_story.schema.yaml +++ b/json_schemas/test_story.schema.yaml @@ -18,7 +18,7 @@ properties: type: array items: $ref: '#/definitions/Chapter' -required: [$schema, description, chapters] +required: [description, chapters] additionalProperties: false definitions: diff --git a/tools/src/_utils/AjvErrorsParser.ts b/tools/src/_utils/AjvErrorsParser.ts new file mode 100644 index 000000000..7085d8230 --- /dev/null +++ b/tools/src/_utils/AjvErrorsParser.ts @@ -0,0 +1,103 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import { Ajv2019 as AJV, ErrorsTextOptions, ErrorObject } from 'ajv/dist/2019' +import _ from 'lodash' + +interface GroupedErrors { + required: ErrorObject[] + prohibited: ErrorObject[] + enum: ErrorObject[] + others: ErrorObject[] +} + +export default class AjvErrorsParser { + private readonly ajv: AJV + private readonly options: ErrorsTextOptions + + constructor(ajv: AJV, options: ErrorsTextOptions = {}) { + this.ajv = ajv + this.options = { separator: ' --- ', ...options } + } + + parse(errors: ErrorObject[] | undefined | null): string { + const error_groups = this.#group_errors(errors ?? []) + const parsed_errors = [ + this.#prohibited_property_error(error_groups.prohibited), + this.#required_property_error(error_groups.required), + this.#enum_error(error_groups.enum), + ...error_groups.others + ].filter(e => e != null) as ErrorObject[] + return this.ajv.errorsText(parsed_errors, this.options) + } + + #group_errors(errors: ErrorObject[]): GroupedErrors { + const categories = { + required: [] as ErrorObject[], + prohibited: [] as ErrorObject[], + enum: [] as ErrorObject[], + others: [] as ErrorObject[] + } + _.values(_.groupBy(errors, 'instancePath')).forEach((path_errors) => { + for (const error of path_errors) { + switch (error.keyword) { + case 'required': + categories.required.push(error) + break + case 'unevaluatedProperties': + case 'additionalProperties': + categories.prohibited.push(error) + break + case 'enum': + categories.enum.push(error) + break + default: + categories.others.push(error) + } + } + }) + return categories + } + + #prohibited_property_error(errors: ErrorObject[]): ErrorObject | undefined { + if (errors.length === 0) return + const properties = errors.map((error) => error.params.additionalProperty ?? error.params.unevaluatedProperty).join(', ') + return { + keyword: 'prohibited', + instancePath: errors[0].instancePath, + schemaPath: errors[0].schemaPath, + params: {}, + message: `contains unsupported properties: ${properties}` + } + } + + #required_property_error(errors: ErrorObject[]): ErrorObject | undefined { + if (errors.length === 0) return + const properties = errors.map((error) => error.params.missingProperty).join(', ') + return { + keyword: 'required', + instancePath: errors[0].instancePath, + schemaPath: errors[0].schemaPath, + params: {}, + message: `MUST contain the missing properties: ${properties}` + } + } + + #enum_error(errors: ErrorObject[]): ErrorObject | undefined { + if (errors.length === 0) return + const allowed_values = errors[0].params.allowedValues.join(', ') + return { + keyword: 'enum', + instancePath: errors[0].instancePath, + schemaPath: errors[0].schemaPath, + params: {}, + message: `MUST be equal to one of the allowed values: ${allowed_values}` + } + } +} diff --git a/tools/src/_utils/JsonSchemaValidator.ts b/tools/src/_utils/JsonSchemaValidator.ts new file mode 100644 index 000000000..e72b5654b --- /dev/null +++ b/tools/src/_utils/JsonSchemaValidator.ts @@ -0,0 +1,61 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import { Ajv2019 as AJV, ErrorsTextOptions, Options as AjvOptions, ValidateFunction } from 'ajv/dist/2019' +import ajv_errors, { ErrorMessageOptions } from 'ajv-errors' +import addFormats from 'ajv-formats'; +import AjvErrorsParser from './AjvErrorsParser'; + +interface JsonSchemaValidatorOpts { + ajv_opts?: AjvOptions, + ajv_errors_opts?: ErrorMessageOptions, + errors_text_opts?: ErrorsTextOptions + additional_keywords?: string[], + reference_schemas?: Record> +} + +const DEFAULT_AJV_OPTS = { + strict: true, + allErrors: true +} + +// Wrapper for AJV +export default class JsonSchemaValidator { + private readonly ajv: AJV + private readonly errors_parser: AjvErrorsParser + private readonly _validate: ValidateFunction | undefined + message: string | undefined + + constructor(default_schema?: Record, options: JsonSchemaValidatorOpts = {}) { + this.ajv = new AJV({ ...DEFAULT_AJV_OPTS, ...options.ajv_opts }) + addFormats(this.ajv); + if (options.ajv_errors_opts != null) ajv_errors(this.ajv, options.ajv_errors_opts) + for (const keyword of options.additional_keywords ?? []) this.ajv.addKeyword(keyword) + Object.entries(options.reference_schemas ?? {}).forEach(([key, schema]) => this.ajv.addSchema(schema, key)) + this.errors_parser = new AjvErrorsParser(this.ajv, options.errors_text_opts) + if (default_schema) this._validate = this.ajv.compile(default_schema) + } + + validate_data(data: any, schema?: Record): string | undefined { + if (schema) return this.#validate(this.ajv.compile(schema), data) + if (this._validate) return this.#validate(this._validate, data) + throw new Error('No schema provided') + } + + validate_schema(schema: any): string | undefined { + const validate_func = this.ajv.validateSchema.bind(this.ajv) as ValidateFunction + return this.#validate(validate_func, schema ?? {}, true) + } + + #validate(validate_func: ValidateFunction, data: any, is_schema: boolean = false): string | undefined { + const valid = validate_func(data) as boolean + const errors = is_schema ? this.ajv.errors : validate_func.errors + return valid ? undefined : this.errors_parser.parse(errors) + } +} \ No newline at end of file diff --git a/tools/src/linter/SchemasValidator.ts b/tools/src/linter/SchemasValidator.ts index 9390f09a1..5192595c7 100644 --- a/tools/src/linter/SchemasValidator.ts +++ b/tools/src/linter/SchemasValidator.ts @@ -7,16 +7,10 @@ * compatible open source license. */ -import AJV from 'ajv' -import addFormats from 'ajv-formats' import OpenApiMerger from '../merger/OpenApiMerger' import { type ValidationError } from '../types' -import { type Logger } from '../Logger' - -const IGNORED_ERROR_PREFIXES = [ - 'can\'t resolve reference', // errors in referenced schemas will also cause reference errors - 'discriminator: oneOf subschemas' // known bug in ajv: https://github.com/ajv-validator/ajv/issues/2281 -] +import { Logger, LogLevel } from '../Logger' +import JsonSchemaValidator from "../_utils/JsonSchemaValidator"; const ADDITIONAL_KEYWORDS = [ 'x-version-added', @@ -29,18 +23,16 @@ export default class SchemasValidator { logger: Logger root_folder: string spec: Record = {} - ajv: AJV + json_validator: JsonSchemaValidator constructor (root_folder: string, logger: Logger) { this.logger = logger this.root_folder = root_folder - this.ajv = new AJV({ strict: true, discriminator: true }) - addFormats(this.ajv) - for (const keyword of ADDITIONAL_KEYWORDS) this.ajv.addKeyword(keyword) + this.json_validator = new JsonSchemaValidator(undefined, { additional_keywords: ADDITIONAL_KEYWORDS }) } validate (): ValidationError[] { - this.spec = new OpenApiMerger(this.root_folder, this.logger).merge().components as Record + this.spec = new OpenApiMerger(this.root_folder, new Logger(LogLevel.error)).merge().components as Record const named_schemas_errors = this.validate_named_schemas() if (named_schemas_errors.length > 0) return named_schemas_errors return [ @@ -52,25 +44,24 @@ export default class SchemasValidator { validate_named_schemas (): ValidationError[] { return Object.entries(this.spec.schemas as Record).map(([key, _schema]) => { - const schema = _schema as Record - const error = this.validate_schema(schema, `#/components/schemas/${key}`) - if (error == null) return + const message = this.json_validator.validate_schema(_schema) + if (message == null) return const file = `schemas/${key.split(':')[0]}.yaml` const location = `#/components/schemas/${key.split(':')[1]}` - return this.error(file, location, error) + return this.error(file, location, message) }).filter((error) => error != null) as ValidationError[] } validate_parameter_schemas (): ValidationError[] { return Object.entries(this.spec.parameters as Record).map(([key, param]) => { - const error = this.validate_schema(param.schema as Record) - if (error == null) return + const message = this.json_validator.validate_schema(param.schema) + if (message == null) return const namespace = this.group_to_namespace(key.split('::')[0]) const file = namespace === '_global' ? '_global_parameters.yaml' : `namespaces/${namespace}.yaml` const location = namespace === '_global' ? param.name as string : `#/components/parameters/${key}` - return this.error(file, location, error) + return this.error(file, location, message) }).filter((error) => error != null) as ValidationError[] } @@ -94,31 +85,18 @@ export default class SchemasValidator { validate_content_schemas (file: string, location: string, content: Record | undefined): ValidationError[] { return Object.entries(content ?? {}).map(([media_type, value]) => { - const schema = value.schema as Record - const error = this.validate_schema(schema) - if (error != null) return this.error(file, `${location}/content/${media_type}`, error) + const message = this.json_validator.validate_schema(value.schema) + if (message != null) return this.error(file, `${location}/content/${media_type}`, message) }).filter(e => e != null) as ValidationError[] } - validate_schema (schema: Record, key: string | undefined = undefined): Error | undefined { - if (schema == null || schema.$ref != null) return - try { - if (key != null) this.ajv.addSchema(schema, key) - this.ajv.compile(schema) - } catch (_e: any) { - const error = _e as Error - for (const prefix of IGNORED_ERROR_PREFIXES) if (error.message.startsWith(prefix)) return - return error - } - } - group_to_namespace (group: string): string { if (group === '_global') return '_global' const [, namespace] = group.split('.').reverse() return namespace ?? '_core' } - error (file: string, location: string, error: Error): ValidationError { - return { file, location, message: error.message } + error (file: string, location: string, message: string): ValidationError { + return { file, location, message } } } diff --git a/tools/src/linter/components/base/FileValidator.ts b/tools/src/linter/components/base/FileValidator.ts index a6f9da82e..d5f144dba 100644 --- a/tools/src/linter/components/base/FileValidator.ts +++ b/tools/src/linter/components/base/FileValidator.ts @@ -10,9 +10,8 @@ import ValidatorBase from './ValidatorBase' import { type ValidationError } from 'types' import { type OpenAPIV3 } from 'openapi-types' -import { read_yaml, to_json } from '../../../helpers' -import AJV from 'ajv' -import addFormats from 'ajv-formats' +import { read_yaml } from '../../../helpers' +import JsonSchemaValidator from "../../../_utils/JsonSchemaValidator"; export default class FileValidator extends ValidatorBase { file_path: string @@ -61,11 +60,10 @@ export default class FileValidator extends ValidatorBase { const json_schema_path: string = (this.spec() as any).$schema ?? '' if (json_schema_path === '') return this.error('JSON Schema is required but not found in this file.', '$schema') const schema = read_yaml(json_schema_path) - const ajv = new AJV({ schemaId: 'id' }) - addFormats(ajv) - const validator = ajv.compile(schema) - if (!validator(this.spec())) { - return this.error(`File content does not match JSON schema found in '${json_schema_path}':\n ${to_json(validator.errors)}`) - } + delete schema.$schema + const validator = new JsonSchemaValidator() + const message = validator.validate_data(this.spec(), schema) + if (message != null) + return this.error(`File content does not match JSON schema found in '${json_schema_path}': ${message}`) } } diff --git a/tools/src/tester/SchemaValidator.ts b/tools/src/tester/SchemaValidator.ts index eddf54a09..1dc2898d1 100644 --- a/tools/src/tester/SchemaValidator.ts +++ b/tools/src/tester/SchemaValidator.ts @@ -7,13 +7,12 @@ * compatible open source license. */ -import AJV from 'ajv' -import ajv_errors from 'ajv-errors' -import addFormats from 'ajv-formats' +import JsonSchemaValidator from "../_utils/JsonSchemaValidator"; import { type OpenAPIV3 } from 'openapi-types' import { type Evaluation, Result } from './types/eval.types' import { Logger } from 'Logger' import { to_json } from '../helpers' +import _ from 'lodash' const ADDITIONAL_KEYWORDS = [ 'discriminator', @@ -24,36 +23,24 @@ const ADDITIONAL_KEYWORDS = [ ] export default class SchemaValidator { - private readonly ajv: AJV + private readonly json_validator: JsonSchemaValidator private readonly logger: Logger constructor (spec: OpenAPIV3.Document, logger: Logger) { this.logger = logger - this.ajv = new AJV({ allErrors: true, strict: true }) - addFormats(this.ajv) - for (const keyword of ADDITIONAL_KEYWORDS) this.ajv.addKeyword(keyword) - ajv_errors(this.ajv, { singleError: true }) - const schemas = spec.components?.schemas ?? {} - for (const key in schemas) this.ajv.addSchema(schemas[key], `#/components/schemas/${key}`) + const component_schemas = spec.components?.schemas ?? {} + const reference_schemas = _.mapKeys(component_schemas, (_, name) => `#/components/schemas/${name}`) + this.json_validator = new JsonSchemaValidator(undefined, { reference_schemas, additional_keywords: ADDITIONAL_KEYWORDS, ajv_errors_opts: { singleError: true } }) } validate (schema: OpenAPIV3.SchemaObject, data: any): Evaluation { - const validate = this.ajv.compile(schema) - const valid = validate(data) - if (!valid) { + const message = this.json_validator.validate_data(data as Record, schema) + if (message != null) { this.logger.info(`# ${to_json(schema)}`) this.logger.info(`* ${to_json(data)}`) - this.logger.info(`& ${to_json(validate.errors)}`) + this.logger.info(`& ${to_json(message)}`) + return { result: Result.FAILED, message } } - - var result: Evaluation = { - result: valid ? Result.PASSED : Result.FAILED, - } - - if (!valid) { - result.message = this.ajv.errorsText(validate.errors) - } - - return result + return { result: Result.PASSED } } } diff --git a/tools/src/tester/StoryValidator.ts b/tools/src/tester/StoryValidator.ts index 6b15228d5..e9a2ef097 100644 --- a/tools/src/tester/StoryValidator.ts +++ b/tools/src/tester/StoryValidator.ts @@ -9,20 +9,16 @@ import { Result, StoryEvaluation, StoryFile } from "./types/eval.types"; import * as path from "path"; -import { Ajv2019, ValidateFunction } from 'ajv/dist/2019' -import addFormats from 'ajv-formats' import { read_yaml } from "../helpers"; +import JsonSchemaValidator from "../_utils/JsonSchemaValidator"; export default class StoryValidator { private static readonly SCHEMA_FILE = path.resolve("./json_schemas/test_story.schema.yaml") - private readonly ajv: Ajv2019 - private readonly validate_schema: ValidateFunction + private readonly json_validator: JsonSchemaValidator constructor() { - this.ajv = new Ajv2019({ allErrors: true, strict: false }) - addFormats(this.ajv) const schema = read_yaml(StoryValidator.SCHEMA_FILE) - this.validate_schema = this.ajv.compile(schema) + this.json_validator = new JsonSchemaValidator(schema, { ajv_opts: { strictTypes: false } }) } validate(story_file: StoryFile): StoryEvaluation | undefined { @@ -39,8 +35,8 @@ export default class StoryValidator { } #validate_schema(story_file: StoryFile): StoryEvaluation | undefined { - const valid = this.validate_schema(story_file.story) - if (!valid) return this.#invalid(story_file, this.ajv.errorsText(this.validate_schema.errors)) + const message = this.json_validator.validate_data(story_file.story) + if (message != null) return this.#invalid(story_file, message) } #invalid({ story, display_path, full_path }: StoryFile, message: string): StoryEvaluation { diff --git a/tools/tests/_utils/AjvErrorsParser.test.ts b/tools/tests/_utils/AjvErrorsParser.test.ts new file mode 100644 index 000000000..60b16bb89 --- /dev/null +++ b/tools/tests/_utils/AjvErrorsParser.test.ts @@ -0,0 +1,62 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import { Ajv2019 } from "ajv/dist/2019"; +import AjvErrorsParser from "../../src/_utils/AjvErrorsParser"; + +describe('AjvErrorsParser', () => { + const ajv = new Ajv2019({ allErrors: true, strict: false }) + const parser = new AjvErrorsParser(ajv, { separator: ' | ' , dataVar: 'Obj' }) + const schema = { + type: 'object', + additionalProperties: false, + required: [ 'a_boolean', 'a_number', 'an_array', 'an_enum', 'a_compound_object'], + properties: { + a_boolean: { type: 'boolean' }, + a_number: { type: 'number' }, + a_nullable_string: { type: ['string', 'null'] }, + a_non_nullable_string: { type: 'string' }, + an_array: { type: 'array', items: { type: 'string' }, minItems: 1 }, + an_enum: { type: 'string', enum: ['a', 'b', 'c'] }, + a_compound_object: { + unevaluatedProperties: false, + allOf: [ + { type: 'object', properties: { a: { type: 'string' } } }, + { type: 'object', properties: { b: { type: 'number' } } }, + ], + } + } + } + + const data = { + an_array: [], + an_enum: 'd', + a_compound_object: { stranger: 'danger', hello: 'world' }, + space_odyssey: 42, + thirteen_sentinels: 426 + } + + const func = ajv.compile(schema) + func(data) + + it('can parse multiple errors', () => { + expect(parser.parse(func.errors)).toEqual( + 'Obj contains unsupported properties: space_odyssey, thirteen_sentinels, stranger, hello | ' + + 'Obj MUST contain the missing properties: a_boolean, a_number | ' + + 'Obj/an_enum MUST be equal to one of the allowed values: a, b, c | ' + + 'Obj/an_array must NOT have fewer than 1 items' + ) + }); + + it('can parse empty errors', () => { + const empty_func = ajv.compile({ type: 'object' }) + empty_func({}) + expect(parser.parse(empty_func.errors)).toEqual(ajv.errorsText(undefined)) + }); +}); \ No newline at end of file diff --git a/tools/tests/_utils/JsonSchemaValidator.test.ts b/tools/tests/_utils/JsonSchemaValidator.test.ts new file mode 100644 index 000000000..474dc1206 --- /dev/null +++ b/tools/tests/_utils/JsonSchemaValidator.test.ts @@ -0,0 +1,86 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import JsonSchemaValidator from "../../src/_utils/JsonSchemaValidator"; +import _ from 'lodash'; + +describe('JsonSchemaValidator', () => { + const options = { + ajv_opts: { discriminator: true }, + ajv_errors_opts: { singleError: true }, + errors_text_opts: { separator: ' <*> ' }, + additional_keywords: ['x-extension'], + reference_schemas: { '#/definitions/name': { type: 'string' } } + } + + const schema = { + type: 'object', + properties: { + name: { $ref: '#/definitions/name' }, + age: { type: 'number' }, + occupation: { type: 'string', enum: ['student', 'teacher', 'engineer'] }, + parents: { + type: 'object', + properties: { + mother: { $ref: '#/definitions/name' }, + father: { $ref: '#/definitions/name' } + }, + additionalProperties: { + not: true, + errorMessage: "property is not defined in the spec" + } + } + }, + required: ['name', 'age'] + }; + + const validator = new JsonSchemaValidator(schema, options); + + test('validate_data()', () => { + const valid_data = { + name: 'John Doe', + age: 25, + occupation: 'student' + } + expect(validator.validate_data(valid_data)).toBeUndefined(); + + const invalid_data = { + name: 'John Doe', + age: '25', + occupation: 'doctor', + parents: { + mom: 'Jane Doe', + dad: 'Jack Doe' + } + } + expect(validator.validate_data(invalid_data)).toEqual( + 'data/occupation MUST be equal to one of the allowed values: student, teacher, engineer <*> ' + + 'data/age must be number <*> ' + + 'data/parents/mom property is not defined in the spec <*> ' + + 'data/parents/dad property is not defined in the spec'); + + const override_schema = _.cloneDeep(schema) + _.set(override_schema, 'properties.occupation.enum', ['student', 'teacher', 'engineer', 'doctor']); + _.set(override_schema, 'properties.parents.additionalProperties', false) + expect(validator.validate_data(invalid_data, override_schema)).toEqual( + 'data/parents contains unsupported properties: mom, dad <*> ' + + 'data/age must be number'); + + const no_schema_validator = new JsonSchemaValidator(undefined, options); + expect(() => no_schema_validator.validate_data(valid_data)).toThrow('No schema provided'); + }); + + test('validate_schema()', () => { + expect(validator.validate_schema(schema)).toBeUndefined() + + const invalid_schema = _.set(_.cloneDeep(schema), 'required', true); + + expect(validator.validate_schema(invalid_schema)).toEqual('data/required must be array'); + }); +}); \ No newline at end of file diff --git a/tools/tests/linter/SchemasValidator.test.ts b/tools/tests/linter/SchemasValidator.test.ts index c2d48dc4f..c0929b6ec 100644 --- a/tools/tests/linter/SchemasValidator.test.ts +++ b/tools/tests/linter/SchemasValidator.test.ts @@ -16,12 +16,12 @@ test('validate() - named_schemas', () => { { file: 'schemas/actions.yaml', location: '#/components/schemas/Bark', - message: 'schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf' + message: 'data/type MUST be equal to one of the allowed values: array, boolean, integer, null, number, object, string --- data/type must be array --- data/type must match a schema in anyOf' }, { file: 'schemas/animals.yaml', location: '#/components/schemas/Dog', - message: 'schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf' + message: 'data/type MUST be equal to one of the allowed values: array, boolean, integer, null, number, object, string --- data/type must be array --- data/type must match a schema in anyOf' } ]) }) @@ -32,22 +32,22 @@ test('validate() - anonymous_schemas', () => { { file: '_global_parameters.yaml', location: 'human', - message: 'schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf' + message: 'data/type MUST be equal to one of the allowed values: array, boolean, integer, null, number, object, string --- data/type must be array --- data/type must match a schema in anyOf' }, { file: 'namespaces/_core.yaml', location: '#/components/parameters/adopt::path.docket', - message: 'schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf' + message: 'data/type MUST be equal to one of the allowed values: array, boolean, integer, null, number, object, string --- data/type must be array --- data/type must match a schema in anyOf' }, { file: 'namespaces/adopt.yaml', location: '#/components/requestBodies/adopt/content/application/json', - message: 'schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf' + message: 'data/type MUST be equal to one of the allowed values: array, boolean, integer, null, number, object, string --- data/type must be array --- data/type must match a schema in anyOf' }, { file: 'namespaces/_core.yaml', location: '#/components/responses/adopt@200/content/application/json', - message: 'schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf' + message: 'data/type MUST be equal to one of the allowed values: array, boolean, integer, null, number, object, string --- data/type must be array --- data/type must match a schema in anyOf' } ]) }) diff --git a/tools/tests/linter/SupersededOperationsFile.test.ts b/tools/tests/linter/SupersededOperationsFile.test.ts index 423b68520..db68fdc1b 100644 --- a/tools/tests/linter/SupersededOperationsFile.test.ts +++ b/tools/tests/linter/SupersededOperationsFile.test.ts @@ -15,26 +15,7 @@ describe('validate()', () => { expect(validator.validate()).toEqual([ { file: 'superseded_operations/invalid_schema.yaml', - message: "File content does not match JSON schema found in './json_schemas/_superseded_operations.schema.yaml':\n " + - JSON.stringify([ - { - "instancePath": "/~1hello~1world/operations/1", - "schemaPath": "#/patternProperties/%5E~1/properties/operations/items/enum", - "keyword": "enum", - "params": { - "allowedValues": [ - "GET", - "POST", - "PUT", - "DELETE", - "HEAD", - "OPTIONS", - "PATCH" - ] - }, - "message": "must be equal to one of the allowed values" - } - ], null, 2), + message: "File content does not match JSON schema found in './json_schemas/_superseded_operations.schema.yaml': data/~1hello~1world/operations/1 MUST be equal to one of the allowed values: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" }, ]) }) diff --git a/tools/tests/tester/StoryValidator.test.ts b/tools/tests/tester/StoryValidator.test.ts index c3a42d372..4a755b7a2 100644 --- a/tools/tests/tester/StoryValidator.test.ts +++ b/tools/tests/tester/StoryValidator.test.ts @@ -29,7 +29,10 @@ describe('StoryValidator', () => { test('invalid story', () => { const evaluation = validate('tools/tests/tester/fixtures/invalid_story.yaml') expect(evaluation?.result).toBe('ERROR') - expect(evaluation?.message).toBe("Invalid Story: data/epilogues/0 must NOT have unevaluated properties, data/chapters/0 must have required property 'method', data/chapters/1/method must be equal to one of the allowed values") + expect(evaluation?.message).toBe("Invalid Story: " + + "data/epilogues/0 contains unsupported properties: response --- " + + "data/chapters/0 MUST contain the missing properties: method --- " + + "data/chapters/1/method MUST be equal to one of the allowed values: GET, PUT, POST, DELETE, PATCH, HEAD, OPTIONS") }) test('valid story', () => { diff --git a/tools/tests/tester/fixtures/evals/failed/invalid_data.yaml b/tools/tests/tester/fixtures/evals/failed/invalid_data.yaml index d61215a8d..ec0c9043f 100644 --- a/tools/tests/tester/fixtures/evals/failed/invalid_data.yaml +++ b/tools/tests/tester/fixtures/evals/failed/invalid_data.yaml @@ -33,7 +33,7 @@ chapters: result: PASSED request_body: result: FAILED - message: data must NOT have additional properties + message: 'data contains unsupported properties: aliases' response: status: result: PASSED @@ -77,7 +77,7 @@ chapters: message: expected acknowledged='false', got 'true', missing shards_acknowledged='true' payload_schema: result: FAILED - message: data must NOT have additional properties + message: 'data contains unsupported properties: acknowledged' - title: This chapter should fail because the response status does not match. overall: result: ERROR diff --git a/tools/tests/tester/test.test.ts b/tools/tests/tester/test.test.ts index e2b2ea330..b82e2abd7 100644 --- a/tools/tests/tester/test.test.ts +++ b/tools/tests/tester/test.test.ts @@ -41,7 +41,7 @@ test('displays story filename', () => { test('invalid story', () => { expect(spec(['--tests', 'tools/tests/tester/fixtures/invalid_story.yaml']).stdout).toContain( - `${ansi.gray("(Invalid Story: data/epilogues/0 must NOT have unevaluated properties, ...)")}` + `\x1b[90m(Invalid Story:` ) })