Repository: Redocly/redocly-cli
Pull request #882 (2022-09-21 16:01:12)

fix: response-contains-header force toLowerCase on comparison
## What/Why/How?
In the `response-contains-header` rule we need to force toLowerCase on comparison for accurate results. 

## Reference
Closes  #824

## Testing
Locally.

## Screenshots (optional)
N/a

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 



| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 73.04%     |    3257/4459    |
|                                                                           🟡                                                                           | Branches   | 64.72%     |    1809/2795    |
|                                                                           🟡                                                                           | Functions  | 65.5%      |     545/832     |
|                                                                           🟡                                                                           | Lines      | 72.94%     |    3025/4147    |


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                                                     | Statements | Branches                                                     | Functions | Lines |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------- | :--------- | :----------------------------------------------------------- | :-------- | :---- |
|                                                                           🟢                                                                           | <div title="core/src/rules/common/response-contains-header.ts">`...` / response-contains-header.ts</div> | 100%       | <div title="Base coverage is: 100%">92.31% (-7.69% 🔻)</div> | 100%      | 100%  |

</details>

## Test suite run success
500 tests passing in 85 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from a37ac5ba082baeaf0fb07e398fd7af0d0b6f6ced</p>


Patch:
diff --git a/packages/core/src/rules/common/operation-2xx-response.ts b/packages/core/src/rules/common/operation-2xx-response.ts
index 6f898d74..476d09e3 100644
--- a/packages/core/src/rules/common/operation-2xx-response.ts
+++ b/packages/core/src/rules/common/operation-2xx-response.ts
@@ -4,7 +4,7 @@ import { UserContext } from '../../walk';
 export const Operation2xxResponse: Oas3Rule | Oas2Rule = () => {
   return {
     ResponsesMap(responses: Record<string, object>, { report }: UserContext) {
-      const codes = Object.keys(responses);
+      const codes = Object.keys(responses || {});
       if (!codes.some((code) => code === 'default' || /2[Xx0-9]{2}/.test(code))) {
         report({
           message: 'Operation must have at least one `2XX` response.',
diff --git a/packages/core/src/rules/common/operation-4xx-response.ts b/packages/core/src/rules/common/operation-4xx-response.ts
index e3022b21..bc4ecc3b 100644
--- a/packages/core/src/rules/common/operation-4xx-response.ts
+++ b/packages/core/src/rules/common/operation-4xx-response.ts
@@ -4,7 +4,7 @@ import { UserContext } from '../../walk';
 export const Operation4xxResponse: Oas3Rule | Oas2Rule = () => {
   return {
     ResponsesMap(responses: Record<string, object>, { report }: UserContext) {
-      const codes = Object.keys(responses);
+      const codes = Object.keys(responses || {});
 
       if (!codes.some((code) => /4[Xx0-9]{2}/.test(code))) {
         report({
diff --git a/packages/core/src/rules/common/response-contains-header.ts b/packages/core/src/rules/common/response-contains-header.ts
index 40ae786a..2d4193f3 100644
--- a/packages/core/src/rules/common/response-contains-header.ts
+++ b/packages/core/src/rules/common/response-contains-header.ts
@@ -16,7 +16,12 @@ export const ResponseContainsHeader: Oas3Rule | Oas2Rule = (options) => {
             names[getMatchingStatusCodeRange(key).toLowerCase()] ||
             [];
           for (const expectedHeader of expectedHeaders) {
-            if (!response.headers?.[expectedHeader]) {
+            if (
+              !response?.headers ||
+              !Object.keys(response?.headers).some(
+                (header) => header.toLowerCase() === expectedHeader.toLowerCase()
+              )
+            ) {
               report({
                 message: `Response object must contain a "${expectedHeader}" header.`,
                 location: location.child('headers').key(),


Test patch:
diff --git a/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts b/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts
index 0328717a..d3da9e5b 100644
--- a/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts
+++ b/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts
@@ -88,4 +88,41 @@ describe('Oas3 operation-2xx-response', () => {
 
     expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
   });
+
+  it('should report even if the responses are null', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+          openapi: 3.0.0
+          paths:
+            '/test/':
+              put:
+                responses: null
+        `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'operation-2xx-response': 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "location": Array [
+            Object {
+              "pointer": "#/paths/~1test~1/put/responses",
+              "reportOnKey": true,
+              "source": "foobar.yaml",
+            },
+          ],
+          "message": "Operation must have at least one \`2XX\` response.",
+          "ruleId": "operation-2xx-response",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
 });
diff --git a/packages/core/src/rules/common/__tests__/operation-4xx-response.test.ts b/packages/core/src/rules/common/__tests__/operation-4xx-response.test.ts
index 486d1610..8c87102c 100644
--- a/packages/core/src/rules/common/__tests__/operation-4xx-response.test.ts
+++ b/packages/core/src/rules/common/__tests__/operation-4xx-response.test.ts
@@ -127,4 +127,41 @@ describe('Oas3 operation-4xx-response', () => {
       ]
     `);
   });
+
+  it('should report even if the responses are null', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+          openapi: 3.0.0
+          paths:
+            '/test/':
+              put:
+                responses: null
+        `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'operation-2xx-response': 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "location": Array [
+            Object {
+              "pointer": "#/paths/~1test~1/put/responses",
+              "reportOnKey": true,
+              "source": "foobar.yaml",
+            },
+          ],
+          "message": "Operation must have at least one \`2XX\` response.",
+          "ruleId": "operation-2xx-response",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
 });
diff --git a/packages/core/src/rules/oas3/__tests__/response-contains-header.test.ts b/packages/core/src/rules/oas3/__tests__/response-contains-header.test.ts
index 1bdc1615..73fb4935 100644
--- a/packages/core/src/rules/oas3/__tests__/response-contains-header.test.ts
+++ b/packages/core/src/rules/oas3/__tests__/response-contains-header.test.ts
@@ -270,4 +270,120 @@ describe('Oas3 response-contains-header', () => {
     });
     expect(results).toMatchInlineSnapshot(`Array []`);
   });
+
+  it('should not report response object containing header name upper cased', async () => {
+    const document = parseYamlToDocument(outdent`
+      openapi: 3.0.3
+      info:
+        version: 3.0.0
+      paths:
+        /store/subscribe:
+          post:
+            responses:
+              '200':
+                description: successful operation
+                headers:
+                  X-Test-Header:
+                    description: calls per hour allowed by the user
+                    schema:
+                      type: integer
+                      format: int32
+		`);
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        'response-contains-header': {
+          severity: 'error',
+          names: { '2XX': ['x-test-header'] },
+        },
+      }),
+    });
+    expect(results).toMatchInlineSnapshot(`Array []`);
+  });
+
+  it('should not report response object containing header name in the rule upper cased', async () => {
+    const document = parseYamlToDocument(outdent`
+      openapi: 3.0.3
+      info:
+        version: 3.0.0
+      paths:
+        /store/subscribe:
+          post:
+            responses:
+              '200':
+                description: successful operation
+                headers:
+                  x-test-header:
+                    description: calls per hour allowed by the user
+                    schema:
+                      type: integer
+                      format: int32
+		`);
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        'response-contains-header': {
+          severity: 'error',
+          names: { '2XX': ['X-Test-Header'] },
+        },
+      }),
+    });
+    expect(results).toMatchInlineSnapshot(`Array []`);
+  });
+
+  it('should report even if the response is null', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+          openapi: 3.0.0
+          paths:
+            '/test/':
+              put:
+                responses: 
+                  '200': null
+        `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        'response-contains-header': {
+          severity: 'error',
+          names: { '2XX': ['X-Test-Header'] },
+        },
+      }),
+    });
+
+    expect(results).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "location": Array [
+            Object {
+              "pointer": "#/paths/~1test~1/put/responses/200/headers",
+              "reportOnKey": true,
+              "source": Source {
+                "absoluteRef": "foobar.yaml",
+                "body": "openapi: 3.0.0
+      paths:
+        '/test/':
+          put:
+            responses: 
+              '200': null",
+                "mimeType": undefined,
+              },
+            },
+          ],
+          "message": "Response object must contain a \\"X-Test-Header\\" header.",
+          "ruleId": "response-contains-header",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
 });