UNPKG

@redocly/openapi-core

Version:

See https://github.com/Redocly/redocly-cli

1,752 lines (1,685 loc) 50.5 kB
import * as path from 'path'; import { outdent } from 'outdent'; import { lintFromString, lintConfig, lintDocument, lint } from '../lint'; import { BaseResolver } from '../resolve'; import { createConfig, loadConfig } from '../config/load'; import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../__tests__/utils'; import { detectSpec } from '../oas-types'; import { rootRedoclyConfigSchema } from '@redocly/config'; import { createConfigTypes } from '../types/redocly-yaml'; const testPortalConfig = parseYamlToDocument( outdent` licenseKey: 123 # Must be a string apis: without-root: foo: Not expected! output: file.json with-wrong-root: root: 456 # Must be a string with-theme: root: ./openapi.yaml theme: openapi: wrong, must be an object not-expected: Must fail seo: keywords: 789 # Must be an array redirects: some-redirect: t1o: Wrong name, should be 'two' type: wrong type, must be a number rbac: 'team-b.md': TeamB: read team-c.md: TeamC: read /blog/*: anonymous: none authenticated: read /blogpost/: TeamD: none '**/*.md': TeamA: none authenticated: none '*': read 'blog/april-2022.md': TeamA: none TeamC: read test.md: TeamC: none TeamB: none authenticated: none '*': read test/**: TeamB: read TeamC: read authenticated: read anonymous: read additional-property: something: 123 # Must be a string content: '**': additionalProp: 456 # Must be a stirng foo: additionalProp2: 789 # Must be a stirng responseHeaders: some-header: wrong, must be an array some-header2: - wrong, must be an object - unexpected-property: Should fail # name: Must be reported as a missing required prop value: 123 # Must be a string ssoDirect: oidc: title: 456 # Must be a string type: OIDC configurationUrl: http://localhost/oidc/.well-known/openid-configuration clientId: '{{ process.env.public }}' clientSecret: '{{ process.env.secret }}' teamsClaimName: https://test.com scopes: - openid audience: - default authorizationRequestCustomParams: login_hint: 789 # Must be a string prompt: login configuration: token_endpoint: 123 # Must be a string # authorization_endpoint: Must be reported as a missing required prop additional-propery: Must be allowed defaultTeams: - 456 # Must be a string sso-config-schema-without-configurationUrl: type: OIDC # clientId: Must be reported as a missing required prop # configurationUrl: Must be reported as a missing required prop clientSecret: '{{ process.env.secret }}' sso: - WRONG # Does not match allowed options developerOnboarding: wrong: A not allowed field adapters: - should be object - type: 123 # Must be a string - type: APIGEE_X # organizationName: Must be reported as a missing required prop auth: type: OAUTH2 # tokenEndpoint: Must be reported as a missing required prop clientId: 456 # Must be a string clientSecret: '{{ process.env.secret }}' not-expected: Must fail - type: APIGEE_X organizationName: Test auth: type: SERVICE_ACCOUNT # serviceAccountPrivateKey: Must be reported as a missing required prop serviceAccountEmail: 789 # Must be a string l10n: defaultLocale: en-US locales: - code: 123 # Must be a string name: English - code: es-ES name: Spanish metadata: test: anything not-listed-filed: Must be reported as not expected env: some-env: mockServer: off: must be boolean not-expected: Must fail apis: no-root: # root: Must be defined rules: {} wrong-root: root: 789 # Must be a string theme: breadcrumbs: hide: false prefixItems: - label: Home page: '/' imports: - '@redocly/theme-experimental' logo: srcSet: './images/redocly-black-logo.svg light, ./images/redocly-brand-logo.svg dark' altText: Test link: / asyncapi: hideInfo: false expandSchemas: root: true elements: true navbar: items: - label: Markdown page: /markdown/ search: shortcuts: - ctrl+f - cmd+k - / suggestedPages: - label: TSX page page: tsx.page.tsx - page: /my-catalog/ footer: copyrightText: Copyright © Test 2019-2020. items: - group: Legal items: - label: Terms of Use href: 'https://test.com/' # Not expected markdown: lastUpdatedBlock: format: 'long' editPage: baseUrl: https://test.com graphql: menu: requireExactGroups: false groups: - name: 'GraphQL custom group' directives: includeByName: - cacheControl - typeDirective otherItemsGroupName: 'Other' sidebar: separatorLine: true linePosition: top catalog: main: title: API Catalog description: 'This is a description of the API Catalog' slug: /my-catalog/ filters: - title: Domain property: domain missingCategoryName: Other - title: API Category property: category missingCategoryName: Other groupByFirstFilter: false items: - directory: ./ flatten: true includeByMetadata: type: [openapi] scorecard: ignoreNonCompliant: true levels: - name: Baseline extends: - minimal - name: Silver extends: - recommended rules: info-description: off - name: Gold rules: rule/path-item-get-required: severity: warn subject: type: PathItem message: Every path item must have a GET operation. assertions: required: - get operation-4xx-response: warn targets: - where: metadata: l0: Distribution publishDateRange: 2021-01-01T00:00:00Z/2022-01-01 minimumLevel: Silver `, '' ); describe('lint', () => { it('lintFromString should work', async () => { const results = await lintFromString({ absoluteRef: '/test/spec.yaml', source: outdent` openapi: 3.0.0 info: title: Test API version: "1.0" description: Test license: Fail servers: - url: http://redocly-example.com paths: {} `, config: await loadConfig({ configPath: path.join(__dirname, 'fixtures/redocly.yaml') }), }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/info/license", "reportOnKey": false, "source": "/test/spec.yaml", }, ], "message": "Expected type \`License\` (object) but got \`string\`", "ruleId": "struct", "severity": "error", "suggest": [], }, ] `); }); it('lint should work', async () => { const results = await lint({ ref: path.join(__dirname, 'fixtures/lint/openapi.yaml'), config: await loadConfig({ configPath: path.join(__dirname, 'fixtures/redocly.yaml'), }), }); expect(replaceSourceWithRef(results, path.join(__dirname, 'fixtures/lint/'))) .toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/info/license", "reportOnKey": false, "source": "openapi.yaml", }, ], "message": "Expected type \`License\` (object) but got \`string\`", "ruleId": "struct", "severity": "error", "suggest": [], }, ] `); }); it('lintConfig should work', async () => { const document = parseYamlToDocument( outdent` apis: error string plugins: - './local-plugin.js' extends: - recommended - local/all rules: operation-2xx-response: warn no-invalid-media-type-examples: error path-http-verbs-order: error boolean-parameter-prefixes: off rule/operation-summary-length: subject: type: Operation property: summary message: Operation summary should start with an active verb assertions: local/checkWordsCount: min: 3 theme: openapi: showConsole: true # Not expected anymore layout: wrong-option `, '' ); const config = await createConfig({}); const results = await lintConfig({ document, config }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/apis", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`ConfigApis\` (object) but got \`string\`", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/theme/openapi/layout", "reportOnKey": false, "source": "", }, ], "message": "\`layout\` can be one of the following only: "stacked", "three-panel".", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, ] `); }); it('lintConfig should detect wrong fields and suggest correct ones', async () => { const document = parseYamlToDocument( outdent` api: name@version: root: ./file.yaml rules: operation-2xx-response: warn `, '' ); const config = await createConfig({}); const results = await lintConfig({ document, config }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/api", "reportOnKey": true, "source": "", }, ], "message": "Property \`api\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [ "apis", "seo", "sso", "env", ], }, ] `); }); it('lintConfig should work with legacy fields - referenceDocs', async () => { const document = parseYamlToDocument( outdent` apis: entry: root: ./file.yaml rules: operation-2xx-response: warn referenceDocs: showConsole: true `, '' ); const config = await createConfig({}); const results = await lintConfig({ document, config }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/referenceDocs", "reportOnKey": true, "source": "", }, ], "message": "Property \`referenceDocs\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, ] `); }); it("'plugins' shouldn't be allowed in 'apis'", async () => { const document = parseYamlToDocument( outdent` apis: main: root: ./main.yaml plugins: - './local-plugin.js' plugins: - './local-plugin.js' `, '' ); const config = await createConfig({}); const results = await lintConfig({ document, config }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/apis/main/plugins", "reportOnKey": true, "source": "", }, ], "message": "Property \`plugins\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, ] `); }); it('lintConfig should detect wrong fields in the default configuration after merging with the portal config schema', async () => { const document = testPortalConfig; const config = await createConfig({}); const results = await lintConfig({ document, config }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/licenseKey", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/sso/0", "reportOnKey": false, "source": "", }, ], "message": "\`sso\` can be one of the following only: "REDOCLY", "CORPORATE", "GUEST".", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/not-listed-filed", "reportOnKey": true, "source": "", }, ], "message": "Property \`not-listed-filed\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/redirects/some-redirect/t1o", "reportOnKey": true, "source": "", }, ], "message": "Property \`t1o\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [ "to", "type", ], }, { "from": undefined, "location": [ { "pointer": "#/redirects/some-redirect/type", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`number\` but got \`string\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/seo/keywords", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`array\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/rbac/content/**/additionalProp", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/rbac/content/foo/additionalProp2", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/rbac/additional-property/something", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/responseHeaders/some-header", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`rootRedoclyConfigSchema.responseHeaders_additionalProperties\` (array) but got \`string\`", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/responseHeaders/some-header2/0", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`rootRedoclyConfigSchema.responseHeaders_additionalProperties_items\` (object) but got \`string\`", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/responseHeaders/some-header2/1", "reportOnKey": true, "source": "", }, ], "message": "The field \`name\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/responseHeaders/some-header2/1/unexpected-property", "reportOnKey": true, "source": "", }, ], "message": "Property \`unexpected-property\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/responseHeaders/some-header2/1/value", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/without-root", "reportOnKey": true, "source": "", }, ], "message": "The field \`root\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/without-root/foo", "reportOnKey": true, "source": "", }, ], "message": "Property \`foo\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [ "root", ], }, { "from": undefined, "location": [ { "pointer": "#/apis/with-wrong-root/root", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/with-theme/theme/not-expected", "reportOnKey": true, "source": "", }, ], "message": "Property \`not-expected\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/with-theme/theme/openapi", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`rootRedoclyConfigSchema.apis_additionalProperties.theme.openapi\` (object) but got \`string\`", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/oidc/title", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/oidc/defaultTeams/0", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/oidc/configuration", "reportOnKey": true, "source": "", }, ], "message": "The field \`authorization_endpoint\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/oidc/configuration/token_endpoint", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/oidc/authorizationRequestCustomParams/login_hint", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/sso-config-schema-without-configurationUrl", "reportOnKey": true, "source": "", }, ], "message": "The field \`clientId\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect/sso-config-schema-without-configurationUrl", "reportOnKey": true, "source": "", }, ], "message": "The field \`configurationUrl\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/wrong", "reportOnKey": true, "source": "", }, ], "message": "Property \`wrong\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/0", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`APIGEE_X\` (object) but got \`string\`", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/1", "reportOnKey": true, "source": "", }, ], "message": "The field \`organizationName\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/1", "reportOnKey": true, "source": "", }, ], "message": "The field \`auth\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/1/type", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/2", "reportOnKey": true, "source": "", }, ], "message": "The field \`organizationName\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/2/auth", "reportOnKey": true, "source": "", }, ], "message": "The field \`tokenEndpoint\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/2/auth/clientId", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/2/auth/not-expected", "reportOnKey": true, "source": "", }, ], "message": "Property \`not-expected\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/3/auth", "reportOnKey": true, "source": "", }, ], "message": "The field \`serviceAccountPrivateKey\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding/adapters/3/auth/serviceAccountEmail", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/l10n/locales/0/code", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/env/some-env/mockServer/off", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`boolean\` but got \`string\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/env/some-env/mockServer/not-expected", "reportOnKey": true, "source": "", }, ], "message": "Property \`not-expected\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/env/some-env/apis/no-root", "reportOnKey": true, "source": "", }, ], "message": "The field \`root\` must be present on this level.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/env/some-env/apis/wrong-root/root", "reportOnKey": false, "source": "", }, ], "message": "Expected type \`string\` but got \`integer\`.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, ] `); }); it('lintConfig should alternate its behavior when supplied externalConfigTypes', async () => { const document = testPortalConfig; const config = await createConfig({}); const results = await lintConfig({ document, externalConfigTypes: createConfigTypes( { type: 'object', properties: { theme: rootRedoclyConfigSchema.properties.theme }, additionalProperties: false, }, config ), config, }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/licenseKey", "reportOnKey": true, "source": "", }, ], "message": "Property \`licenseKey\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/seo", "reportOnKey": true, "source": "", }, ], "message": "Property \`seo\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/redirects", "reportOnKey": true, "source": "", }, ], "message": "Property \`redirects\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/rbac", "reportOnKey": true, "source": "", }, ], "message": "Property \`rbac\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/responseHeaders", "reportOnKey": true, "source": "", }, ], "message": "Property \`responseHeaders\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/ssoDirect", "reportOnKey": true, "source": "", }, ], "message": "Property \`ssoDirect\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/sso", "reportOnKey": true, "source": "", }, ], "message": "Property \`sso\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/developerOnboarding", "reportOnKey": true, "source": "", }, ], "message": "Property \`developerOnboarding\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/l10n", "reportOnKey": true, "source": "", }, ], "message": "Property \`l10n\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/metadata", "reportOnKey": true, "source": "", }, ], "message": "Property \`metadata\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/not-listed-filed", "reportOnKey": true, "source": "", }, ], "message": "Property \`not-listed-filed\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/env", "reportOnKey": true, "source": "", }, ], "message": "Property \`env\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/without-root/foo", "reportOnKey": true, "source": "", }, ], "message": "Property \`foo\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/without-root/output", "reportOnKey": true, "source": "", }, ], "message": "Property \`output\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/with-wrong-root/root", "reportOnKey": true, "source": "", }, ], "message": "Property \`root\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/with-theme/root", "reportOnKey": true, "source": "", }, ], "message": "Property \`root\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, { "from": undefined, "location": [ { "pointer": "#/apis/with-theme/theme", "reportOnKey": true, "source": "", }, ], "message": "Property \`theme\` is not expected here.", "ruleId": "configuration spec", "severity": "error", "suggest": [], }, ] `); }); it("'const' can have any type", async () => { const document = parseYamlToDocument( outdent` openapi: "3.1.0" info: version: 1.0.0 title: Swagger Petstore description: Information about Petstore license: name: MIT url: https://opensource.org/licenses/MIT servers: - url: http://petstore.swagger.io/v1 paths: /pets: get: summary: List all pets operationId: listPets tags: - pets responses: 200: description: An paged array of pets content: application/json: schema: type: string const: ABC `, 'foobar.yaml' ); const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: await makeConfig({ rules: { spec: 'error' } }), }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); it('detect OpenAPI should throw an error when version is not string', () => { const testDocument = parseYamlToDocument( outdent` openapi: 3.0 `, '' ); expect(() => detectSpec(testDocument.parsed)).toThrow( `Invalid OpenAPI version: should be a string but got "number"` ); }); it('detect unsupported OpenAPI version', () => { const testDocument = parseYamlToDocument( outdent` openapi: 1.0.4 `, '' ); expect(() => detectSpec(testDocument.parsed)).toThrow(`Unsupported OpenAPI version: 1.0.4`); }); it('detect unsupported AsyncAPI version', () => { const testDocument = parseYamlToDocument( outdent` asyncapi: 1.0.4 `, '' ); expect(() => detectSpec(testDocument.parsed)).toThrow(`Unsupported AsyncAPI version: 1.0.4`); }); it('detect unsupported spec format', () => { const testDocument = parseYamlToDocument( outdent` notapi: 3.1.0 `, '' ); expect(() => detectSpec(testDocument.parsed)).toThrow(`Unsupported specification`); }); it("spec rule shouldn't throw an error for named callback", async () => { const document = parseYamlToDocument( outdent` openapi: 3.1.0 info: title: Callback test version: 'alpha' components: callbacks: resultCallback: '{$url}': post: requestBody: description: Callback payload content: 'application/json': schema: type: object properties: test: type: string responses: '200': description: callback successfully processed `, 'foobar.yaml' ); const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: await makeConfig({ rules: { spec: 'error' } }), }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); it('should ignore error because ignore file passed', async () => { const absoluteRef = path.join(__dirname, 'fixtures/openapi.yaml'); const document = parseYamlToDocument( outdent` openapi: 3.0.0 info: version: 1.0.0 title: Example OpenAPI 3 definition. description: Information about API license: name: MIT url: 'https://opensource.org/licenses/MIT' servers: - url: 'https://redocly.com/v1' paths: '/pets/{petId}': post: responses: '201': summary: Exist description: example description `, absoluteRef ); const configFilePath = path.join(__dirname, 'fixtures'); const result = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: await makeConfig({ rules: { 'operation-operationId': 'error' }, decorators: undefined, configPath: configFilePath, }), }); expect(result).toHaveLength(1); expect(result).toMatchObject([ { ignored: true, location: [{ pointer: '#/paths/~1pets~1{petId}/post/operationId' }], message: 'Operation object should contain `operationId` field.', ruleId: 'operation-operationId', severity: 'error', }, ]); expect(result[0]).toHaveProperty('ignored', true); expect(result[0]).toHaveProperty('ruleId', 'operation-operationId'); }); it('should throw an error for dependentRequired not expected here - OAS 3.0.x', async () => { const document = parseYamlToDocument( outdent` openapi: 3.0.3 info: title: test json schema validation keyword - dependentRequired version: 1.0.0 paths: '/thing': get: summary: a sample api responses: '200': description: OK content: 'application/json': schema: $ref: '#/components/schemas/test_schema' examples: dependentRequired_passing: summary: an example schema value: { "name": "bobby", "age": 25} dependentRequired_failing: summary: an example schema value: { "name": "jennie"} components: schemas: test_schema: type: object properties: name: type: string age: type: number dependentRequired: name: - age `, '' ); const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml'); const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: await makeConfig({ rules: { spec: 'error' }, decorators: undefined, configPath: configFilePath, }), }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": { "pointer": "#/paths/~1thing/get/responses/200/content/application~1json/schema", "source": "", }, "location": [ { "pointer": "#/components/schemas/test_schema/dependentRequired", "reportOnKey": true, "source": "", }, ], "message": "Property \`dependentRequired\` is not expected here.", "ruleId": "struct", "severity": "error", "suggest": [], }, ] `); }); it('should not throw an error for dependentRequired not expected here - OAS 3.1.x', async () => { const document = parseYamlToDocument( outdent` openapi: 3.1.0 info: title: test json schema validation keyword - dependentRequired version: 1.0.0 paths: '/thing': get: summary: a sample api responses: '200': description: OK content: 'application/json': schema: $ref: '#/components/schemas/test_schema' examples: dependentRequired_passing: summary: an example schema value: { "name": "bobby", "age": 25} dependentRequired_failing: summary: an example schema value: { "name": "jennie"} components: schemas: test_schema: type: object properties: name: type: string age: type: number dependentRequired: name: - age `, '' ); const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml'); const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: await makeConfig({ rules: { spec: 'error' }, decorators: undefined, configPath: configFilePath, }), }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); it('should throw an error for $schema not expected here - OAS 3.0.x', async () => { const document = parseYamlToDocument( outdent` openapi: 3.0.4 info: title: test json schema validation keyword $schema - should use an OAS Schema, not JSON Schema version: 1.0.0 paths: '/thing': get: summary: a sample api responses: '200': description: OK content: 'application/json': schema: $schema: http://json-schema.org/draft-04/schema# type: object properties: {} `, '' ); const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml'); const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: await makeConfig({ rules: { spec: 'error' }, decorators: undefined, configPath: configFilePath, }), }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` [ { "from": undefined, "location": [ { "pointer": "#/paths/~1thing/get/responses/200/content/application~1json/schema/$schema", "reportOnKey": true, "source": "", }, ], "message": "Property \`$schema\` is not expected here.", "ruleId": "struct", "severity": "error", "suggest": [], }, ] `); }); it('should allow for $schema to be defined - OAS 3.1.x', async () => { const document = parseYamlToDocument( outdent` openapi: 3.1.1 info: title: test json schema validation keyword $schema - should allow a JSON Schema version: 1.0.0 paths: '/thing': get: summary: a sample api responses: '200': description: OK content: 'application/json': schema: $schema: http://json-schema.org/draft-04/schema#