From 53ec2fcdee3658961afe79b4dd6e70620acc9a07 Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Tue, 25 Jan 2022 18:07:54 +1300 Subject: [PATCH 1/9] We now have a whole bunch of valid and invalid tests for OpenAPI V3.0.x --- test/specs/real-world/known-errors.js | 117 +++++++++++++ test/specs/real-world/real-world.spec.js | 4 +- .../invalid-v3/array-body-no-items.yaml | 13 ++ .../invalid-v3/array-no-items.yaml | 16 ++ .../array-response-body-no-items.yaml | 13 ++ .../array-response-header-no-items.yaml | 18 ++ .../invalid-v3/duplicate-operation-ids.yaml | 17 ++ .../duplicate-operation-params.yaml | 34 ++++ .../invalid-v3/duplicate-path-params.yaml | 35 ++++ .../duplicate-path-placeholders.yaml | 22 +++ .../invalid-v3/file-invalid-consumes.yaml | 27 +++ .../invalid-v3/file-no-consumes.yaml | 21 +++ .../invalid-v3/multiple-body-params.yaml | 55 ++++++ .../multiple-operation-body-params.yaml | 30 ++++ .../invalid-v3/multiple-path-body-params.yaml | 30 ++++ .../invalid-v3/no-path-params.yaml | 37 ++++ .../invalid-v3/path-param-no-placeholder.yaml | 42 +++++ .../invalid-v3/path-placeholder-no-param.yaml | 37 ++++ ...ired-property-not-defined-definitions.yaml | 34 ++++ .../required-property-not-defined-input.yaml | 30 ++++ .../inherited-required-properties.yaml | 58 +++++++ .../validate-spec/validate-spec-v3.spec.js | 162 ++++++++++++++++++ .../specs/validate-spec/validate-spec.spec.js | 9 +- 23 files changed, 856 insertions(+), 5 deletions(-) create mode 100644 test/specs/validate-spec/invalid-v3/array-body-no-items.yaml create mode 100644 test/specs/validate-spec/invalid-v3/array-no-items.yaml create mode 100644 test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml create mode 100644 test/specs/validate-spec/invalid-v3/array-response-header-no-items.yaml create mode 100644 test/specs/validate-spec/invalid-v3/duplicate-operation-ids.yaml create mode 100644 test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/duplicate-path-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/duplicate-path-placeholders.yaml create mode 100644 test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml create mode 100644 test/specs/validate-spec/invalid-v3/file-no-consumes.yaml create mode 100644 test/specs/validate-spec/invalid-v3/multiple-body-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/no-path-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/path-param-no-placeholder.yaml create mode 100644 test/specs/validate-spec/invalid-v3/path-placeholder-no-param.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-definitions.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-input.yaml create mode 100644 test/specs/validate-spec/valid-v3/inherited-required-properties.yaml create mode 100644 test/specs/validate-spec/validate-spec-v3.spec.js diff --git a/test/specs/real-world/known-errors.js b/test/specs/real-world/known-errors.js index 8ce5172e..2c386429 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..bcb6359e 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); }); @@ -52,6 +52,7 @@ describe("Real-world APIs", () => { * Downloads an API definition and validates it. Automatically retries if the download fails. */ async function validateApi (api, attemptNumber = 1) { +// if(api.name.includes('atlassian.com')) try { await SwaggerParser.validate(api.swaggerYamlUrl); } @@ -83,6 +84,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-v3/array-body-no-items.yaml b/test/specs/validate-spec/invalid-v3/array-body-no-items.yaml new file mode 100644 index 00000000..376ad024 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/array-body-no-items.yaml @@ -0,0 +1,13 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users: + get: + responses: + default: + description: hello world + schema: + type: array diff --git a/test/specs/validate-spec/invalid-v3/array-no-items.yaml b/test/specs/validate-spec/invalid-v3/array-no-items.yaml new file mode 100644 index 00000000..1f16af30 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/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/array-response-body-no-items.yaml b/test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml new file mode 100644 index 00000000..b7ee10a2 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml @@ -0,0 +1,13 @@ +openapi: "3.0.2" +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-v3/array-response-header-no-items.yaml b/test/specs/validate-spec/invalid-v3/array-response-header-no-items.yaml new file mode 100644 index 00000000..ca52a301 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/array-response-header-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/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..6e49c469 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml @@ -0,0 +1,34 @@ +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 + - name: username # <---- Another username but in query is ok + in: query + schema: + type: string + - name: username # <---- Duplicate param + in: path + 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/file-invalid-consumes.yaml b/test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml new file mode 100644 index 00000000..387c1758 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml @@ -0,0 +1,27 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +consumes: + - multipart/form-data # <--- The API allows "file" params + - application/x-www-form-urlencoded # <--- The API allows "file" params + +paths: + /users/{username}/profile/image: + parameters: + - name: username + in: path + type: string + required: true + - name: image + in: formData + schema: + type: file # <--- "file" params REQUIRE multipart/form-data or application/x-www-form-urlencoded + post: + consumes: # <--- This operation's "consumes" OVERRIDES the API's "consumes" + - application/octet-stream + - image/png + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/file-no-consumes.yaml b/test/specs/validate-spec/invalid-v3/file-no-consumes.yaml new file mode 100644 index 00000000..dddc5560 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/file-no-consumes.yaml @@ -0,0 +1,21 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}/profile/image: + parameters: + - name: username + in: path + schema: + type: string + required: true + - name: image + in: formData + schema: + type: file # <--- "file" type requires "consumes" to be specified + post: + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-body-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-body-params.yaml new file mode 100644 index 00000000..5700ca27 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-body-params.yaml @@ -0,0 +1,55 @@ +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: username + in: body # <---- Body param #1 + schema: + type: string + get: + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: bar + in: header + schema: + type: number + required: true + - name: username # <---- Not an error. This just overrides the path-level param + in: body + schema: + type: number + responses: + default: + description: hello world + post: + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: bar + in: header + schema: + type: number + required: true + - name: bar + in: body # <---- Body param #2 + schema: + type: number + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml new file mode 100644 index 00000000..5500d5a3 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml @@ -0,0 +1,30 @@ +openapi: "3.0.2" +info: + version: "1.0.0" + title: Invalid API + +paths: + /users/{username}: + patch: + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: username + in: body # <---- Body param #1 + schema: + type: string + - name: bar + in: header + schema: + type: number + required: true + - name: bar + in: body # <---- Body param #2 + schema: + type: number + responses: + default: + description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml new file mode 100644 index 00000000..93c0bb02 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml @@ -0,0 +1,30 @@ +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: username + in: body # <---- Body param #1 + schema: + type: string + - name: bar + in: header + schema: + type: number + required: true + - name: bar + in: body # <---- Body param #2 + schema: + type: number + get: + 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/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-definitions.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-definitions.yaml new file mode 100644 index 00000000..06d7f7de --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-definitions.yaml @@ -0,0 +1,34 @@ +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': + description: successful operation + schema: + $ref: '#/components/responses/Pet' +components: + responses: + Pet: + 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-input.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-input.yaml new file mode 100644 index 00000000..843e6bd2 --- /dev/null +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-input.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.2 +info: + version: 1.0.0 + title: Swagger Petstore +paths: + /pets: + post: + description: Creates a new pet in the store + parameters: + - name: pet + in: body + 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 + schema: + type: object + properties: + name: + type: string 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-v3.spec.js b/test/specs/validate-spec/validate-spec-v3.spec.js new file mode 100644 index 00000000..10707035 --- /dev/null +++ b/test/specs/validate-spec/validate-spec-v3.spec.js @@ -0,0 +1,162 @@ +"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 body parameters in path", + valid: false, + file: "multiple-path-body-params.yaml", + error: "Validation failed. /paths/users/{username}/get has 2 body parameters. Only one is allowed." + }, + { + name: "multiple body parameters in operation", + valid: false, + file: "multiple-operation-body-params.yaml", + error: "Validation failed. /paths/users/{username}/patch has 2 body parameters. Only one is allowed." + }, + { + name: "multiple body parameters in path & operation", + valid: false, + file: "multiple-body-params.yaml", + error: "Validation failed. /paths/users/{username}/post has 2 body parameters. Only one is allowed." + }, + { + 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: "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", + error: 'Validation failed. /paths/users/post/parameters/people is an array, so it must include an \"items\" schema' + }, + { + name: "array response header without items", + valid: false, + 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: '"file" param without "consumes"', + valid: false, + file: "file-no-consumes.yaml", + error: "Validation failed. /paths/users/{username}/profile/image/post has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded" + }, + { + name: '"file" param with invalid "consumes"', + valid: false, + file: "file-invalid-consumes.yaml", + error: "Validation failed. /paths/users/{username}/profile/image/post has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded" + }, + { + name: "required property in input does not exist", + valid: false, + file: "required-property-not-defined-input.yaml", + error: "Validation failed. Property 'notExists' listed as required but does not exist in '/paths/pets/post/parameters/pet'" + }, + { + name: "required property in definition does not exist", + valid: false, + file: "required-property-not-defined-definitions.yaml", + error: "Validation failed. Property 'photoUrls' listed as required but does not exist in '/definitions/Pet'" + }, + { + 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'" + }, + { + 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' + } + ]; + + 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/" + 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./); + } + }); + } + } +}); diff --git a/test/specs/validate-spec/validate-spec.spec.js b/test/specs/validate-spec/validate-spec.spec.js index 70bd5041..912ae47e 100644 --- a/test/specs/validate-spec/validate-spec.spec.js +++ b/test/specs/validate-spec/validate-spec.spec.js @@ -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", @@ -159,6 +159,7 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { const api = await SwaggerParser .validate(path.rel("specs/validate-spec/valid/" + 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); @@ -173,8 +174,8 @@ describe("Invalid APIs (Swagger 2.0 specification validation)", () => { } 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./); } }); } From 024d6b92efd3000b4ac1df02365c5bb41d4c4f0c Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Tue, 25 Jan 2022 18:08:42 +1300 Subject: [PATCH 2/9] Make use of the new valid and invalid tests for OpenAPI v3.0.x schemas --- .vscode/launch.json | 6 +- README.md | 5 +- lib/validators/spec.js | 337 ++++++++++++++++++++++++++++++++--------- package.json | 1 + 4 files changed, 276 insertions(+), 73 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index fb87b664..fcbe5482 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,8 @@ "stopOnEntry": false, "args": [ "--quick-test", - "--timeout=600000" + "--timeout=600000", + "Real" ], "cwd": "${workspaceRoot}", "preLaunchTask": null, @@ -18,7 +19,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/validators/spec.js b/lib/validators/spec.js index 21e965d3..035ee725 100644 --- a/lib/validators/spec.js +++ b/lib/validators/spec.js @@ -9,44 +9,89 @@ 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 */ function validateSpec (api) { - if (api.openapi) { - // We don't (yet) support validating against the OpenAPI spec - return; - } let paths = Object.keys(api.paths || {}); let operationIds = []; + + // accumulate errors + let message = "Specification check failed.\n"; + let isValid = true; + 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, path, pathId, operationIds); + } + } + catch (err) { + message += err.message; + isValid = false; + } + + // in OpenAPI v3.0 they are in re-usable schemas are in #/components/schemas + if (api.openapi) { + let schemas = Object.keys(api.components?.schemas || {}); + for (let schemaName of schemas) { + let schema = api.components.schemas[schemaName]; + let schemaId = "/components/schemas/" + schemaName; + + try { + validateRequiredPropertiesExist(schema, schemaId); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } + else { + // ... in Swagger v2.0 they were definitions + let definitions = Object.keys(api.definitions || {}); + for (let definitionName of definitions) { + let definition = api.definitions[definitionName]; + let definitionId = "/definitions/" + definitionName; + + try { + validateRequiredPropertiesExist(definition, definitionId); + } + 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} api - The entire Swagger/OpenAPI API object * @param {object} path - A Path object, from the Swagger 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) { + // accumulate errors + let message = ""; + let isValid = true; + for (let operationName of swaggerMethods) { let operation = path[operationName]; let operationId = pathId + "/" + operationName; @@ -58,25 +103,43 @@ 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, path, pathId, operation, operationId); + } + catch (err) { + message += err.message; + isValid = false; + } 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, 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} 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 @@ -86,12 +149,17 @@ function validateParameters (api, 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 +167,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 +183,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, 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,18 +221,27 @@ 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 +255,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,24 +274,30 @@ 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); + } } /** @@ -197,28 +309,46 @@ function validatePathParameters (params, pathId, operationId) { * @param {string} operationId - A value that uniquely identifies the operation */ function validateParameterTypes (params, api, 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); + validateRequiredPropertiesExist(schema, parameterId); + } 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 +360,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 +380,89 @@ 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 {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, 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; } 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; + + // schema is always inside 'schema' tag in openapi specs... + if (api.openapi) { + schema = header.schema; + validTypes = schemaTypes; + } + else { + // Swagger 2.0 was different... + schema = header; + validTypes = primitiveTypes; + } + try { + validateSchema(schema, headerId, validTypes); + } + catch (err) { + message += err.message; + isValid = false; + } } 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})`); + message += + `Validation failed. ${responseId} has an invalid response schema type (${response.schema.type})\n`; + isValid = false; } else { - validateSchema(response.schema, responseId + "/schema", validTypes); + try { + validateSchema(response.schema, responseId + "/schema", validTypes); + } 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); + } } /** @@ -295,14 +473,24 @@ function validateResponse (code, response, responseId) { * @param {string[]} validTypes - An array of the allowed schema types */ function validateSchema (schema, schemaId, validTypes) { + let message = ""; + let isValid = true; + 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; } 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; } + + // If we got some errors, then now is the time to throw the exception... + if (!isValid) { + throw ono.syntax(message); + } } /** @@ -312,6 +500,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 +519,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/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", From e73f58eb857158ba8e23170cc2b151fc4abf472e Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Wed, 26 Jan 2022 09:23:29 +1300 Subject: [PATCH 3/9] Fixed up ESLINT warnings in jsdoc annotations --- online/src/js/analytics.js | 2 +- online/src/js/dropdowns.js | 10 +++++----- online/src/js/editors.js | 10 +++++----- online/src/js/querystring.js | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) 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") { From 09a4d92e9c354bef7b71d8afa631b9ec79126c4c Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Wed, 26 Jan 2022 09:23:56 +1300 Subject: [PATCH 4/9] Fixed up ESLINT warnings and errors - mostly trailing spaces and indentations --- test/specs/real-world/known-errors.js | 4 ++-- test/specs/real-world/real-world.spec.js | 1 - test/specs/validate-spec/validate-spec-v3.spec.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/specs/real-world/known-errors.js b/test/specs/real-world/known-errors.js index 2c386429..9fa7ba61 100644 --- a/test/specs/real-world/known-errors.js +++ b/test/specs/real-world/known-errors.js @@ -108,7 +108,7 @@ function getKnownApiErrors () { 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' ?? { @@ -165,7 +165,7 @@ function getKnownApiErrors () { error: "Property 'email' listed as required but does not exist ", whatToDo: "ignore" }, - + // old field that used to exist included in 'required' ?? { api: "byautomata.io", diff --git a/test/specs/real-world/real-world.spec.js b/test/specs/real-world/real-world.spec.js index bcb6359e..80d8e546 100644 --- a/test/specs/real-world/real-world.spec.js +++ b/test/specs/real-world/real-world.spec.js @@ -52,7 +52,6 @@ describe("Real-world APIs", () => { * Downloads an API definition and validates it. Automatically retries if the download fails. */ async function validateApi (api, attemptNumber = 1) { -// if(api.name.includes('atlassian.com')) try { await SwaggerParser.validate(api.swaggerYamlUrl); } diff --git a/test/specs/validate-spec/validate-spec-v3.spec.js b/test/specs/validate-spec/validate-spec-v3.spec.js index 10707035..5c59112d 100644 --- a/test/specs/validate-spec/validate-spec-v3.spec.js +++ b/test/specs/validate-spec/validate-spec-v3.spec.js @@ -10,7 +10,7 @@ describe("Invalid APIs (OpenAPI v3.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", From e90a0b73eddc652392a606deb4bdee7aa2b08f63 Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Wed, 26 Jan 2022 09:24:46 +1300 Subject: [PATCH 5/9] Fixed up ESLINT warnings and errors - mostly type definitions --- lib/index.d.ts | 7 +++++- lib/index.js | 12 ++++++---- lib/validators/schema.js | 9 +++---- lib/validators/spec.js | 52 +++++++++++++++++++++------------------- 4 files changed, 45 insertions(+), 35 deletions(-) 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..0fbf0a1d 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) @@ -194,8 +194,10 @@ SwaggerParser.prototype.validate = async function (path, api, options, callback) }; /** - * The Swagger object + * The Swagger or OpenAPI object * 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: {}}} SwaggerObject + * @typedef {{swagger: string, info: {}, paths: {}, + * openapi:string, }} SwaggerOrOpenAPIObject */ diff --git a/lib/validators/schema.js b/lib/validators/schema.js index d22ea681..abca7d99 100644 --- a/lib/validators/schema.js +++ b/lib/validators/schema.js @@ -5,13 +5,14 @@ const { ono } = require("@jsdevtools/ono"); const AjvDraft4 = require("ajv-draft-04"); const Ajv = require("ajv/dist/2020"); const { openapi } = require("@apidevtools/openapi-schemas"); +const { SwaggerOrOpenAPIObject } = require("../index.js"); 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 +60,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 +82,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 035ee725..0f604cd5 100644 --- a/lib/validators/spec.js +++ b/lib/validators/spec.js @@ -125,7 +125,7 @@ function validatePath (api, path, pathId, operationIds) { catch (err) { message += err.message; isValid = false; - } + } } } } @@ -133,7 +133,7 @@ function validatePath (api, path, pathId, operationIds) { // If we got some errors, then now is the time to throw the exception... if (!isValid) { throw ono.syntax(message); - } + } } /** @@ -152,7 +152,7 @@ function validateParameters (api, path, pathId, operation, operationId) { // accumulate errors let message = ""; let isValid = true; - + // Check for duplicate path parameters try { checkForDuplicates(pathParams); @@ -208,7 +208,7 @@ function validateParameters (api, path, pathId, operation, operationId) { // If we got some errors, then now is the time to throw the exception... if (!isValid) { throw ono.syntax(message); - } + } } /** @@ -229,19 +229,19 @@ function validateBodyParameters (params, operationId) { if (bodyParams.length > 1) { message += `Validation failed. ${operationId} has ${bodyParams.length} body parameters. Only one is allowed.\n`; - isValid = false; + isValid = false; } else if (bodyParams.length > 0 && formParams.length > 0) { // "body" params and "formData" params are mutually exclusive message += `Validation failed. ${operationId} has body parameters and formData parameters. Only one or the other is allowed.\n`; - isValid = false; + isValid = false; } // If we got some errors, then now is the time to throw the exception... if (!isValid) { throw ono.syntax(message); - } + } } /** @@ -265,7 +265,7 @@ function validatePathParameters (params, pathId, operationId) { if (placeholders[i] === placeholders[j]) { message += `Validation failed. ${operationId} has multiple path placeholders named ${placeholders[i]}\n`; - isValid = false; + isValid = false; } } } @@ -281,7 +281,7 @@ function validatePathParameters (params, pathId, operationId) { } let match = placeholders.indexOf("{" + param.name + "}"); if (match === -1) { - message += + message += `Validation failed. ${operationId} has a path parameter named "${param.name}", ` + `but there is no corresponding {${param.name}} in the path string\n`; isValid = false; @@ -297,7 +297,7 @@ function validatePathParameters (params, pathId, operationId) { // If we got some errors, then now is the time to throw the exception... if (!isValid) { throw ono.syntax(message); - } + } } /** @@ -313,7 +313,7 @@ function validateParameterTypes (params, api, operation, operationId) { // accumulate errors let message = ""; let isValid = true; - + for (let param of params) { let parameterId = operationId + "/parameters/" + param.name; let schema, validTypes; @@ -343,7 +343,8 @@ function validateParameterTypes (params, api, operation, operationId) { try { validateSchema(schema, parameterId, validTypes); validateRequiredPropertiesExist(schema, parameterId); - } catch (err) { + } + catch (err) { message += err.message; isValid = false; } @@ -360,18 +361,18 @@ function validateParameterTypes (params, api, operation, operationId) { }); if (!hasValidMimeType) { - message += + message += `Validation failed. ${operationId} has a file parameter, so it must consume multipart/form-data ` + "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); - } + } } /** @@ -397,7 +398,7 @@ function checkForDuplicates (params) { // If we got some errors, then now is the time to throw the exception... if (!isValid) { throw ono.syntax(message); - } + } } /** @@ -411,7 +412,7 @@ function checkForDuplicates (params) { function validateResponse (api, code, response, responseId) { let message = ""; let isValid = true; - + if (code !== "default" && (code < 100 || code > 599)) { message += `Validation failed. ${responseId} has an invalid response code (${code})\n`; isValid = false; @@ -447,12 +448,13 @@ function validateResponse (api, code, response, responseId) { if (validTypes.indexOf(response.schema.type) === -1) { message += `Validation failed. ${responseId} has an invalid response schema type (${response.schema.type})\n`; - isValid = false; + isValid = false; } else { try { validateSchema(response.schema, responseId + "/schema", validTypes); - } catch (err) { + } + catch (err) { message += err.message; isValid = false; } @@ -479,7 +481,7 @@ function validateSchema (schema, schemaId, validTypes) { if (validTypes.indexOf(schema.type) === -1) { message += `Validation failed. ${schemaId} has an invalid type (${schema.type})\n`; - isValid = false; + isValid = false; } if (schema.type === "array" && !schema.items) { @@ -490,7 +492,7 @@ function validateSchema (schema, schemaId, validTypes) { // If we got some errors, then now is the time to throw the exception... if (!isValid) { throw ono.syntax(message); - } + } } /** @@ -500,7 +502,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. */ @@ -527,15 +529,15 @@ function validateRequiredPropertiesExist (schema, schemaId) { collectProperties(schema, props); for (let requiredProperty of schema.required) { if (!props[requiredProperty]) { - message += + 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); - } + } } From 6bed451e77ece0bc0b9b0a3f60ab7429b09a3685 Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Wed, 26 Jan 2022 18:38:18 +1300 Subject: [PATCH 6/9] Renamed test directories 'valid' and 'invalid' to 'valid-v2' and 'invalid-v2' to make it clear which version of spec the tests are for --- .../array-body-no-items.yaml | 0 .../{invalid => invalid-v2}/array-no-items.yaml | 0 .../array-request-body-no-items.yaml} | 0 .../invalid-v2/array-response-body-no-items.yaml | 13 +++++++++++++ .../array-response-header-no-items.yaml | 0 .../body-and-form-params.yaml | 0 .../duplicate-operation-ids.yaml | 0 .../duplicate-operation-params.yaml | 0 .../duplicate-path-params.yaml | 0 .../duplicate-path-placeholders.yaml | 0 .../file-invalid-consumes.yaml | 0 .../file-no-consumes.yaml | 0 .../invalid-response-code.yaml | 0 .../multiple-body-params.yaml | 0 .../multiple-operation-body-params.yaml | 0 .../multiple-path-body-params.yaml | 0 .../{invalid => invalid-v2}/no-path-params.yaml | 0 .../invalid-v2/param-array-body-no-items.yaml | 16 ++++++++++++++++ .../invalid-v2/param-array-no-items.yaml | 15 +++++++++++++++ .../path-param-no-placeholder.yaml | 0 .../path-placeholder-no-param.yaml | 0 ...equired-property-not-defined-definitions.yaml | 0 .../required-property-not-defined-input.yaml | 0 .../response-array-header-no-items.yaml | 16 ++++++++++++++++ .../file-vendor-specific-consumes-formdata.yaml | 0 ...file-vendor-specific-consumes-urlencoded.yaml | 0 .../inherited-required-properties.yaml | 0 27 files changed, 60 insertions(+) rename test/specs/validate-spec/{invalid => invalid-v2}/array-body-no-items.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/array-no-items.yaml (100%) rename test/specs/validate-spec/{invalid/array-response-body-no-items.yaml => invalid-v2/array-request-body-no-items.yaml} (100%) create mode 100644 test/specs/validate-spec/invalid-v2/array-response-body-no-items.yaml rename test/specs/validate-spec/{invalid => invalid-v2}/array-response-header-no-items.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/body-and-form-params.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/duplicate-operation-ids.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/duplicate-operation-params.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/duplicate-path-params.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/duplicate-path-placeholders.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/file-invalid-consumes.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/file-no-consumes.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/invalid-response-code.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/multiple-body-params.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/multiple-operation-body-params.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/multiple-path-body-params.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/no-path-params.yaml (100%) create mode 100644 test/specs/validate-spec/invalid-v2/param-array-body-no-items.yaml create mode 100644 test/specs/validate-spec/invalid-v2/param-array-no-items.yaml rename test/specs/validate-spec/{invalid => invalid-v2}/path-param-no-placeholder.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/path-placeholder-no-param.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/required-property-not-defined-definitions.yaml (100%) rename test/specs/validate-spec/{invalid => invalid-v2}/required-property-not-defined-input.yaml (100%) create mode 100644 test/specs/validate-spec/invalid-v2/response-array-header-no-items.yaml rename test/specs/validate-spec/{valid => valid-v2}/file-vendor-specific-consumes-formdata.yaml (100%) rename test/specs/validate-spec/{valid => valid-v2}/file-vendor-specific-consumes-urlencoded.yaml (100%) rename test/specs/validate-spec/{valid => valid-v2}/inherited-required-properties.yaml (100%) 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/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 From 6ecaa2113450491b42a3d1a235cd61bee4ccc9e4 Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Wed, 26 Jan 2022 18:40:51 +1300 Subject: [PATCH 7/9] Made sure correct tests are run for Swagger (v2.0) and OpenAPI (v3.0.x) specs; Made validateSchema recursively check the entire object, not just the top level; check all items that could be in #/components/{schemas,parameters, requestBodies, responses} --- .vscode/launch.json | 3 +- lib/validators/spec.js | 160 +++++++++++++++--- .../array-response-body-no-items.yaml | 13 -- .../duplicate-operation-params.yaml | 5 +- .../invalid-v3/file-invalid-consumes.yaml | 27 --- .../invalid-v3/file-no-consumes.yaml | 21 --- .../invalid-v3/multiple-body-params.yaml | 55 ------ .../invalid-v3/multiple-cookie-params.yaml | 22 +++ .../invalid-v3/multiple-header-params.yaml | 22 +++ .../invalid-v3/multiple-path-body-params.yaml | 30 ---- ...-params.yaml => multiple-path-params.yaml} | 12 +- .../invalid-v3/multiple-query-params.yaml | 22 +++ ...o-items.yaml => param-array-no-items.yaml} | 0 ...-defined-components-parameters-unused.yaml | 51 ++++++ ...rty-not-defined-components-parameters.yaml | 52 ++++++ ...-not-defined-components-requestBodies.yaml | 36 ++++ ...erty-not-defined-components-responses.yaml | 36 ++++ ...-defined-components-schemas-recursive.yaml | 47 +++++ ...operty-not-defined-components-schemas.yaml | 36 ++++ ...ired-property-not-defined-definitions.yaml | 34 ---- ... required-property-not-defined-param.yaml} | 16 +- ...tems.yaml => response-array-no-items.yaml} | 6 +- ...ml => response-header-array-no-items.yaml} | 0 ...-spec.spec.js => validate-spec-v2.spec.js} | 28 +-- .../validate-spec/validate-spec-v3.spec.js | 92 ++++++---- 25 files changed, 559 insertions(+), 267 deletions(-) delete mode 100644 test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml delete mode 100644 test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml delete mode 100644 test/specs/validate-spec/invalid-v3/file-no-consumes.yaml delete mode 100644 test/specs/validate-spec/invalid-v3/multiple-body-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/multiple-cookie-params.yaml create mode 100644 test/specs/validate-spec/invalid-v3/multiple-header-params.yaml delete mode 100644 test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml rename test/specs/validate-spec/invalid-v3/{multiple-operation-body-params.yaml => multiple-path-params.yaml} (58%) create mode 100644 test/specs/validate-spec/invalid-v3/multiple-query-params.yaml rename test/specs/validate-spec/invalid-v3/{array-no-items.yaml => param-array-no-items.yaml} (100%) create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters-unused.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-components-parameters.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-components-requestBodies.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-components-responses.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas-recursive.yaml create mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-components-schemas.yaml delete mode 100644 test/specs/validate-spec/invalid-v3/required-property-not-defined-definitions.yaml rename test/specs/validate-spec/invalid-v3/{required-property-not-defined-input.yaml => required-property-not-defined-param.yaml} (71%) rename test/specs/validate-spec/invalid-v3/{array-body-no-items.yaml => response-array-no-items.yaml} (61%) rename test/specs/validate-spec/invalid-v3/{array-response-header-no-items.yaml => response-header-array-no-items.yaml} (100%) rename test/specs/validate-spec/{validate-spec.spec.js => validate-spec-v2.spec.js} (90%) diff --git a/.vscode/launch.json b/.vscode/launch.json index fcbe5482..859ce6df 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,8 +9,7 @@ "stopOnEntry": false, "args": [ "--quick-test", - "--timeout=600000", - "Real" + "--timeout=600000" ], "cwd": "${workspaceRoot}", "preLaunchTask": null, diff --git a/lib/validators/spec.js b/lib/validators/spec.js index 0f604cd5..03e6edd2 100644 --- a/lib/validators/spec.js +++ b/lib/validators/spec.js @@ -23,6 +23,7 @@ function validateSpec (api) { 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; @@ -39,31 +40,92 @@ function validateSpec (api) { isValid = false; } - // in OpenAPI v3.0 they are in re-usable schemas are in #/components/schemas + // in OpenAPI v3.0 #/components holds lots of different definitions... if (api.openapi) { - let schemas = Object.keys(api.components?.schemas || {}); - for (let schemaName of schemas) { + // #/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 { - validateRequiredPropertiesExist(schema, schemaId); + validateSchema(schema, schemaId, schemaTypes); } 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); + } + 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); + } + 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); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } } else { - // ... in Swagger v2.0 they were definitions + // ... 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 { - validateRequiredPropertiesExist(definition, definitionId); + validateSchema(definition, definitionId, schemaTypes); } catch (err) { message += err.message; @@ -83,7 +145,7 @@ function validateSpec (api) { * Validates the given path. * * @param {object} api - The entire Swagger/OpenAPI API object - * @param {object} path - A Path object, from the Swagger API + * @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 */ @@ -115,6 +177,9 @@ function validatePath (api, path, pathId, operationIds) { 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]; @@ -342,7 +407,6 @@ function validateParameterTypes (params, api, operation, operationId) { try { validateSchema(schema, parameterId, validTypes); - validateRequiredPropertiesExist(schema, parameterId); } catch (err) { message += err.message; @@ -418,6 +482,7 @@ function validateResponse (api, code, response, responseId) { isValid = false; } + // Check response Headers let headers = Object.keys(response.headers || {}); for (let headerName of headers) { let header = response.headers[headerName]; @@ -443,21 +508,52 @@ function validateResponse (api, code, response, responseId) { } } - 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); + // 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); + } + catch (err) { + message += err.message; + isValid = false; + } + } + } } - catch (err) { - message += err.message; + } + } + 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); + } + catch (err) { + message += err.message; + isValid = false; + } + } } } @@ -478,17 +574,41 @@ function validateSchema (schema, schemaId, validTypes) { let message = ""; let isValid = true; + // make sure the schema type is known if (validTypes.indexOf(schema.type) === -1) { message += `Validation failed. ${schemaId} has an invalid type (${schema.type})\n`; isValid = false; } + // make sure that array's have items defined if (schema.type === "array" && !schema.items) { 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... + let propNames = Object.keys(schema.properties || {}); + for (let propName of propNames) { + + try { + validateSchema(schema.properties[propName], schemaId + "/" + propName, schemaTypes); + } + 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); diff --git a/test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml b/test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml deleted file mode 100644 index b7ee10a2..00000000 --- a/test/specs/validate-spec/invalid-v3/array-response-body-no-items.yaml +++ /dev/null @@ -1,13 +0,0 @@ -openapi: "3.0.2" -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-v3/duplicate-operation-params.yaml b/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml index 6e49c469..5fc48c96 100644 --- a/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml +++ b/test/specs/validate-spec/invalid-v3/duplicate-operation-params.yaml @@ -21,13 +21,16 @@ paths: 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 - type: number + schema: + type: number required: true responses: default: diff --git a/test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml b/test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml deleted file mode 100644 index 387c1758..00000000 --- a/test/specs/validate-spec/invalid-v3/file-invalid-consumes.yaml +++ /dev/null @@ -1,27 +0,0 @@ -openapi: "3.0.2" -info: - version: "1.0.0" - title: Invalid API - -consumes: - - multipart/form-data # <--- The API allows "file" params - - application/x-www-form-urlencoded # <--- The API allows "file" params - -paths: - /users/{username}/profile/image: - parameters: - - name: username - in: path - type: string - required: true - - name: image - in: formData - schema: - type: file # <--- "file" params REQUIRE multipart/form-data or application/x-www-form-urlencoded - post: - consumes: # <--- This operation's "consumes" OVERRIDES the API's "consumes" - - application/octet-stream - - image/png - responses: - default: - description: hello world diff --git a/test/specs/validate-spec/invalid-v3/file-no-consumes.yaml b/test/specs/validate-spec/invalid-v3/file-no-consumes.yaml deleted file mode 100644 index dddc5560..00000000 --- a/test/specs/validate-spec/invalid-v3/file-no-consumes.yaml +++ /dev/null @@ -1,21 +0,0 @@ -openapi: "3.0.2" -info: - version: "1.0.0" - title: Invalid API - -paths: - /users/{username}/profile/image: - parameters: - - name: username - in: path - schema: - type: string - required: true - - name: image - in: formData - schema: - type: file # <--- "file" type requires "consumes" to be specified - post: - responses: - default: - description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-body-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-body-params.yaml deleted file mode 100644 index 5700ca27..00000000 --- a/test/specs/validate-spec/invalid-v3/multiple-body-params.yaml +++ /dev/null @@ -1,55 +0,0 @@ -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: username - in: body # <---- Body param #1 - schema: - type: string - get: - parameters: - - name: username - in: path - required: true - schema: - type: string - - name: bar - in: header - schema: - type: number - required: true - - name: username # <---- Not an error. This just overrides the path-level param - in: body - schema: - type: number - responses: - default: - description: hello world - post: - parameters: - - name: username - in: path - required: true - schema: - type: string - - name: bar - in: header - schema: - type: number - required: true - - name: bar - in: body # <---- Body param #2 - schema: - type: number - 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-body-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml deleted file mode 100644 index 93c0bb02..00000000 --- a/test/specs/validate-spec/invalid-v3/multiple-path-body-params.yaml +++ /dev/null @@ -1,30 +0,0 @@ -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: username - in: body # <---- Body param #1 - schema: - type: string - - name: bar - in: header - schema: - type: number - required: true - - name: bar - in: body # <---- Body param #2 - schema: - type: number - get: - responses: - default: - description: hello world diff --git a/test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml b/test/specs/validate-spec/invalid-v3/multiple-path-params.yaml similarity index 58% rename from test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml rename to test/specs/validate-spec/invalid-v3/multiple-path-params.yaml index 5500d5a3..c5738409 100644 --- a/test/specs/validate-spec/invalid-v3/multiple-operation-body-params.yaml +++ b/test/specs/validate-spec/invalid-v3/multiple-path-params.yaml @@ -8,21 +8,13 @@ paths: patch: parameters: - name: username - in: path + in: path # <---- Path param #1 required: true schema: type: string - name: username - in: body # <---- Body param #1 - schema: - type: string - - name: bar - in: header - schema: - type: number + in: path # <---- Path param #2 required: true - - name: bar - in: body # <---- Body param #2 schema: type: number responses: 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/array-no-items.yaml b/test/specs/validate-spec/invalid-v3/param-array-no-items.yaml similarity index 100% rename from test/specs/validate-spec/invalid-v3/array-no-items.yaml rename to test/specs/validate-spec/invalid-v3/param-array-no-items.yaml 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-definitions.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-definitions.yaml deleted file mode 100644 index 06d7f7de..00000000 --- a/test/specs/validate-spec/invalid-v3/required-property-not-defined-definitions.yaml +++ /dev/null @@ -1,34 +0,0 @@ -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': - description: successful operation - schema: - $ref: '#/components/responses/Pet' -components: - responses: - Pet: - 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-input.yaml b/test/specs/validate-spec/invalid-v3/required-property-not-defined-param.yaml similarity index 71% rename from test/specs/validate-spec/invalid-v3/required-property-not-defined-input.yaml rename to test/specs/validate-spec/invalid-v3/required-property-not-defined-param.yaml index 843e6bd2..0b6b2c44 100644 --- a/test/specs/validate-spec/invalid-v3/required-property-not-defined-input.yaml +++ b/test/specs/validate-spec/invalid-v3/required-property-not-defined-param.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.2 +openapi: 3.0.3 info: version: 1.0.0 title: Swagger Petstore @@ -8,7 +8,7 @@ paths: description: Creates a new pet in the store parameters: - name: pet - in: body + in: query description: Pet to add to the store required: true schema: @@ -23,8 +23,10 @@ paths: responses: '200': description: pet response - schema: - type: object - properties: - name: - type: string + content: + application/json: + schema: + type: object + properties: + name: + type: string diff --git a/test/specs/validate-spec/invalid-v3/array-body-no-items.yaml b/test/specs/validate-spec/invalid-v3/response-array-no-items.yaml similarity index 61% rename from test/specs/validate-spec/invalid-v3/array-body-no-items.yaml rename to test/specs/validate-spec/invalid-v3/response-array-no-items.yaml index 376ad024..895eccae 100644 --- a/test/specs/validate-spec/invalid-v3/array-body-no-items.yaml +++ b/test/specs/validate-spec/invalid-v3/response-array-no-items.yaml @@ -9,5 +9,7 @@ paths: responses: default: description: hello world - schema: - type: array + content: + application/json: + schema: + type: array diff --git a/test/specs/validate-spec/invalid-v3/array-response-header-no-items.yaml b/test/specs/validate-spec/invalid-v3/response-header-array-no-items.yaml similarity index 100% rename from test/specs/validate-spec/invalid-v3/array-response-header-no-items.yaml rename to test/specs/validate-spec/invalid-v3/response-header-array-no-items.yaml diff --git a/test/specs/validate-spec/validate-spec.spec.js b/test/specs/validate-spec/validate-spec-v2.spec.js similarity index 90% rename from test/specs/validate-spec/validate-spec.spec.js rename to test/specs/validate-spec/validate-spec-v2.spec.js index 912ae47e..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", @@ -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,7 +163,7 @@ 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"); } @@ -169,7 +175,7 @@ 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) { diff --git a/test/specs/validate-spec/validate-spec-v3.spec.js b/test/specs/validate-spec/validate-spec-v3.spec.js index 5c59112d..9c5a5fa4 100644 --- a/test/specs/validate-spec/validate-spec-v3.spec.js +++ b/test/specs/validate-spec/validate-spec-v3.spec.js @@ -19,22 +19,28 @@ describe("Invalid APIs (OpenAPI v3.0 specification validation)", () => { error: 'Validation failed. /paths/users/{username}/get has duplicate parameters\nValidation failed. Found multiple path parameters named \"username\"' }, { - name: "multiple body parameters in path", + name: "multiple parameters in path", valid: false, - file: "multiple-path-body-params.yaml", - error: "Validation failed. /paths/users/{username}/get has 2 body parameters. Only one is allowed." + file: "multiple-path-params.yaml", + error: 'Validation failed. Found multiple path parameters named \"username\"' }, { - name: "multiple body parameters in operation", + name: "multiple parameters in query", valid: false, - file: "multiple-operation-body-params.yaml", - error: "Validation failed. /paths/users/{username}/patch has 2 body parameters. Only one is allowed." + file: "multiple-query-params.yaml", + error: 'Validation failed. Found multiple query parameters named \"username\"' }, { - name: "multiple body parameters in path & operation", + name: "multiple parameters in header", valid: false, - file: "multiple-body-params.yaml", - error: "Validation failed. /paths/users/{username}/post has 2 body parameters. Only one is allowed." + 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", @@ -63,44 +69,68 @@ describe("Invalid APIs (OpenAPI v3.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", + name: "response array without items", valid: false, - file: "array-body-no-items.yaml", - error: 'Validation failed. /paths/users/post/parameters/people is an array, so it must include an \"items\" schema' + 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: "array response header without items", + name: "response header array without items", valid: false, - file: "array-response-header-no-items.yaml", + 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: '"file" param without "consumes"', + name: "required property in param does not exist", valid: false, - file: "file-no-consumes.yaml", - error: "Validation failed. /paths/users/{username}/profile/image/post has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded" + 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: '"file" param with invalid "consumes"', + name: "required property in components/parameters does not exist", valid: false, - file: "file-invalid-consumes.yaml", - error: "Validation failed. /paths/users/{username}/profile/image/post has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded" + file: "required-property-not-defined-components-parameters.yaml", + error: "Validation failed. Property \'paramNotExists\' listed as required but does not exist" }, { - name: "required property in input does not exist", + name: "required property in unused components/parameters does not exist", valid: false, - file: "required-property-not-defined-input.yaml", - error: "Validation failed. Property 'notExists' listed as required but does not exist in '/paths/pets/post/parameters/pet'" + 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 definition does not exist", + name: "required property in components/parameters does not exist", valid: false, - file: "required-property-not-defined-definitions.yaml", - error: "Validation failed. Property 'photoUrls' listed as required but does not exist in '/definitions/Pet'" + 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)", @@ -112,12 +142,6 @@ describe("Invalid APIs (OpenAPI v3.0 specification validation)", () => { valid: false, file: "duplicate-operation-ids.yaml", error: "Validation failed. Duplicate operation id 'users'" - }, - { - 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' } ]; @@ -148,7 +172,7 @@ describe("Invalid APIs (OpenAPI v3.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-v3/" + test.file)); throw new Error("Validation should have failed, but it succeeded!\n File:" + test.file + "\n"); } catch (err) { From 3cb3fa197e2601e7b04119c7d942eb8ad30500b4 Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Wed, 26 Jan 2022 22:49:01 +1300 Subject: [PATCH 8/9] Pickup the .circular meta-data result, so we can know if the API has circular while checking things; validateSchema() is now recursive for non-circular APIs --- lib/index.js | 7 +++-- lib/validators/spec.js | 69 ++++++++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/lib/index.js b/lib/index.js index 0fbf0a1d..9938f5dd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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)); @@ -201,3 +201,6 @@ SwaggerParser.prototype.validate = async function (path, api, options, callback) * @typedef {{swagger: string, info: {}, paths: {}, * openapi:string, }} SwaggerOrOpenAPIObject */ + +// Only the one export; don't try to export the @typedef of SwaggerOrOpenAPIObject +module.exports = SwaggerParser; diff --git a/lib/validators/spec.js b/lib/validators/spec.js index 03e6edd2..3e330349 100644 --- a/lib/validators/spec.js +++ b/lib/validators/spec.js @@ -12,9 +12,10 @@ module.exports = validateSpec; * 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 {object} api - the entire Swagger API object + * @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) { +function validateSpec (api, isCircular) { let paths = Object.keys(api.paths || {}); let operationIds = []; @@ -32,7 +33,7 @@ function validateSpec (api) { // ...and off we go ... if (path && pathName.indexOf("/") === 0) { - validatePath(api, path, pathId, operationIds); + validatePath(api, isCircular, path, pathId, operationIds); } } catch (err) { @@ -50,7 +51,7 @@ function validateSpec (api) { let schemaId = "/components/schemas/" + schemaName; try { - validateSchema(schema, schemaId, schemaTypes); + validateSchema(schema, schemaId, schemaTypes, isCircular); } catch (err) { message += err.message; @@ -67,7 +68,7 @@ function validateSpec (api) { let schemaId = "/components/parameters/" + paramName + "/schema"; try { - validateSchema(schema, schemaId, schemaTypes); + validateSchema(schema, schemaId, schemaTypes, isCircular); } catch (err) { message += err.message; @@ -87,7 +88,7 @@ function validateSpec (api) { let schemaId = "/components/requestBodies/" + reqBodyName + "/content/" + contentType + "/schema"; try { - validateSchema(schema, schemaId, schemaTypes); + validateSchema(schema, schemaId, schemaTypes, isCircular); } catch (err) { message += err.message; @@ -108,7 +109,7 @@ function validateSpec (api) { let schemaId = "/components/responses/" + rspName + "/content/" + contentType + "/schema"; try { - validateSchema(schema, schemaId, schemaTypes); + validateSchema(schema, schemaId, schemaTypes, isCircular); } catch (err) { message += err.message; @@ -125,7 +126,7 @@ function validateSpec (api) { let definitionId = "/definitions/" + definitionName; try { - validateSchema(definition, definitionId, schemaTypes); + validateSchema(definition, definitionId, schemaTypes, isCircular); } catch (err) { message += err.message; @@ -145,11 +146,12 @@ function validateSpec (api) { * Validates the given path. * * @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; @@ -170,7 +172,7 @@ function validatePath (api, path, pathId, operationIds) { } } try { - validateParameters(api, path, pathId, operation, operationId); + validateParameters(api, isCircular, path, pathId, operation, operationId); } catch (err) { message += err.message; @@ -185,7 +187,7 @@ function validatePath (api, path, pathId, operationIds) { let response = operation.responses[responseName]; let responseId = operationId + "/responses/" + responseName; try { - validateResponse(api, responseName, (response || {}), responseId); + validateResponse(api, isCircular, responseName, (response || {}), responseId); } catch (err) { message += err.message; @@ -205,12 +207,13 @@ function validatePath (api, path, pathId, operationIds) { * Validates the parameters for the given operation. * * @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 || []; @@ -263,7 +266,7 @@ function validateParameters (api, path, pathId, operation, operationId) { isValid = false; } try { - validateParameterTypes(params, api, operation, operationId); + validateParameterTypes(params, api, isCircular, operation, operationId); } catch (err) { message += err.message; @@ -370,10 +373,11 @@ 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 = ""; @@ -406,7 +410,7 @@ function validateParameterTypes (params, api, operation, operationId) { } try { - validateSchema(schema, parameterId, validTypes); + validateSchema(schema, parameterId, validTypes, isCircular); } catch (err) { message += err.message; @@ -469,11 +473,12 @@ function checkForDuplicates (params) { * 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 (api, code, response, responseId) { +function validateResponse (api, isCircular, code, response, responseId) { let message = ""; let isValid = true; @@ -500,7 +505,7 @@ function validateResponse (api, code, response, responseId) { validTypes = primitiveTypes; } try { - validateSchema(schema, headerId, validTypes); + validateSchema(schema, headerId, validTypes, isCircular); } catch (err) { message += err.message; @@ -525,7 +530,7 @@ function validateResponse (api, code, response, responseId) { } else { try { - validateSchema(content.schema, responseId + "/content/" + contentType + "/schema", validTypes); + validateSchema(content.schema, responseId + "/content/" + contentType + "/schema", validTypes, isCircular); } catch (err) { message += err.message; @@ -547,7 +552,7 @@ function validateResponse (api, code, response, responseId) { } else { try { - validateSchema(response.schema, responseId + "/schema", validTypes); + validateSchema(response.schema, responseId + "/schema", validTypes, isCircular); } catch (err) { message += err.message; @@ -569,8 +574,9 @@ function validateResponse (api, 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; @@ -581,7 +587,7 @@ function validateSchema (schema, schemaId, validTypes) { isValid = false; } - // make sure that array's have items defined + // make sure that arrays have items defined if (schema.type === "array" && !schema.items) { message += `Validation failed. ${schemaId} is an array, so it must include an "items" schema\n`; isValid = false; @@ -596,16 +602,19 @@ function validateSchema (schema, schemaId, validTypes) { isValid = false; } - // Recursively check all the properties... - let propNames = Object.keys(schema.properties || {}); - for (let propName of propNames) { + // 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); - } - catch (err) { - message += err.message; - isValid = false; + try { + validateSchema(schema.properties[propName], schemaId + "/" + propName, schemaTypes, circular); + } + catch (err) { + message += err.message; + isValid = false; + } } } From 667bc0ab337d169bdf050cc5e663d3ef7fb82189 Mon Sep 17 00:00:00 2001 From: Stuart McGrigor Date: Thu, 27 Jan 2022 11:14:11 +1300 Subject: [PATCH 9/9] Moved declaration of SwaggerOrOpenAPIObject out of lib/index.js so Node v14 stops spitting out warning in circular dependency --- lib/index.js | 9 --------- lib/validators/schema.js | 12 +++++++++++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/index.js b/lib/index.js index 9938f5dd..5367df76 100644 --- a/lib/index.js +++ b/lib/index.js @@ -193,14 +193,5 @@ SwaggerParser.prototype.validate = async function (path, api, options, callback) } }; -/** - * The Swagger or OpenAPI object - * 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 - */ - // 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 abca7d99..f88be694 100644 --- a/lib/validators/schema.js +++ b/lib/validators/schema.js @@ -5,7 +5,17 @@ const { ono } = require("@jsdevtools/ono"); const AjvDraft4 = require("ajv-draft-04"); const Ajv = require("ajv/dist/2020"); const { openapi } = require("@apidevtools/openapi-schemas"); -const { SwaggerOrOpenAPIObject } = require("../index.js"); + +/** + * 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;