From 2822632ceaa12058ccffb28af050e6c9fa5d6475 Mon Sep 17 00:00:00 2001 From: Toby Vestal Date: Tue, 14 Mar 2023 10:49:48 -0400 Subject: [PATCH] add conditionalRequestHeaderMatch rule and tests --- functions/conditionalRequestHeaderMatch.js | 74 ++++++++++++ .../conditionalRequestHeaderMatch.spec.js | 84 ++++++++++++++ ...onal-request-etag-or-last-modified.spec.js | 94 +++++++++++++++ .../negative-oas2.yml | 49 ++++++++ .../negative.yml | 51 +++++++++ .../positive-oas2.yml | 99 ++++++++++++++++ .../positive.yml | 108 ++++++++++++++++++ 7 files changed, 559 insertions(+) create mode 100644 functions/conditionalRequestHeaderMatch.js create mode 100644 functions/conditionalRequestHeaderMatch.spec.js create mode 100644 test/conditional-request-etag-or-last-modified.spec.js create mode 100644 test/resources/conditional-request-etag-or-last-modified/negative-oas2.yml create mode 100644 test/resources/conditional-request-etag-or-last-modified/negative.yml create mode 100644 test/resources/conditional-request-etag-or-last-modified/positive-oas2.yml create mode 100644 test/resources/conditional-request-etag-or-last-modified/positive.yml diff --git a/functions/conditionalRequestHeaderMatch.js b/functions/conditionalRequestHeaderMatch.js new file mode 100644 index 0000000..019eabf --- /dev/null +++ b/functions/conditionalRequestHeaderMatch.js @@ -0,0 +1,74 @@ +/** + * Copyright 2022 Cisco Systems, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * Checks targetVal has corresponding conditional request and response header. + * @param {string} targetVal The string to lint + */ +module.exports = function (targetVal) { + if (typeof targetVal !== 'object') { + return; + } + + let successResponse = {}; + + for (const code in targetVal.responses) { + if (code.startsWith('2')) { + successResponse = targetVal.responses[code]; + break; + } + } + + if (successResponse.headers == null) { + return []; + } + + const resHeaders = []; + + for (const header in successResponse.headers) { + if (Object.values(pairs).includes(header)) { + resHeaders.push(header); + } + } + + if (targetVal.parameters == null) { + return []; + } + + for (const param of targetVal.parameters) { + if (param.in === 'header' && pairs[param.name] !== undefined) { + if (!resHeaders.includes(pairs[param.name])) { + return [ + { + message: `${ + pairs[param.name] + } is missing in response header for conditional request header ${ + param.name + }: (https://developer.cisco.com/docs/api-insights/#!api-guidelines-analyzer)`, + }, + ]; + } + } + } +}; + +const pairs = { + 'If-Unmodified-Since': 'Last-Modified', + 'If-Modified-Since': 'Last-Modified', + 'If-Range': 'Last-Modified', + 'If-Match': 'Etag', +}; diff --git a/functions/conditionalRequestHeaderMatch.spec.js b/functions/conditionalRequestHeaderMatch.spec.js new file mode 100644 index 0000000..e2f591c --- /dev/null +++ b/functions/conditionalRequestHeaderMatch.spec.js @@ -0,0 +1,84 @@ +/** + * Copyright 2022 Cisco Systems, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +const conditionalRequestHeaderMatch = require('./conditionalRequestHeaderMatch'); + +describe('conditionalRequestHeaderMatch', () => { + const targetVal = { + parameters: [ + { + in: 'header', + name: 'If-Match', + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + headers: { + Etag: { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }; + + test('should pass when header have all the values', () => { + const res = conditionalRequestHeaderMatch(targetVal); + + expect(res).toBeUndefined(); + }); + + test('should fail if pair response header is missing for conditional request', () => { + const targetVal = { + parameters: [ + { + in: 'header', + name: 'If-Match', + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + headers: { + 'Last-Modified': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }; + const res = conditionalRequestHeaderMatch(targetVal); + + expect(res).toEqual([ + { + message: + 'Etag is missing in response header for conditional request header If-Match: (https://developer.cisco.com/docs/api-insights/#!api-guidelines-analyzer)', + }, + ]); + }); +}); diff --git a/test/conditional-request-etag-or-last-modified.spec.js b/test/conditional-request-etag-or-last-modified.spec.js new file mode 100644 index 0000000..bcf0430 --- /dev/null +++ b/test/conditional-request-etag-or-last-modified.spec.js @@ -0,0 +1,94 @@ +import fsPromises from 'fs/promises'; +import path from 'path'; +import CiscoLinter from '../src/CiscoLinter'; +import { prepLinter } from '../src/util/testUtils'; +const ruleName = 'conditional-request-etag-or-last-modified'; +/** + * Copyright 2022 Cisco Systems, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +const resPath = path.join(__dirname, `resources/${ ruleName }`); + +describe(ruleName, () => { + let spectral; + + beforeAll(async () => { + spectral = new CiscoLinter(undefined); + await prepLinter(spectral, 'cisco-without-oas', ruleName); + }); + + test('should throw an error if conditional request does not contain etag or last-modified', async () => { + const spec = await fsPromises.readFile(`${ resPath }/negative.yml`); + const res = await spectral.run(spec.toString()); + + expect(res).toEqual([ + { + code: ruleName, + message: "Conditional requests are designed with 'Etag' or 'Last-Modified' headers (https://developer.cisco.com/docs/api-insights/#!api-guidelines-analyzer)", + path: [ + 'paths', + '/test/{testId}', + 'get', + ], + range: { + start: { + line: 14, + character: 8, + }, + end: { + line: 32, + character: 32, + }, + }, + severity: 1, + }, + { + code: ruleName, + message: "Conditional requests are designed with 'Etag' or 'Last-Modified' headers (https://developer.cisco.com/docs/api-insights/#!api-guidelines-analyzer)", + path: [ + 'paths', + '/anotherTest', + 'get', + ], + range: { + start: { + line: 34, + character: 8, + }, + end: { + line: 50, + character: 28, + }, + }, + severity: 1, + }, + ]); + }); + + test('should pass if conditional request contains etag or last-modified header for openapi v3', async () => { + const spec = await fsPromises.readFile(`${ resPath }/positive.yml`); + const res = await spectral.run(spec.toString()); + + expect(res).toEqual([]); + }); + + test('should pass if conditional request contains etag or last-modified header for openapi v2', async () => { + const spec = await fsPromises.readFile(`${ resPath }/positive-oas2.yml`); + const res = await spectral.run(spec.toString()); + + expect(res).toEqual([]); + }); +}); diff --git a/test/resources/conditional-request-etag-or-last-modified/negative-oas2.yml b/test/resources/conditional-request-etag-or-last-modified/negative-oas2.yml new file mode 100644 index 0000000..00eeba2 --- /dev/null +++ b/test/resources/conditional-request-etag-or-last-modified/negative-oas2.yml @@ -0,0 +1,49 @@ +swagger: '2.0' +info: + contact: + name: Scott Hardin + description: This is a sample API. + title: Sample API + version: '1.0' +host: api.example.com +basePath: / +schemes: +- http +paths: + /anotherTest: + get: + parameters: + - in: header + name: If-Range + type: string + responses: + '200': + description: No Content + headers: + Etag: + type: string + tags: + - Sample + description: get some more test data. + operationId: getAnotherTestData + /test/{testId}: + get: + parameters: + - in: header + name: If-Range + type: string + responses: + '200': + description: Created + headers: + SomeHeader: + type: string + '404': + description: Not Found + tags: + - Sample + description: get some test data. + operationId: getTestData +tags: +- description: This is a sample tag. + name: Sample diff --git a/test/resources/conditional-request-etag-or-last-modified/negative.yml b/test/resources/conditional-request-etag-or-last-modified/negative.yml new file mode 100644 index 0000000..8ab9c25 --- /dev/null +++ b/test/resources/conditional-request-etag-or-last-modified/negative.yml @@ -0,0 +1,51 @@ +openapi: '3.0.3' +info: + title: Sample API + description: This is a sample API. + version: '1.0' + contact: + name: Scott Hardin +servers: + - url: http://api.example.com +tags: + - name: Sample + description: This is a sample tag. +paths: + /test/{testId}: + get: + description: get some test data. + operationId: getTestData + tags: + - Sample + parameters: + - in: header + name: If-Range + schema: + type: string + responses: + '200': + description: Created + headers: + SomeHeader: + schema: + type: string + '404': + description: Not Found + /anotherTest: + get: + description: get some more test data. + operationId: getAnotherTestData + tags: + - Sample + parameters: + - in: header + name: If-Range + schema: + type: string + responses: + '200': + description: No Content + headers: + Etag: + schema: + type: string \ No newline at end of file diff --git a/test/resources/conditional-request-etag-or-last-modified/positive-oas2.yml b/test/resources/conditional-request-etag-or-last-modified/positive-oas2.yml new file mode 100644 index 0000000..2738a45 --- /dev/null +++ b/test/resources/conditional-request-etag-or-last-modified/positive-oas2.yml @@ -0,0 +1,99 @@ +swagger: '2.0' +info: + contact: + name: Scott Hardin + description: This is a sample API. + title: Sample API + version: '1.0' +host: api.example.com +basePath: / +schemes: +- http +paths: + /anotherTest: + get: + parameters: + - in: header + name: If-Unmodified-Since + type: string + responses: + '200': + description: No Content + headers: + Last-Modified: + type: string + tags: + - Sample + description: get some more test data. + operationId: getAnotherTestData + /correctTest/{testId}: + get: + parameters: + - in: header + name: If-Match + type: string + responses: + '200': + description: No Content + headers: + Etag: + type: string + tags: + - Sample + description: This is an endpoint that shouldn't raise an error. + operationId: getCorrectTestData + /correctTest2/{testId}: + get: + parameters: + - in: header + name: If-Modified-Since + type: string + responses: + '200': + description: No Content + headers: + Last-Modified: + type: string + tags: + - Sample + description: This is an endpoint that shouldn't raise an error. + operationId: getCorrectTestData2 + /correctTest3/{testId}: + get: + parameters: + - in: header + name: If-None-Match + type: string + responses: + '200': + description: No Content + headers: + Etag: + type: string + tags: + - Sample + description: This is an endpoint that shouldn't raise an error. + operationId: getCorrectTestData3 + /test: + post: + parameters: + - in: header + name: If-Range + type: string + responses: + '201': + description: Created + headers: + Last-Modified: + type: string + Location: + type: string + '404': + description: Not Found + tags: + - Sample + description: Create some test data. + operationId: postTestData +tags: +- description: This is a sample tag. + name: Sample diff --git a/test/resources/conditional-request-etag-or-last-modified/positive.yml b/test/resources/conditional-request-etag-or-last-modified/positive.yml new file mode 100644 index 0000000..815c3d4 --- /dev/null +++ b/test/resources/conditional-request-etag-or-last-modified/positive.yml @@ -0,0 +1,108 @@ +openapi: '3.0.3' +info: + title: Sample API + description: This is a sample API. + version: '1.0' + contact: + name: Scott Hardin +servers: + - url: http://api.example.com +tags: + - name: Sample + description: This is a sample tag. +paths: + /test: + post: + description: Create some test data. + operationId: postTestData + tags: + - Sample + parameters: + - in: header + name: If-Range + schema: + type: string + responses: + '201': + description: Created + headers: + Last-Modified: + schema: + type: string + Location: + schema: + type: string + '404': + description: Not Found + /anotherTest: + get: + description: get some more test data. + operationId: getAnotherTestData + tags: + - Sample + parameters: + - in: header + name: If-Unmodified-Since + schema: + type: string + responses: + '200': + description: No Content + headers: + Last-Modified: + schema: + type: string + /correctTest/{testId}: + get: + description: This is an endpoint that shouldn't raise an error. + operationId: getCorrectTestData + tags: + - Sample + parameters: + - in: header + name: If-Match + schema: + type: string + responses: + '200': + description: No Content + headers: + Etag: + schema: + type: string + /correctTest3/{testId}: + get: + description: This is an endpoint that shouldn't raise an error. + operationId: getCorrectTestData3 + tags: + - Sample + parameters: + - in: header + name: If-None-Match + schema: + type: string + responses: + '200': + description: No Content + headers: + Etag: + schema: + type: string + /correctTest2/{testId}: + get: + description: This is an endpoint that shouldn't raise an error. + operationId: getCorrectTestData2 + tags: + - Sample + parameters: + - in: header + name: If-Modified-Since + schema: + type: string + responses: + '200': + description: No Content + headers: + Last-Modified: + schema: + type: string \ No newline at end of file