Repository: Redocly/redocly-cli
Pull request #891 (2022-09-30 08:14:45)

fix: validate types in spec rule
## What/Why/How?

Validate types list in spec rule

## Reference
closes https://github.com/Redocly/redocly-cli/issues/856
## Testing

## Screenshots (optional)
<img width="959" alt="Знімок екрана 2022-09-30 о 11 46 27" src="https://user-images.githubusercontent.com/106662428/193231200-ea60f450-e210-423c-9e32-9fa84e9c04b5.png">


## 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.06%     |    3265/4469    |
|                                                                           🟡                                                                           | Branches   | 64.46%     |    1828/2836    |
|                                                                           🟡                                                                           | Functions  | 65.47%     |     546/834     |
|                                                                           🟡                                                                           | Lines      | 72.96%     |    3033/4157    |


<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/spec.ts">`...` / spec.ts</div> | <div title="Base coverage is: 91.78%">85% (-6.78% 🔻)</div> | <div title="Base coverage is: 78.13%">63.22% (-14.91% 🔻)</div> | <div title="Base coverage is: 100%">75% (-25% 🔻)</div> | <div title="Base coverage is: 91.18%">84% (-7.18% 🔻)</div> |

</details>

## Test suite run success
505 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 117e31e984bd3288362843683e74eb456f6a74b2</p>


Patch:
diff --git a/packages/core/src/rules/common/spec.ts b/packages/core/src/rules/common/spec.ts
index 5c7c50df..d2ec1baf 100644
--- a/packages/core/src/rules/common/spec.ts
+++ b/packages/core/src/rules/common/spec.ts
@@ -1,8 +1,9 @@
 import type { Oas3Rule, Oas2Rule } from '../../visitors';
 import { isNamedType } from '../../types';
-import { oasTypeOf, matchesJsonSchemaType, getSuggest } from '../utils';
+import { oasTypeOf, matchesJsonSchemaType, getSuggest, validateSchemaEnumType } from '../utils';
 import { isRef } from '../../ref-utils';
 import { isPlainObject } from '../../utils';
+import { UserContext } from '../../walk';
 
 export const OasSpec: Oas3Rule | Oas2Rule = () => {
   return {
@@ -114,17 +115,20 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
           propValue = resolve(propValue).node;
         }
 
-        if (propSchema.enum) {
-          if (!propSchema.enum.includes(propValue)) {
-            report({
-              location: propLocation,
-              message: `\`${propName}\` can be one of the following only: ${propSchema.enum
-                .map((i) => `"${i}"`)
-                .join(', ')}.`,
-              from: refLocation,
-              suggest: getSuggest(propValue, propSchema.enum),
-            });
+        if (propSchema.items && propSchema.items?.enum && Array.isArray(propValue)) {
+          for (let i = 0; i < propValue.length; i++) {
+            validateSchemaEnumType(propSchema.items?.enum, propValue[i], propName, refLocation, {
+              report,
+              location: location.child([propName, i]),
+            } as UserContext);
           }
+        }
+
+        if (propSchema.enum) {
+          validateSchemaEnumType(propSchema.enum, propValue, propName, refLocation, {
+            report,
+            location: location.child([propName]),
+          } as UserContext);
         } else if (propSchema.type && !matchesJsonSchemaType(propValue, propSchema.type, false)) {
           report({
             message: `Expected type \`${propSchema.type}\` but got \`${propValueType}\`.`,
diff --git a/packages/core/src/rules/utils.ts b/packages/core/src/rules/utils.ts
index b7407b3d..0184a066 100644
--- a/packages/core/src/rules/utils.ts
+++ b/packages/core/src/rules/utils.ts
@@ -138,3 +138,25 @@ export function getAdditionalPropertiesOption(opts: Record<string, any>): boolea
   showWarningForDeprecatedField('disallowAdditionalProperties', 'allowAdditionalProperties');
   return !opts.disallowAdditionalProperties;
 }
+
+export function validateSchemaEnumType(
+  schemaEnum: string[],
+  propertyValue: string,
+  propName: string,
+  refLocation: Location | undefined,
+  { report, location }: UserContext
+) {
+  if (!schemaEnum) {
+    return;
+  }
+  if (!schemaEnum.includes(propertyValue === null ? 'null' : propertyValue)) {
+    report({
+      location,
+      message: `\`${propName}\` can be one of the following only: ${schemaEnum
+        .map((type) => `"${type}"`)
+        .join(', ')}.`,
+      from: refLocation,
+      suggest: getSuggest(propertyValue, schemaEnum),
+    });
+  }
+}


Test patch:
diff --git a/packages/core/src/rules/common/__tests__/spec.test.ts b/packages/core/src/rules/common/__tests__/spec.test.ts
index b2190acd..1d45dfb4 100644
--- a/packages/core/src/rules/common/__tests__/spec.test.ts
+++ b/packages/core/src/rules/common/__tests__/spec.test.ts
@@ -139,3 +139,128 @@ describe('Oas3 spec', () => {
     `);
   });
 });
+
+describe('Oas3.1 spec', () => {
+  it('should report with "type can be one of the following only"', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+      openapi: 3.1.0
+      info:
+        version: 1.0.0
+        title: Example.com
+        description: info,
+        license:
+          name: Apache 2.0
+          url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
+      components:
+        schemas:
+          TestSchema:
+            title: TestSchema
+            description: Property name's description
+            type: test
+        `
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ spec: 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "from": undefined,
+          "location": Array [
+            Object {
+              "pointer": "#/components/schemas/TestSchema/type",
+              "reportOnKey": false,
+              "source": "",
+            },
+          ],
+          "message": "\`type\` can be one of the following only: \\"object\\", \\"array\\", \\"string\\", \\"number\\", \\"integer\\", \\"boolean\\", \\"null\\".",
+          "ruleId": "spec",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
+
+  it('should report with unknown type in type`s list', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+      openapi: 3.1.0
+      info:
+        version: 1.0.0
+        title: Example.com
+        description: info,
+        license:
+          name: Apache 2.0
+          url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
+      components:
+        schemas:
+          TestSchema:
+            title: TestSchema
+            description: Property name's description
+            type:
+              - string
+              - foo
+        `
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ spec: 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "from": undefined,
+          "location": Array [
+            Object {
+              "pointer": "#/components/schemas/TestSchema/type/1",
+              "reportOnKey": false,
+              "source": "",
+            },
+          ],
+          "message": "\`type\` can be one of the following only: \\"object\\", \\"array\\", \\"string\\", \\"number\\", \\"integer\\", \\"boolean\\", \\"null\\".",
+          "ruleId": "spec",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
+
+  it('should not report about unknown type', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+      openapi: 3.1.0
+      info:
+        version: 1.0.0
+        title: Example.com
+        description: info,
+        license:
+          name: Apache 2.0
+          url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
+      components:
+        schemas:
+          TestSchema:
+            title: TestSchema
+            description: Property name's description
+            type: null
+        `
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ spec: 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
+  });
+});