diff --git a/.vscode/launch.json b/.vscode/launch.json index fb87b664..859ce6df 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,8 @@ "--nolazy" ], "env": { - "NODE_ENV": "development" + "NODE_ENV": "development", + "NODE_TLS_REJECT_UNAUTHORIZED": "0" }, "console": "internalConsole", "sourceMaps": false, diff --git a/README.md b/README.md index ed639468..bdd9a11f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,10 @@ Features - Tested on **[over 1,500 real-world APIs](https://apis.guru/browse-apis/)** from Google, Microsoft, Facebook, Spotify, etc. - Supports [circular references](https://apitools.dev/swagger-parser/docs/#circular-refs), nested references, back-references, and cross-references - Maintains object reference equality — `$ref` pointers to the same value always resolve to the same object instance - +- Checks for inconsistencies in Swagger v2.0 and OpenAPI v3.0 specs: + - path parameter mis-matches + - required field mis-matches + - arrays without item definition Related Projects diff --git a/lib/index.d.ts b/lib/index.d.ts index fc9c618b..6dd459c0 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -206,9 +206,10 @@ declare class SwaggerParser { // eslint-disable-next-line no-redeclare declare namespace SwaggerParser { - + /* eslint-disable @typescript-eslint/no-explicit-any */ export type ApiCallback = (err: Error | null, api?: OpenAPI.Document) => any; export type $RefsCallback = (err: Error | null, $refs?: $Refs) => any; + /* eslint-enable */ /** * See https://apitools.dev/swagger-parser/docs/options.html @@ -323,6 +324,7 @@ declare namespace SwaggerParser { */ read( file: FileInfo, + // eslint-disable-next-line @typescript-eslint/no-explicit-any callback?: (error: Error | null, data: string | null) => any ): string | Buffer | Promise; } @@ -407,6 +409,7 @@ declare namespace SwaggerParser { * * @param types (optional) Optionally only return values from certain locations ("file", "http", etc.) */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any public values(...types: string[]): { [url: string]: any } /** @@ -425,6 +428,7 @@ declare namespace SwaggerParser { * * @param $ref The JSON Reference path, optionally with a JSON Pointer in the hash */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any public get($ref: string): any /** @@ -433,6 +437,7 @@ declare namespace SwaggerParser { * @param $ref The JSON Reference path, optionally with a JSON Pointer in the hash * @param value The value to assign. Can be anything (object, string, number, etc.) */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any public set($ref: string, value: any): void } diff --git a/lib/index.js b/lib/index.js index 22514465..5367df76 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,7 +5,7 @@ const validateSchema = require("./validators/schema"); const validateSpec = require("./validators/spec"); const normalizeArgs = require("@apidevtools/json-schema-ref-parser/lib/normalize-args"); const util = require("./util"); -const Options = require("./options"); +const ParserOptions = require("./options"); const maybe = require("call-me-maybe"); const { ono } = require("@jsdevtools/ono"); const $RefParser = require("@apidevtools/json-schema-ref-parser"); @@ -54,7 +54,7 @@ Object.defineProperty(SwaggerParser.prototype, "api", { */ SwaggerParser.prototype.parse = async function (path, api, options, callback) { let args = normalizeArgs(arguments); - args.options = new Options(args.options); + args.options = new ParserOptions(args.options); try { let schema = await $RefParser.prototype.parse.call(this, args.path, args.schema, args.options); @@ -150,7 +150,7 @@ SwaggerParser.validate = function (path, api, options, callback) { SwaggerParser.prototype.validate = async function (path, api, options, callback) { let me = this; let args = normalizeArgs(arguments); - args.options = new Options(args.options); + args.options = new ParserOptions(args.options); // ZSchema doesn't support circular objects, so don't dereference circular $refs yet // (see https://github.com/zaggino/z-schema/issues/137) @@ -182,8 +182,8 @@ SwaggerParser.prototype.validate = async function (path, api, options, callback) } if (args.options.validate.spec) { - // Validate the API against the Swagger spec - validateSpec(me.api); + // Validate the API against the Swagger spec; hand in $refs.circular meta-data from the SwaggerParser + validateSpec(me.api, me.$refs.circular); } return maybe(args.callback, Promise.resolve(me.schema)); @@ -193,9 +193,5 @@ SwaggerParser.prototype.validate = async function (path, api, options, callback) } }; -/** - * The Swagger object - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object - * - * @typedef {{swagger: string, info: {}, paths: {}}} SwaggerObject - */ +// Only the one export; don't try to export the @typedef of SwaggerOrOpenAPIObject +module.exports = SwaggerParser; diff --git a/lib/validators/schema.js b/lib/validators/schema.js index d22ea681..f88be694 100644 --- a/lib/validators/schema.js +++ b/lib/validators/schema.js @@ -6,12 +6,23 @@ const AjvDraft4 = require("ajv-draft-04"); const Ajv = require("ajv/dist/2020"); const { openapi } = require("@apidevtools/openapi-schemas"); +/** + * The Swagger v2.0 or OpenAPI v3.0.x object - it could be either (but not both) + * + * cf. + * - https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object + * - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#oasObject + * + * @typedef {{swagger: string, info: {}, paths: {}, + * openapi:string, }} SwaggerOrOpenAPIObject + */ + module.exports = validateSchema; /** * Validates the given Swagger API against the Swagger 2.0 or OpenAPI 3.0 and 3.1 schemas. * - * @param {SwaggerObject} api + * @param {SwaggerOrOpenAPIObject} api Either a Swagger or OpenAPI object - determined by presence of swagger, or openapi fields */ function validateSchema (api) { let ajv; @@ -59,8 +70,8 @@ function validateSchema (api) { /** * Determines which version of Ajv to load and prepares it for use. * - * @param {bool} draft04 - * @returns {Ajv} + * @param {boolean} draft04 Are we initialising for JsonSchemaDraft04? + * @returns {Ajv} The initialized Ajv environment */ function initializeAjv (draft04 = true) { const opts = { @@ -81,7 +92,7 @@ function initializeAjv (draft04 = true) { * * @param {object[]} errors - The Ajv errors * @param {string} [indent] - The whitespace used to indent the error message - * @returns {string} + * @returns {string} - Formatted error message string */ function formatAjvError (errors, indent) { indent = indent || " "; diff --git a/lib/validators/spec.js b/lib/validators/spec.js index 21e965d3..3e330349 100644 --- a/lib/validators/spec.js +++ b/lib/validators/spec.js @@ -9,44 +9,153 @@ const schemaTypes = ["array", "boolean", "integer", "number", "string", "object" module.exports = validateSpec; /** - * Validates parts of the Swagger 2.0 spec that aren't covered by the Swagger 2.0 JSON Schema. + * Validates parts of the Swagger 2.0 spec that aren't covered by the Swagger 2.0 JSON Schema; + * and parts of the OpenAPI v3.0.2 spec that aren't covered by the OpenAPI 3.0 JSON SChema * - * @param {SwaggerObject} api + * @param {object} api - the entire Swagger API object (plus some isCircular meta data from the parser) + * @param {object} isCircular - meta data from the SwaggerParser; does the API have circular $refs ?? */ -function validateSpec (api) { - if (api.openapi) { - // We don't (yet) support validating against the OpenAPI spec - return; - } +function validateSpec (api, isCircular) { let paths = Object.keys(api.paths || {}); let operationIds = []; + + // accumulate errors + let message = "Specification check failed.\n"; + let isValid = true; + + // Check all the paths for (let pathName of paths) { let path = api.paths[pathName]; let pathId = "/paths" + pathName; - if (path && pathName.indexOf("/") === 0) { - validatePath(api, path, pathId, operationIds); + try { + + // ...and off we go ... + if (path && pathName.indexOf("/") === 0) { + validatePath(api, isCircular, path, pathId, operationIds); + } + } + catch (err) { + message += err.message; + isValid = false; + } + + // in OpenAPI v3.0 #/components holds lots of different definitions... + if (api.openapi) { + // #/components/schemas (don't have contentType) + let schemaNames = Object.keys(api.components?.schemas || {}); + for (let schemaName of schemaNames) { + + let schema = api.components.schemas[schemaName]; + let schemaId = "/components/schemas/" + schemaName; + + try { + validateSchema(schema, schemaId, schemaTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + + // #/components/parameters (don't have contentTypes) + // NOTE: the #/components/parameters that have been used, have already been cheked. + let paramNames = Object.keys(api.components?.parameters || {}); + for (let paramName of paramNames) { + + let schema = api.components.parameters[paramName].schema; + let schemaId = "/components/parameters/" + paramName + "/schema"; + + try { + validateSchema(schema, schemaId, schemaTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + + // #/components/requestBodies + let reqBodyNames = Object.keys(api.components?.requestBodies || {}); + for (let reqBodyName of reqBodyNames) { + + // Loop through the contentTypes + let contentTypes = Object.keys(api.components.requestBodies[reqBodyName].content || {}); + for (let contentType of contentTypes) { + + let schema = api.components.requestBodies[reqBodyName].content[contentType].schema; + let schemaId = "/components/requestBodies/" + reqBodyName + "/content/" + contentType + "/schema"; + + try { + validateSchema(schema, schemaId, schemaTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } + + // #/components/responses + let rspNames = Object.keys(api.components?.responses || {}); + for (let rspName of rspNames) { + + // Loop through the contentTypes + let contentTypes = Object.keys(api.components.responses[rspName].content || {}); + for (let contentType of contentTypes) { + + let schema = api.components.responses[rspName].content[contentType].schema; + let schemaId = "/components/responses/" + rspName + "/content/" + contentType + "/schema"; + + try { + validateSchema(schema, schemaId, schemaTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } + } + else { + // ... in Swagger v2.0 they were all slapped in definitions + let definitions = Object.keys(api.definitions || {}); + for (let definitionName of definitions) { + let definition = api.definitions[definitionName]; + let definitionId = "/definitions/" + definitionName; + + try { + validateSchema(definition, definitionId, schemaTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } } } - let definitions = Object.keys(api.definitions || {}); - for (let definitionName of definitions) { - let definition = api.definitions[definitionName]; - let definitionId = "/definitions/" + definitionName; - validateRequiredPropertiesExist(definition, definitionId); + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); } } /** * Validates the given path. * - * @param {SwaggerObject} api - The entire Swagger API object - * @param {object} path - A Path object, from the Swagger API + * @param {object} api - The entire Swagger/OpenAPI API object + * @param {object} isCircular - meta data from the SwaggerParser; does the API have circular $refs ?? + * @param {object} path - A Path object, from the Swagger/OpenAPI API * @param {string} pathId - A value that uniquely identifies the path * @param {string} operationIds - An array of collected operationIds found in other paths */ -function validatePath (api, path, pathId, operationIds) { +function validatePath (api, isCircular, path, pathId, operationIds) { + // accumulate errors + let message = ""; + let isValid = true; + for (let operationName of swaggerMethods) { let operation = path[operationName]; let operationId = pathId + "/" + operationName; @@ -58,40 +167,67 @@ function validatePath (api, path, pathId, operationIds) { operationIds.push(declaredOperationId); } else { - throw ono.syntax(`Validation failed. Duplicate operation id '${declaredOperationId}'`); + message += `Validation failed. Duplicate operation id '${declaredOperationId}'\n`; + isValid = false; } } - validateParameters(api, path, pathId, operation, operationId); + try { + validateParameters(api, isCircular, path, pathId, operation, operationId); + } + catch (err) { + message += err.message; + isValid = false; + } + + // Don't forget to validate the Request + // Zoop through all the responses let responses = Object.keys(operation.responses || {}); for (let responseName of responses) { let response = operation.responses[responseName]; let responseId = operationId + "/responses/" + responseName; - validateResponse(responseName, (response || {}), responseId); + try { + validateResponse(api, isCircular, responseName, (response || {}), responseId); + } + catch (err) { + message += err.message; + isValid = false; + } } } } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } /** * Validates the parameters for the given operation. * - * @param {SwaggerObject} api - The entire Swagger API object + * @param {object} api - The entire Swagger/OpenAPI API object + * @param {object} isCircular - meta data from the SwaggerParser; does the API have circular $refs ?? * @param {object} path - A Path object, from the Swagger API * @param {string} pathId - A value that uniquely identifies the path * @param {object} operation - An Operation object, from the Swagger API * @param {string} operationId - A value that uniquely identifies the operation */ -function validateParameters (api, path, pathId, operation, operationId) { +function validateParameters (api, isCircular, path, pathId, operation, operationId) { let pathParams = path.parameters || []; let operationParams = operation.parameters || []; + // accumulate errors + let message = ""; + let isValid = true; + // Check for duplicate path parameters try { checkForDuplicates(pathParams); } catch (e) { - throw ono.syntax(e, `Validation failed. ${pathId} has duplicate parameters`); + message += `Validation failed. ${pathId} has duplicate parameters\n` + e.message; + isValid = false; } // Check for duplicate operation parameters @@ -99,7 +235,8 @@ function validateParameters (api, path, pathId, operation, operationId) { checkForDuplicates(operationParams); } catch (e) { - throw ono.syntax(e, `Validation failed. ${operationId} has duplicate parameters`); + message += `Validation failed. ${operationId} has duplicate parameters\n` + e.message; + isValid = false; } // Combine the path and operation parameters, @@ -114,9 +251,32 @@ function validateParameters (api, path, pathId, operation, operationId) { return combinedParams; }, operationParams.slice()); - validateBodyParameters(params, operationId); - validatePathParameters(params, pathId, operationId); - validateParameterTypes(params, api, operation, operationId); + try { + validateBodyParameters(params, operationId); + } + catch (err) { + message += err.message; + isValid = false; + } + try { + validatePathParameters(params, pathId, operationId); + } + catch (err) { + message += err.message; + isValid = false; + } + try { + validateParameterTypes(params, api, isCircular, operation, operationId); + } + catch (err) { + message += err.message; + isValid = false; + } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } /** @@ -129,17 +289,26 @@ function validateBodyParameters (params, operationId) { let bodyParams = params.filter((param) => { return param.in === "body"; }); let formParams = params.filter((param) => { return param.in === "formData"; }); + // accumulate errors + let message = ""; + let isValid = true; + // There can only be one "body" parameter if (bodyParams.length > 1) { - throw ono.syntax( - `Validation failed. ${operationId} has ${bodyParams.length} body parameters. Only one is allowed.`, - ); + message += + `Validation failed. ${operationId} has ${bodyParams.length} body parameters. Only one is allowed.\n`; + isValid = false; } else if (bodyParams.length > 0 && formParams.length > 0) { // "body" params and "formData" params are mutually exclusive - throw ono.syntax( - `Validation failed. ${operationId} has body parameters and formData parameters. Only one or the other is allowed.`, - ); + message += + `Validation failed. ${operationId} has body parameters and formData parameters. Only one or the other is allowed.\n`; + isValid = false; + } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); } } @@ -154,12 +323,17 @@ function validatePathParameters (params, pathId, operationId) { // Find all {placeholders} in the path string let placeholders = pathId.match(util.swaggerParamRegExp) || []; + // accumulate errors + let message = ""; + let isValid = true; + // Check for duplicates for (let i = 0; i < placeholders.length; i++) { for (let j = i + 1; j < placeholders.length; j++) { if (placeholders[i] === placeholders[j]) { - throw ono.syntax( - `Validation failed. ${operationId} has multiple path placeholders named ${placeholders[i]}`); + message += + `Validation failed. ${operationId} has multiple path placeholders named ${placeholders[i]}\n`; + isValid = false; } } } @@ -168,23 +342,29 @@ function validatePathParameters (params, pathId, operationId) { for (let param of params) { if (param.required !== true) { - throw ono.syntax( + message += "Validation failed. Path parameters cannot be optional. " + - `Set required=true for the "${param.name}" parameter at ${operationId}`, - ); + `Set required=true for the "${param.name}" parameter at ${operationId}\n`; + isValid = false; } let match = placeholders.indexOf("{" + param.name + "}"); if (match === -1) { - throw ono.syntax( + message += `Validation failed. ${operationId} has a path parameter named "${param.name}", ` + - `but there is no corresponding {${param.name}} in the path string` - ); + `but there is no corresponding {${param.name}} in the path string\n`; + isValid = false; } placeholders.splice(match, 1); } if (placeholders.length > 0) { - throw ono.syntax(`Validation failed. ${operationId} is missing path parameter(s) for ${placeholders}`); + message += `Validation failed. ${operationId} is missing path parameter(s) for ${placeholders}\n`; + isValid = false; + } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); } } @@ -193,32 +373,51 @@ function validatePathParameters (params, pathId, operationId) { * * @param {object[]} params - An array of Parameter objects * @param {object} api - The entire Swagger API object + * @param {object} isCircular - meta data from the SwaggerParser; does the API have circular $refs ?? * @param {object} operation - An Operation object, from the Swagger API * @param {string} operationId - A value that uniquely identifies the operation */ -function validateParameterTypes (params, api, operation, operationId) { +function validateParameterTypes (params, api, isCircular, operation, operationId) { + + // accumulate errors + let message = ""; + let isValid = true; + for (let param of params) { let parameterId = operationId + "/parameters/" + param.name; let schema, validTypes; - switch (param.in) { - case "body": - schema = param.schema; - validTypes = schemaTypes; - break; - case "formData": - schema = param; - validTypes = primitiveTypes.concat("file"); - break; - default: - schema = param; - validTypes = primitiveTypes; + // schema is always inside 'schema' tag in openapi specs... + if (api.openapi) { + schema = param.schema; + validTypes = schemaTypes; + } + else { + // Swagger 2.0 was different... + switch (param.in) { + case "body": + schema = param.schema; + validTypes = schemaTypes; + break; + case "formData": + schema = param; + validTypes = primitiveTypes.concat("file"); + break; + default: + schema = param; + validTypes = primitiveTypes; + } } - validateSchema(schema, parameterId, validTypes); - validateRequiredPropertiesExist(schema, parameterId); + try { + validateSchema(schema, parameterId, validTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } - if (schema.type === "file") { + if (schema?.type === "file") { // "file" params must consume at least one of these MIME types let formData = /multipart\/(.*\+)?form-data/; let urlEncoded = /application\/(.*\+)?x-www-form-urlencoded/; @@ -230,13 +429,18 @@ function validateParameterTypes (params, api, operation, operationId) { }); if (!hasValidMimeType) { - throw ono.syntax( + message += `Validation failed. ${operationId} has a file parameter, so it must consume multipart/form-data ` + - "or application/x-www-form-urlencoded", - ); + "or application/x-www-form-urlencoded\n"; + isValid = false; } } } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } /** @@ -245,46 +449,123 @@ function validateParameterTypes (params, api, operation, operationId) { * @param {object[]} params - An array of Parameter objects */ function checkForDuplicates (params) { + let message = ""; + let isValid = true; + for (let i = 0; i < params.length - 1; i++) { let outer = params[i]; for (let j = i + 1; j < params.length; j++) { let inner = params[j]; if (outer.name === inner.name && outer.in === inner.in) { - throw ono.syntax(`Validation failed. Found multiple ${outer.in} parameters named "${outer.name}"`); + message += `Validation failed. Found multiple ${outer.in} parameters named "${outer.name}"\n`; + isValid = false; } } } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } /** * Validates the given response object. * + * @param {object} api - The entire Swagger/OpenAPI API object + * @param {object} isCircular - meta data from the SwaggerParser; does the API have circular $refs ?? * @param {string} code - The HTTP response code (or "default") * @param {object} response - A Response object, from the Swagger API * @param {string} responseId - A value that uniquely identifies the response */ -function validateResponse (code, response, responseId) { +function validateResponse (api, isCircular, code, response, responseId) { + let message = ""; + let isValid = true; + if (code !== "default" && (code < 100 || code > 599)) { - throw ono.syntax(`Validation failed. ${responseId} has an invalid response code (${code})`); + message += `Validation failed. ${responseId} has an invalid response code (${code})\n`; + isValid = false; } + // Check response Headers let headers = Object.keys(response.headers || {}); for (let headerName of headers) { let header = response.headers[headerName]; let headerId = responseId + "/headers/" + headerName; - validateSchema(header, headerId, primitiveTypes); - } + let schema, validTypes; - if (response.schema) { - let validTypes = schemaTypes.concat("file"); - if (validTypes.indexOf(response.schema.type) === -1) { - throw ono.syntax( - `Validation failed. ${responseId} has an invalid response schema type (${response.schema.type})`); + // schema is always inside 'schema' tag in openapi specs... + if (api.openapi) { + schema = header.schema; + validTypes = schemaTypes; } else { - validateSchema(response.schema, responseId + "/schema", validTypes); + // Swagger 2.0 was different... + schema = header; + validTypes = primitiveTypes; + } + try { + validateSchema(schema, headerId, validTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + + // OpenAPI has different responses for each content-type + if (api.openapi) { + if (response.content) { + // Loop through the different content-types + let contentTypes = Object.keys(response.content); + for (let contentType of contentTypes) { + + let content = response.content[contentType]; + if (content.schema) { + let validTypes = schemaTypes.concat("file"); + if (validTypes.indexOf(content.schema.type) === -1) { + message += + `Validation failed. ${responseId}/content/${contentType}/schema has an invalid response schema type (${content.schema.type})\n`; + isValid = false; + } + else { + try { + validateSchema(content.schema, responseId + "/content/" + contentType + "/schema", validTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } + } + } + } + else { + // Swagger 2.0 was different + if (response.schema) { + let validTypes = schemaTypes.concat("file"); + if (validTypes.indexOf(response.schema.type) === -1) { + message += + `Validation failed. ${responseId} has an invalid response schema type (${response.schema.type})\n`; + isValid = false; + } + else { + try { + validateSchema(response.schema, responseId + "/schema", validTypes, isCircular); + } + catch (err) { + message += err.message; + isValid = false; + } + } } } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } /** @@ -293,15 +574,53 @@ function validateResponse (code, response, responseId) { * @param {object} schema - A Schema object, from the Swagger API * @param {string} schemaId - A value that uniquely identifies the schema object * @param {string[]} validTypes - An array of the allowed schema types + * @param {boolean} circular - Does the parent API have circular isCircular?? */ -function validateSchema (schema, schemaId, validTypes) { +function validateSchema (schema, schemaId, validTypes, circular) { + let message = ""; + let isValid = true; + + // make sure the schema type is known if (validTypes.indexOf(schema.type) === -1) { - throw ono.syntax( - `Validation failed. ${schemaId} has an invalid type (${schema.type})`); + message += + `Validation failed. ${schemaId} has an invalid type (${schema.type})\n`; + isValid = false; } + // make sure that arrays have items defined if (schema.type === "array" && !schema.items) { - throw ono.syntax(`Validation failed. ${schemaId} is an array, so it must include an "items" schema`); + message += `Validation failed. ${schemaId} is an array, so it must include an "items" schema\n`; + isValid = false; + } + + // make sure that all properties marked as 'required' actually exist + try { + validateRequiredPropertiesExist(schema, schemaId); + } + catch (err) { + message += err.message; + isValid = false; + } + + // Recursively check all the properties (BUT ONLY if the API has no circular $refs) + // TODO: Make this check work on circular $refs + if (!circular) { + let propNames = Object.keys(schema.properties || {}); + for (let propName of propNames) { + + try { + validateSchema(schema.properties[propName], schemaId + "/" + propName, schemaTypes, circular); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); } } @@ -312,6 +631,7 @@ function validateSchema (schema, schemaId, validTypes) { * @param {string} schemaId - A value that uniquely identifies the schema object */ function validateRequiredPropertiesExist (schema, schemaId) { + /** * Recursively collects all properties of the schema and its ancestors. They are added to the props object. */ @@ -330,15 +650,23 @@ function validateRequiredPropertiesExist (schema, schemaId) { } } + let message = ""; + let isValid = true; + if (schema.required && Array.isArray(schema.required)) { let props = {}; collectProperties(schema, props); for (let requiredProperty of schema.required) { if (!props[requiredProperty]) { - throw ono.syntax( - `Validation failed. Property '${requiredProperty}' listed as required but does not exist in '${schemaId}'` - ); + message += + `Validation failed. Property '${requiredProperty}' listed as required but does not exist in '${schemaId}'\n`; + isValid = false; } } } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } diff --git a/online/src/js/analytics.js b/online/src/js/analytics.js index 4876fe6b..3aa372f8 100644 --- a/online/src/js/analytics.js +++ b/online/src/js/analytics.js @@ -43,7 +43,7 @@ analytics.trackEvent = function (category, action, label, value) { /** * Tracks an error in Google Analytics * - * @param {Error} err + * @param {Error} err - The error we're tracking */ analytics.trackError = function (err) { try { diff --git a/online/src/js/dropdowns.js b/online/src/js/dropdowns.js index cd4b8b01..025627f2 100644 --- a/online/src/js/dropdowns.js +++ b/online/src/js/dropdowns.js @@ -42,8 +42,8 @@ function dropdowns () { * Calls the given function whenever the user selects (or deselects) * a value in the given drop-down menu. * - * @param {jQuery} menu - * @param {function} setLabel + * @param {jQuery} menu - dropdown menu we're using + * @param {Function} setLabel - value to be set (or unset) */ function onChange (menu, setLabel) { let dropdown = menu.parent(".dropdown"); @@ -144,7 +144,7 @@ function setSelectedMethod (methodName) { /** * Tracks changes to a checkbox option * - * @param {jQuery} checkbox + * @param {jQuery} checkbox - Checkbox that we're tracking changes for */ function trackCheckbox (checkbox) { checkbox.on("change", () => { @@ -166,8 +166,8 @@ function trackButtonLabel (methodName) { /** * Examines the given checkboxes, and returns arrays of checked and unchecked values. * - * @param {...jQuery} _checkboxes - * @returns {{checked: string[], unchecked: string[]}} + * @param {...jQuery} _checkboxes - Checkboxes we're checking + * @returns {{checked: string[], unchecked: string[]}} - Arrays of checked and unchecked values */ function getCheckedAndUnchecked (_checkboxes) { let checked = [], unchecked = []; diff --git a/online/src/js/editors.js b/online/src/js/editors.js index 9414593e..e8640103 100644 --- a/online/src/js/editors.js +++ b/online/src/js/editors.js @@ -44,7 +44,7 @@ editors.showResult = function (title, content) { /** * Displays an error result * - * @param {Error} err + * @param {Error} err - The error to be displayed */ editors.showError = function (err) { editors.results.removeClass("hidden").addClass("error"); @@ -95,8 +95,8 @@ editors.addResult = function (title, content) { /** * Returns a short version of the given title text, to better fit in a tab * - * @param {string} title - * @returns {string} + * @param {string} title - The Title we're shortening + * @returns {string} - The short version of the title */ function getShortTitle (title) { // Get just the file name @@ -134,8 +134,8 @@ function showResults () { * Converts the given object to text. * If possible, it is converted to JSON; otherwise, plain text. * - * @param {object} obj - * @returns {object} + * @param {object} obj - Object to be converted into text + * @returns {object} - JSON or plain-text version of the object */ function toText (obj) { if (obj instanceof Error) { diff --git a/online/src/js/querystring.js b/online/src/js/querystring.js index 3b63fb56..418a2c32 100644 --- a/online/src/js/querystring.js +++ b/online/src/js/querystring.js @@ -47,8 +47,8 @@ function setFormFields () { /** * Checks or unchecks the given checkbox, based on the given value. * - * @param {jQuery} input - * @param {*} value + * @param {jQuery} input - The checkbox we're manipulating + * @param {*} value - Checked or Unchecked? */ function setCheckbox (input, value) { if (!value || value === "true" || value === "on") { diff --git a/package.json b/package.json index 2b900584..16c76fbd 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "build:website": "simplifyify online/src/js/index.js --outfile online/js/bundle.js --bundle --debug --minify", "build:sass": "node-sass --source-map true --output-style compressed online/src/scss/style.scss online/css/style.min.css", "test": "npm run test:node && npm run test:typescript && npm run test:browser && npm run lint", + "test:quick": "mocha --quick-test", "test:node": "mocha", "test:browser": "karma start --single-run", "test:typescript": "tsc --noEmit --strict --lib esnext,dom test/specs/typescript-definition.spec.ts", diff --git a/test/specs/real-world/known-errors.js b/test/specs/real-world/known-errors.js index 8ce5172e..9fa7ba61 100644 --- a/test/specs/real-world/known-errors.js +++ b/test/specs/real-world/known-errors.js @@ -68,6 +68,62 @@ function getKnownApiErrors () { whatToDo: "ignore", }, + // adyen.com v3.1.0 schemas seem to be interesting ... + { + api: "adyen.com", + error: "must NOT have unevaluated properties", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "airbyte.local:config", + error: "Property 'connection' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "airbyte.local:config", + error: "Property 'json_schema' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "apicurio.local:registry", + error: "Property 'group' listed as required but does not exist", + whatToDo: "ignore" + }, + + // TODO something to investigate + { + api: "apideck.com:file-storage", + error: "Cannot read property 'type' of undefined", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "apideck.com:webhook", + error: "Property 'data' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "apideck.com:hris", + error: "Property 'name' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "atlassian.com:jira", + error: "Property 'defaultScreen' listed as required but does not exist", + whatToDo: "ignore" + }, + // Many Azure API definitions erroneously reference external files that don't exist. { api: "azure.com", @@ -96,6 +152,41 @@ function getKnownApiErrors () { whatToDo: "ignore", }, + // old field that used to exist included in 'required' ?? + { + api: "box.com", + error: " Property 'grant_type' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "britbox.co.uk", + error: "Property 'email' listed as required but does not exist ", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "byautomata.io", + error: "Property 'terms' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "cdcgov.local:prime-data-hub", + error: "Property 'jurisdictionalFilter' listed as required but does not exist", + whatToDo: "ignore" + }, + + // old field that used to exist included in 'required' ?? + { + api: "clicksend.com", + error: "/paths/uploads?convert={convert}/post is missing path parameter(s) for {convert}", + whatToDo: "ignore" + }, + // Cloudmersive.com's API definition contains invalid JSON Schema types { api: "cloudmersive.com:ocr", @@ -110,11 +201,37 @@ function getKnownApiErrors () { whatToDo: "ignore", }, + // old field that used to exist included in 'required' ?? + { + api: "dataflowkit.com", + error: "Property 'proxy' listed as required but does not exist ", + whatToDo: "ignore" + }, + + { + api: "digitallocker.gov.in:authpartner", + error: "Property 'id' listed as required but does not exist", + whatToDo: "ignore" + }, + { api: "enode.io", error: "schema/items must NOT have additional properties", whatToDo: "ignore" }, + + { + api: "etsi.local:MEC010-2_AppPkgMgmt", + error: "Property 'featureName' listed as required but does not exist", + whatToDo: "ignore" + }, + + { + api: "exavault.com", + error: "Property 'homeResource' listed as required but does not exist", + whatToDo: "ignore" + }, + { api: "frankiefinancial.io", error: "Property 'rowid' listed as required but does not exist", diff --git a/test/specs/real-world/real-world.spec.js b/test/specs/real-world/real-world.spec.js index 0dec3269..80d8e546 100644 --- a/test/specs/real-world/real-world.spec.js +++ b/test/specs/real-world/real-world.spec.js @@ -26,7 +26,7 @@ describe("Real-world APIs", () => { // 1) CI is really slow // 2) Some API definitions are HUGE and take a while to download // 3) If the download fails, we retry 2 times, which takes even more time - // 4) Really large API definitions take longer to pase, dereference, and validate + // 4) Really large API definitions take longer to parse, dereference, and validate this.currentTest.timeout(host.ci ? 300000 : 60000); // 5 minutes in CI, 1 minute locally this.currentTest.slow(5000); }); @@ -83,6 +83,7 @@ describe("Real-world APIs", () => { else { // This is not a known error console.error("\n\nERROR IN THIS API:", JSON.stringify(api, null, 2)); + console.error(JSON.stringify(error, null, 2)); throw error; } } diff --git a/test/specs/validate-spec/invalid/array-body-no-items.yaml b/test/specs/validate-spec/invalid-v2/array-body-no-items.yaml similarity index 100% rename from test/specs/validate-spec/invalid/array-body-no-items.yaml rename to test/specs/validate-spec/invalid-v2/array-body-no-items.yaml diff --git a/test/specs/validate-spec/invalid/array-no-items.yaml b/test/specs/validate-spec/invalid-v2/array-no-items.yaml similarity index 100% rename from test/specs/validate-spec/invalid/array-no-items.yaml rename to test/specs/validate-spec/invalid-v2/array-no-items.yaml diff --git a/test/specs/validate-spec/invalid/array-response-body-no-items.yaml b/test/specs/validate-spec/invalid-v2/array-request-body-no-items.yaml similarity index 100% rename from test/specs/validate-spec/invalid/array-response-body-no-items.yaml rename to test/specs/validate-spec/invalid-v2/array-request-body-no-items.yaml diff --git a/test/specs/validate-spec/invalid-v2/array-response-body-no-items.yaml b/test/specs/validate-spec/invalid-v2/array-response-body-no-items.yaml new file mode 100644 index 00000000..d153e6fd --- /dev/null +++ b/test/specs/validate-spec/invalid-v2/array-response-body-no-items.yaml @@ -0,0 +1,13 @@ +swagger: "2.0" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + get: + responses: + 200: + description: hello world + schema: + type: array diff --git a/test/specs/validate-spec/invalid/array-response-header-no-items.yaml b/test/specs/validate-spec/invalid-v2/array-response-header-no-items.yaml similarity index 100% rename from test/specs/validate-spec/invalid/array-response-header-no-items.yaml rename to test/specs/validate-spec/invalid-v2/array-response-header-no-items.yaml diff --git a/test/specs/validate-spec/invalid/body-and-form-params.yaml b/test/specs/validate-spec/invalid-v2/body-and-form-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/body-and-form-params.yaml rename to test/specs/validate-spec/invalid-v2/body-and-form-params.yaml diff --git a/test/specs/validate-spec/invalid/duplicate-operation-ids.yaml b/test/specs/validate-spec/invalid-v2/duplicate-operation-ids.yaml similarity index 100% rename from test/specs/validate-spec/invalid/duplicate-operation-ids.yaml rename to test/specs/validate-spec/invalid-v2/duplicate-operation-ids.yaml diff --git a/test/specs/validate-spec/invalid/duplicate-operation-params.yaml b/test/specs/validate-spec/invalid-v2/duplicate-operation-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/duplicate-operation-params.yaml rename to test/specs/validate-spec/invalid-v2/duplicate-operation-params.yaml diff --git a/test/specs/validate-spec/invalid/duplicate-path-params.yaml b/test/specs/validate-spec/invalid-v2/duplicate-path-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/duplicate-path-params.yaml rename to test/specs/validate-spec/invalid-v2/duplicate-path-params.yaml diff --git a/test/specs/validate-spec/invalid/duplicate-path-placeholders.yaml b/test/specs/validate-spec/invalid-v2/duplicate-path-placeholders.yaml similarity index 100% rename from test/specs/validate-spec/invalid/duplicate-path-placeholders.yaml rename to test/specs/validate-spec/invalid-v2/duplicate-path-placeholders.yaml diff --git a/test/specs/validate-spec/invalid/file-invalid-consumes.yaml b/test/specs/validate-spec/invalid-v2/file-invalid-consumes.yaml similarity index 100% rename from test/specs/validate-spec/invalid/file-invalid-consumes.yaml rename to test/specs/validate-spec/invalid-v2/file-invalid-consumes.yaml diff --git a/test/specs/validate-spec/invalid/file-no-consumes.yaml b/test/specs/validate-spec/invalid-v2/file-no-consumes.yaml similarity index 100% rename from test/specs/validate-spec/invalid/file-no-consumes.yaml rename to test/specs/validate-spec/invalid-v2/file-no-consumes.yaml diff --git a/test/specs/validate-spec/invalid/invalid-response-code.yaml b/test/specs/validate-spec/invalid-v2/invalid-response-code.yaml similarity index 100% rename from test/specs/validate-spec/invalid/invalid-response-code.yaml rename to test/specs/validate-spec/invalid-v2/invalid-response-code.yaml diff --git a/test/specs/validate-spec/invalid/multiple-body-params.yaml b/test/specs/validate-spec/invalid-v2/multiple-body-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/multiple-body-params.yaml rename to test/specs/validate-spec/invalid-v2/multiple-body-params.yaml diff --git a/test/specs/validate-spec/invalid/multiple-operation-body-params.yaml b/test/specs/validate-spec/invalid-v2/multiple-operation-body-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/multiple-operation-body-params.yaml rename to test/specs/validate-spec/invalid-v2/multiple-operation-body-params.yaml diff --git a/test/specs/validate-spec/invalid/multiple-path-body-params.yaml b/test/specs/validate-spec/invalid-v2/multiple-path-body-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/multiple-path-body-params.yaml rename to test/specs/validate-spec/invalid-v2/multiple-path-body-params.yaml diff --git a/test/specs/validate-spec/invalid/no-path-params.yaml b/test/specs/validate-spec/invalid-v2/no-path-params.yaml similarity index 100% rename from test/specs/validate-spec/invalid/no-path-params.yaml rename to test/specs/validate-spec/invalid-v2/no-path-params.yaml diff --git a/test/specs/validate-spec/invalid-v2/param-array-body-no-items.yaml b/test/specs/validate-spec/invalid-v2/param-array-body-no-items.yaml new file mode 100644 index 00000000..beb48108 --- /dev/null +++ b/test/specs/validate-spec/invalid-v2/param-array-body-no-items.yaml @@ -0,0 +1,16 @@ +swagger: "2.0" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + parameters: + - name: people + in: body + schema: + type: array + post: + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v2/param-array-no-items.yaml b/test/specs/validate-spec/invalid-v2/param-array-no-items.yaml new file mode 100644 index 00000000..5c976e2b --- /dev/null +++ b/test/specs/validate-spec/invalid-v2/param-array-no-items.yaml @@ -0,0 +1,15 @@ +swagger: "2.0" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + parameters: + - name: tags + in: query + type: array + get: + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid/path-param-no-placeholder.yaml b/test/specs/validate-spec/invalid-v2/path-param-no-placeholder.yaml similarity index 100% rename from test/specs/validate-spec/invalid/path-param-no-placeholder.yaml rename to test/specs/validate-spec/invalid-v2/path-param-no-placeholder.yaml diff --git a/test/specs/validate-spec/invalid/path-placeholder-no-param.yaml b/test/specs/validate-spec/invalid-v2/path-placeholder-no-param.yaml similarity index 100% rename from test/specs/validate-spec/invalid/path-placeholder-no-param.yaml rename to test/specs/validate-spec/invalid-v2/path-placeholder-no-param.yaml diff --git a/test/specs/validate-spec/invalid/required-property-not-defined-definitions.yaml b/test/specs/validate-spec/invalid-v2/required-property-not-defined-definitions.yaml similarity index 100% rename from test/specs/validate-spec/invalid/required-property-not-defined-definitions.yaml rename to test/specs/validate-spec/invalid-v2/required-property-not-defined-definitions.yaml diff --git a/test/specs/validate-spec/invalid/required-property-not-defined-input.yaml b/test/specs/validate-spec/invalid-v2/required-property-not-defined-input.yaml similarity index 100% rename from test/specs/validate-spec/invalid/required-property-not-defined-input.yaml rename to test/specs/validate-spec/invalid-v2/required-property-not-defined-input.yaml diff --git a/test/specs/validate-spec/invalid-v2/response-array-header-no-items.yaml b/test/specs/validate-spec/invalid-v2/response-array-header-no-items.yaml new file mode 100644 index 00000000..182e3498 --- /dev/null +++ b/test/specs/validate-spec/invalid-v2/response-array-header-no-items.yaml @@ -0,0 +1,16 @@ +swagger: "2.0" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + get: + responses: + "default": + description: hello world + headers: + Content-Type: + type: string + Last-Modified: + type: array diff --git a/test/specs/validate-spec/invalid-v3/duplicate-operation-ids.yaml b/test/specs/validate-spec/invalid-v3/duplicate-operation-ids.yaml new file mode 100644 index 00000000..64cda3c9 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/duplicate-operation-ids.yaml @@ -0,0 +1,17 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + get: + operationId: users + responses: + default: + description: hello world + post: + operationId: users # <---- duplicate + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml b/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml new file mode 100644 index 00000000..5fc48c96 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml @@ -0,0 +1,37 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}: + get: + parameters: + - name: username # <---- Duplicate param + in: path + required: true + schema: + type: string + - name: bar + in: header + schema: + type: string + required: false + - name: username # <---- Another username but in header is ok + in: header + schema: + type: string + required: false + - name: username # <---- Another username but in query is ok + in: query + schema: + type: string + required: false + - name: username # <---- Duplicate param + in: path + schema: + type: number + required: true + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/duplicate-path-params.yaml b/test/specs/validate-spec/invalid-v3/duplicate-path-params.yaml new file mode 100644 index 00000000..063633dc --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/duplicate-path-params.yaml @@ -0,0 +1,35 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}: + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: foo # <---- Duplicate param + in: header + schema: + type: string + required: false + - name: username # <---- Same param but in header is ok + in: header + schema: + type: string + - name: username # <---- Same param but in query is ok + in: query + schema: + type: string + - name: foo # <---- Duplicate param + in: header + schema: + type: number + required: true + get: + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/duplicate-path-placeholders.yaml b/test/specs/validate-spec/invalid-v3/duplicate-path-placeholders.yaml new file mode 100644 index 00000000..578e079d --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/duplicate-path-placeholders.yaml @@ -0,0 +1,22 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}/profile/{username}/image/{img_id}: # <---- duplicate {username} placeholders + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: img_id + in: path + required: true + schema: + type: number + get: + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-cookie-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-cookie-params.yaml new file mode 100644 index 00000000..eddc211a --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-cookie-params.yaml @@ -0,0 +1,22 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + patch: + parameters: + - name: username + in: cookie # <---- Cookie param #1 + required: true + schema: + type: string + - name: username + in: cookie # <---- Cookie param #2 + required: true + schema: + type: number + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-header-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-header-params.yaml new file mode 100644 index 00000000..33eff346 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-header-params.yaml @@ -0,0 +1,22 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + patch: + parameters: + - name: username + in: header # <---- Header param #1 + required: true + schema: + type: string + - name: username + in: header # <---- Header param #2 + required: true + schema: + type: number + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-path-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-path-params.yaml new file mode 100644 index 00000000..c5738409 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-path-params.yaml @@ -0,0 +1,22 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}: + patch: + parameters: + - name: username + in: path # <---- Path param #1 + required: true + schema: + type: string + - name: username + in: path # <---- Path param #2 + required: true + schema: + type: number + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-query-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-query-params.yaml new file mode 100644 index 00000000..69628acf --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-query-params.yaml @@ -0,0 +1,22 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + patch: + parameters: + - name: username + in: query # <---- Query param #1 + required: true + schema: + type: string + - name: username + in: query # <---- Query param #2 + required: true + schema: + type: number + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/no-path-params.yaml b/test/specs/validate-spec/invalid-v3/no-path-params.yaml new file mode 100644 index 00000000..34b29e20 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/no-path-params.yaml @@ -0,0 +1,37 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}/{foo}: # <---- {username} and {foo} placeholders + parameters: # <---- no path params + - $ref: '#/components/parameters/username' + - name: foo + in: query + schema: + type: string + get: + parameters: + - $ref: '#/components/parameters/username' + - name: foo # <---- no path params + in: header + schema: + type: number + responses: + default: + description: hello world + post: + parameters: # <---- no path params + - $ref: '#/components/parameters/username' + responses: + default: + description: hello world +components: + parameters: + username: + name: username + in: header + required: true + schema: + type: string diff --git a/test/specs/validate-spec/invalid-v3/param-array-no-items.yaml b/test/specs/validate-spec/invalid-v3/param-array-no-items.yaml new file mode 100644 index 00000000..1f16af30 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/param-array-no-items.yaml @@ -0,0 +1,16 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + parameters: + - name: tags + in: query + schema: + type: array + get: + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/path-param-no-placeholder.yaml b/test/specs/validate-spec/invalid-v3/path-param-no-placeholder.yaml new file mode 100644 index 00000000..56c0f84a --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/path-param-no-placeholder.yaml @@ -0,0 +1,42 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}: # <---- {username} placeholder + parameters: + - $ref: '#/components/parameters/username' + - name: foo # <---- foo in cookie is OK + in: cookie + schema: + type: string + get: + parameters: + - $ref: '#/components/parameters/username' + - name: foo # <---- foo in query is OK + in: query + schema: + type: number + responses: + default: + description: hello world + post: + parameters: + - $ref: '#/components/parameters/username' + - name: foo # <---- There is no {foo} placeholder + in: path + required: true + schema: + type: number + responses: + default: + description: hello world +components: + parameters: + username: + name: username + in: path + required: true + schema: + type: string diff --git a/test/specs/validate-spec/invalid-v3/path-placeholder-no-param.yaml b/test/specs/validate-spec/invalid-v3/path-placeholder-no-param.yaml new file mode 100644 index 00000000..0a8ae173 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/path-placeholder-no-param.yaml @@ -0,0 +1,37 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}/{foo}: # <---- {username} and {foo} placeholders + parameters: + - $ref: '#/components/parameters/username' # <---- "username" path param + - name: foo # <---- "foo" not in path + in: query + schema: + type: string + get: + parameters: # <---- there's no "foo" path param + - $ref: '#/components/parameters/username' # <---- "username" path param + - name: foo # <---- "foo" not in path + in: cookie + schema: + type: number + responses: + default: + description: hello world + post: + parameters: # <---- there's no "foo" path param + - $ref: '#/components/parameters/username' + responses: + default: + description: hello world +components: + parameters: + username: + name: username + in: path + required: true + schema: + type: string diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters-unused.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters-unused.yaml new file mode 100644 index 00000000..197b89a4 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters-unused.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + parameters: + - $ref: '#/components/parameters/Pet' + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + name: + type: string +components: + parameters: + Pet: + name: pet + in: query + description: Pet to add to the store + required: true + schema: + type: object + required: + - name + properties: + name: + type: string + color: + type: string + Unused: + name: unused + in: query + description: Unused to add to the store + required: true + schema: + type: object + required: + - name + - paramUnusedNotExists # <--- does not exist + properties: + name: + type: string + color: + type: string diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters.yaml new file mode 100644 index 00000000..07de8bc8 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + parameters: + - $ref: '#/components/parameters/Pet' + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + name: + type: string +components: + parameters: + Pet: + name: pet + in: query + description: Pet to add to the store + required: true + schema: + type: object + required: + - name + - paramNotExists # <--- does not exist + properties: + name: + type: string + color: + type: string + Unused: + name: unused + in: query + description: Unused to add to the store + required: true + schema: + type: object + required: + - name + - paramUnusedNotExists # <--- does not exist + properties: + name: + type: string + color: + type: string diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-requestBodies.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-requestBodies.yaml new file mode 100644 index 00000000..cb3d250e --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-requestBodies.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + requestBody: + $ref: '#/components/requestBodies/Pet' + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + name: + type: string + +components: + requestBodies: + Pet: + description: Pet for use in request bodies + content: + application/json: + schema: + type: object + required: + - reqBodyNotExists # <--- does not exist + properties: + name: + type: string + color: + type: string diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-responses.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-responses.yaml new file mode 100644 index 00000000..6d883a45 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-responses.yaml @@ -0,0 +1,36 @@ +openapi: "3.0.2" +info: + version: 1.0.0 + title: Swagger Petstore +paths: + '/pet/{petId}': + get: + summary: Find pet by ID + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + $ref: '#/components/responses/Pet' +components: + responses: + Pet: + description: successful operation + content: + application/json: + schema: + type: object + required: + - name + - photoUrls # <--- does not exist + properties: + name: + type: string + example: doggie + color: + type: string diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas-recursive.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas-recursive.yaml new file mode 100644 index 00000000..ebb26117 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas-recursive.yaml @@ -0,0 +1,47 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + name: + type: string +components: + schemas: + Pet: + type: object + required: + - name + properties: + name: + type: string + color: + type: string + kind: + type: object + required: + - stripes + - secondLevelNotExists + properties: + stripes: + type: boolean + spots: + type: boolean + diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas.yaml new file mode 100644 index 00000000..90e7fa48 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + name: + type: string +components: + schemas: + Pet: + type: object + required: + - notExists # <--- does not exist + properties: + name: + type: string + color: + type: string diff --git a/test/specs/validate-spec/invalid-v3/required-property-not-defined-param.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-param.yaml new file mode 100644 index 00000000..0b6b2c44 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-param.yaml @@ -0,0 +1,32 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + parameters: + - name: pet + in: query + description: Pet to add to the store + required: true + schema: + type: object + required: + - notExists # <--- does not exist + properties: + name: + type: string + color: + type: string + responses: + '200': + description: pet response + content: + application/json: + schema: + type: object + properties: + name: + type: string diff --git a/test/specs/validate-spec/invalid-v3/response-array-no-items.yaml b/test/specs/validate-spec/invalid-v3/response-array-no-items.yaml new file mode 100644 index 00000000..895eccae --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/response-array-no-items.yaml @@ -0,0 +1,15 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + get: + responses: + default: + description: hello world + content: + application/json: + schema: + type: array diff --git a/test/specs/validate-spec/invalid-v3/response-header-array-no-items.yaml b/test/specs/validate-spec/invalid-v3/response-header-array-no-items.yaml new file mode 100644 index 00000000..ca52a301 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/response-header-array-no-items.yaml @@ -0,0 +1,18 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + get: + responses: + "default": + description: hello world + headers: + Content-Type: + schema: + type: string + Last-Modified: + schema: + type: array diff --git a/test/specs/validate-spec/valid/file-vendor-specific-consumes-formdata.yaml b/test/specs/validate-spec/valid-v2/file-vendor-specific-consumes-formdata.yaml similarity index 100% rename from test/specs/validate-spec/valid/file-vendor-specific-consumes-formdata.yaml rename to test/specs/validate-spec/valid-v2/file-vendor-specific-consumes-formdata.yaml diff --git a/test/specs/validate-spec/valid/file-vendor-specific-consumes-urlencoded.yaml b/test/specs/validate-spec/valid-v2/file-vendor-specific-consumes-urlencoded.yaml similarity index 100% rename from test/specs/validate-spec/valid/file-vendor-specific-consumes-urlencoded.yaml rename to test/specs/validate-spec/valid-v2/file-vendor-specific-consumes-urlencoded.yaml diff --git a/test/specs/validate-spec/valid/inherited-required-properties.yaml b/test/specs/validate-spec/valid-v2/inherited-required-properties.yaml similarity index 100% rename from test/specs/validate-spec/valid/inherited-required-properties.yaml rename to test/specs/validate-spec/valid-v2/inherited-required-properties.yaml diff --git a/test/specs/validate-spec/valid-v3/inherited-required-properties.yaml b/test/specs/validate-spec/valid-v3/inherited-required-properties.yaml new file mode 100644 index 00000000..1208ac17 --- /dev/null +++ b/test/specs/validate-spec/valid-v3/inherited-required-properties.yaml @@ -0,0 +1,58 @@ +openapi: '3.0.2' +info: + contact: + x-twitter: hello_iqualify + description: >+ + The iQualify API for testing + title: iQualify + version: v1 +paths: + /offerings: + post: + description: Creates new offering. + requestBody: + description: create offering request + content: + application/json: + schema: + $ref: '#/components/schemas/OfferingRequired' + responses: + '201': + $ref: '#/components/responses/OfferingMetadataResponse' + summary: Create offering +components: + schemas: + Offering: + properties: + contentId: + minLength: 1 + type: string + end: + format: date-time + type: string + isReadonly: + type: boolean + name: + minLength: 1 + type: string + start: + format: date-time + type: string + OfferingRequired: + allOf: + - $ref: '#/components/schemas/Offering' + required: + - contentId # <-- all required properties are inherited + - start + - end + responses: + OfferingMetadataResponse: + description: Offering response + content: + text/plain: + schema: + type: object + properties: + contentId: + minLength: 1 + type: string diff --git a/test/specs/validate-spec/validate-spec.spec.js b/test/specs/validate-spec/validate-spec-v2.spec.js similarity index 85% rename from test/specs/validate-spec/validate-spec.spec.js rename to test/specs/validate-spec/validate-spec-v2.spec.js index 70bd5041..cbe189c2 100644 --- a/test/specs/validate-spec/validate-spec.spec.js +++ b/test/specs/validate-spec/validate-spec-v2.spec.js @@ -1,10 +1,10 @@ "use strict"; const { expect } = require("chai"); -const SwaggerParser = require("../../.."); +const SwaggerParser = require("../../../lib"); const path = require("../../utils/path"); -describe("Invalid APIs (Swagger 2.0 specification validation)", () => { +describe("Invalid APIs (Swagger v2.0 specification validation)", () => { let tests = [ { name: "invalid response code", @@ -16,13 +16,13 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { name: "duplicate path parameters", valid: false, file: "duplicate-path-params.yaml", - error: 'Validation failed. /paths/users/{username} has duplicate parameters \nValidation failed. Found multiple header parameters named \"foo\"' + error: 'Validation failed. /paths/users/{username} has duplicate parameters\nValidation failed. Found multiple header parameters named \"foo\"' }, { name: "duplicate operation parameters", valid: false, file: "duplicate-operation-params.yaml", - error: 'Validation failed. /paths/users/{username}/get has duplicate parameters \nValidation failed. Found multiple path parameters named \"username\"' + error: 'Validation failed. /paths/users/{username}/get has duplicate parameters\nValidation failed. Found multiple path parameters named \"username\"' }, { name: "multiple body parameters in path", @@ -75,13 +75,13 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { { name: "array param without items", valid: false, - file: "array-no-items.yaml", + file: "param-array-no-items.yaml", error: 'Validation failed. /paths/users/get/parameters/tags is an array, so it must include an \"items\" schema' }, { name: "array body param without items", valid: false, - file: "array-body-no-items.yaml", + file: "param-array-body-no-items.yaml", error: 'Validation failed. /paths/users/post/parameters/people is an array, so it must include an \"items\" schema' }, { @@ -90,6 +90,12 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { file: "array-response-header-no-items.yaml", error: 'Validation failed. /paths/users/get/responses/default/headers/Last-Modified is an array, so it must include an \"items\" schema' }, + { + name: "array response body without items", + valid: false, + file: "array-response-body-no-items.yaml", + error: 'Validation failed. /paths/users/get/responses/200/schema is an array, so it must include an \"items\" schema' + }, { name: '"file" param without "consumes"', valid: false, @@ -136,11 +142,11 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { error: "Validation failed. Duplicate operation id 'users'" }, { - name: "array response body without items", + name: "array request body without items", valid: false, - file: "array-response-body-no-items.yaml", - error: 'Validation failed. /paths/users/get/responses/200/schema is an array, so it must include an \"items\" schema' - } + file: "array-request-body-no-items.yaml", + error: "Validation failed. /paths/users/get/responses/200/schema is an array, so it must include an \"items\" schema" + }, ]; it('should pass validation if "options.validate.spec" is false', async () => { @@ -148,7 +154,7 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { expect(invalid.valid).to.equal(false); const api = await SwaggerParser - .validate(path.rel("specs/validate-spec/invalid/" + invalid.file), { validate: { spec: false }}); + .validate(path.rel("specs/validate-spec/invalid-v2/" + invalid.file), { validate: { spec: false }}); expect(api).to.be.an("object"); }); @@ -157,8 +163,9 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { it(test.name, async () => { try { const api = await SwaggerParser - .validate(path.rel("specs/validate-spec/valid/" + test.file)); + .validate(path.rel("specs/validate-spec/valid-v2/" + test.file)); expect(api).to.be.an("object"); + expect(api.swagger).to.equal("2.0"); } catch (err) { throw new Error("Validation should have succeeded, but it failed!\n" + err.stack); @@ -168,13 +175,13 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { else { it(test.name, async () => { try { - await SwaggerParser.validate(path.rel("specs/validate-spec/invalid/" + test.file)); + await SwaggerParser.validate(path.rel("specs/validate-spec/invalid-v2/" + test.file)); throw new Error("Validation should have failed, but it succeeded!"); } catch (err) { expect(err).to.be.an.instanceOf(SyntaxError); - expect(err.message).to.equal(test.error); - expect(err.message).to.match(/^Validation failed. \S+/); + expect(err.message).to.include(test.error); + expect(err.message).to.match(/^Specification check failed.\sValidation failed./); } }); } diff --git a/test/specs/validate-spec/validate-spec-v3.spec.js b/test/specs/validate-spec/validate-spec-v3.spec.js new file mode 100644 index 00000000..9c5a5fa4 --- /dev/null +++ b/test/specs/validate-spec/validate-spec-v3.spec.js @@ -0,0 +1,186 @@ +"use strict"; + +const { expect } = require("chai"); +const SwaggerParser = require("../../.."); +const path = require("../../utils/path"); + +describe("Invalid APIs (OpenAPI v3.0 specification validation)", () => { + let tests = [ + { + name: "duplicate path parameters", + valid: false, + file: "duplicate-path-params.yaml", + error: 'Validation failed. /paths/users/{username} has duplicate parameters\nValidation failed. Found multiple header parameters named \"foo\"' + }, + { + name: "duplicate operation parameters", + valid: false, + file: "duplicate-operation-params.yaml", + error: 'Validation failed. /paths/users/{username}/get has duplicate parameters\nValidation failed. Found multiple path parameters named \"username\"' + }, + { + name: "multiple parameters in path", + valid: false, + file: "multiple-path-params.yaml", + error: 'Validation failed. Found multiple path parameters named \"username\"' + }, + { + name: "multiple parameters in query", + valid: false, + file: "multiple-query-params.yaml", + error: 'Validation failed. Found multiple query parameters named \"username\"' + }, + { + name: "multiple parameters in header", + valid: false, + file: "multiple-header-params.yaml", + error: 'Validation failed. Found multiple header parameters named \"username\"' + }, + { + name: "multiple parameters in cookie", + valid: false, + file: "multiple-cookie-params.yaml", + error: 'Validation failed. Found multiple cookie parameters named \"username\"' + }, + { + name: "path param with no placeholder", + valid: false, + file: "path-param-no-placeholder.yaml", + error: 'Validation failed. /paths/users/{username}/post has a path parameter named \"foo\", but there is no corresponding {foo} in the path string' + }, + { + name: "path placeholder with no param", + valid: false, + file: "path-placeholder-no-param.yaml", + error: "Validation failed. /paths/users/{username}/{foo}/get is missing path parameter(s) for {foo}" + }, + { + name: "duplicate path placeholders", + valid: false, + file: "duplicate-path-placeholders.yaml", + error: "Validation failed. /paths/users/{username}/profile/{username}/image/{img_id}/get has multiple path placeholders named {username}" + }, + { + name: "no path parameters", + valid: false, + file: "no-path-params.yaml", + error: "Validation failed. /paths/users/{username}/{foo}/get is missing path parameter(s) for {username},{foo}" + }, + { + name: "array param without items", + valid: false, + file: "param-array-no-items.yaml", + error: 'Validation failed. /paths/users/get/parameters/tags is an array, so it must include an \"items\" schema' + }, + { + name: "response array without items", + valid: false, + file: "response-array-no-items.yaml", + error: 'Validation failed. /paths/users/get/responses/default/content/application/json/schema is an array, so it must include an "items" schema' + }, + { + name: "response header array without items", + valid: false, + file: "response-header-array-no-items.yaml", + error: 'Validation failed. /paths/users/get/responses/default/headers/Last-Modified is an array, so it must include an \"items\" schema' + }, + { + name: "required property in param does not exist", + valid: false, + file: "required-property-not-defined-param.yaml", + error: "Validation failed. Property 'notExists' listed as required but does not exist in '/paths/pets/post/parameters/pet'" + }, + { + name: "required property in components/parameters does not exist", + valid: false, + file: "required-property-not-defined-components-parameters.yaml", + error: "Validation failed. Property \'paramNotExists\' listed as required but does not exist" + }, + { + name: "required property in unused components/parameters does not exist", + valid: false, + file: "required-property-not-defined-components-parameters-unused.yaml", + error: "Validation failed. Property \'paramUnusedNotExists\' listed as required but does not exist" + }, + { + name: "required property in components/requestBodies does not exist", + valid: false, + file: "required-property-not-defined-components-requestBodies.yaml", + error: "Validation failed. Property \'reqBodyNotExists\' listed as required but does not exist" + }, + { + name: "required property in components/schemas does not exist", + valid: false, + file: "required-property-not-defined-components-schemas.yaml", + error: "Validation failed. Property \'notExists\' listed as required but does not exist" + }, + { + name: "required property in components/schemas second-level does not exist", + valid: false, + file: "required-property-not-defined-components-schemas-recursive.yaml", + error: "Validation failed. Property \'secondLevelNotExists\' listed as required but does not exist" + }, + { + name: "required property in components/responses does not exist", + valid: false, + file: "required-property-not-defined-components-responses.yaml", + error: "Validation failed. Property \'photoUrls\' listed as required but does not exist" + }, + { + name: "required property in components/parameters does not exist", + valid: false, + file: "required-property-not-defined-components-parameters.yaml", + error: "Validation failed. Property \'paramNotExists\' listed as required but does not exist" + }, + { + name: "schema declares required properties which are inherited (allOf)", + valid: true, + file: "inherited-required-properties.yaml" + }, + { + name: "duplicate operation IDs", + valid: false, + file: "duplicate-operation-ids.yaml", + error: "Validation failed. Duplicate operation id 'users'" + } + ]; + + it('should pass validation if "options.validate.spec" is false', async () => { + let invalid = tests[0]; + expect(invalid.valid).to.equal(false); + + const api = await SwaggerParser + .validate(path.rel("specs/validate-spec/invalid-v3/" + invalid.file), { validate: { spec: false }}); + expect(api).to.be.an("object"); + expect(api.openapi).to.match(/^3\.0/); + }); + + for (let test of tests) { + if (test.valid) { + it(test.name, async () => { + try { + const api = await SwaggerParser + .validate(path.rel("specs/validate-spec/valid-v3/" + test.file)); + expect(api).to.be.an("object"); + expect(api.openapi).to.match(/^3\.0/); + } + catch (err) { + throw new Error("Validation should have succeeded, but it failed!\n File:" + test.file + "\n" + err.stack); + } + }); + } + else { + it(test.name, async () => { + try { + await SwaggerParser.validate(path.rel("specs/validate-spec/invalid-v3/" + test.file)); + throw new Error("Validation should have failed, but it succeeded!\n File:" + test.file + "\n"); + } + catch (err) { + expect(err).to.be.an.instanceOf(SyntaxError); + expect(err.message).to.include(test.error); + expect(err.message).to.match(/^Specification check failed.\sValidation failed./); + } + }); + } + } +});