UNPKG

@redocly/openapi-core

Version:

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

1,486 lines (1,378 loc) 40.1 kB
import outdent from 'outdent'; import each from 'jest-each'; import * as path from 'path'; import { lintDocument } from '../src/lint'; import { parseYamlToDocument, replaceSourceWithRef, makeConfigForRuleset } from './utils'; import { BaseResolver, Document } from '../src/resolve'; import { listOf } from '../src/types'; import { Oas3RuleSet } from '../src/oas-types'; describe('walk order', () => { it('should run visitors', async () => { const visitors = { DefinitionRoot: { enter: jest.fn(), leave: jest.fn(), }, Info: { enter: jest.fn(), leave: jest.fn(), }, Contact: { enter: jest.fn(), leave: jest.fn(), }, License: { enter: jest.fn(), leave: jest.fn(), }, }; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return visitors; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 info: contact: {} license: {} `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(testRuleSet.test).toBeCalledTimes(1); for (const fns of Object.values(visitors)) { expect(fns.enter).toBeCalled(); expect(fns.leave).toBeCalled(); } }); it('should run nested visitors correctly', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Operation: { enter: jest.fn((op) => calls.push(`enter operation: ${op.operationId}`)), leave: jest.fn((op) => calls.push(`leave operation: ${op.operationId}`)), Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push( `enter operation ${parents.Operation.operationId} > param ${param.name}`, ), ), leave: jest.fn((param, _ctx, parents) => calls.push( `leave operation ${parents.Operation.operationId} > param ${param.name}`, ), ), }, }, Parameter: { enter: jest.fn((param) => calls.push(`enter param ${param.name}`)), leave: jest.fn((param) => calls.push(`leave param ${param.name}`)), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 info: contact: {} license: {} paths: /pet: parameters: - name: path-param get: operationId: get parameters: - name: get_a - name: get_b post: operationId: post parameters: - name: post_a `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter param path-param", "leave param path-param", "enter operation: get", "enter operation get > param get_a", "enter param get_a", "leave param get_a", "leave operation get > param get_a", "enter operation get > param get_b", "enter param get_b", "leave param get_b", "leave operation get > param get_b", "leave operation: get", "enter operation: post", "enter operation post > param post_a", "enter param post_a", "leave param post_a", "leave operation post > param post_a", "leave operation: post", ] `); }); it('should run nested visitors correctly oas2', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Operation: { enter: jest.fn((op) => calls.push(`enter operation: ${op.operationId}`)), leave: jest.fn((op) => calls.push(`leave operation: ${op.operationId}`)), Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push( `enter operation ${parents.Operation.operationId} > param ${param.name}`, ), ), leave: jest.fn((param, _ctx, parents) => calls.push( `leave operation ${parents.Operation.operationId} > param ${param.name}`, ), ), }, }, Parameter: { enter: jest.fn((param) => calls.push(`enter param ${param.name}`)), leave: jest.fn((param) => calls.push(`leave param ${param.name}`)), }, }; }), }; const document = parseYamlToDocument( outdent` swagger: "2.0" info: contact: {} license: {} paths: /pet: parameters: - name: path-param get: operationId: get parameters: - name: get_a - name: get_b post: operationId: post parameters: - name: post_a `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet, undefined, 'oas2'), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter param path-param", "leave param path-param", "enter operation: get", "enter operation get > param get_a", "enter param get_a", "leave param get_a", "leave operation get > param get_a", "enter operation get > param get_b", "enter param get_b", "leave param get_b", "leave operation get > param get_b", "leave operation: get", "enter operation: post", "enter operation post > param post_a", "enter param post_a", "leave param post_a", "leave operation post > param post_a", "leave operation: post", ] `); }); it('should resolve refs', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Operation: { enter: jest.fn((op) => calls.push(`enter operation: ${op.operationId}`)), leave: jest.fn((op) => calls.push(`leave operation: ${op.operationId}`)), Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push( `enter operation ${parents.Operation.operationId} > param ${param.name}`, ), ), leave: jest.fn((param, _ctx, parents) => calls.push( `leave operation ${parents.Operation.operationId} > param ${param.name}`, ), ), }, }, Parameter: { enter: jest.fn((param) => calls.push(`enter param ${param.name}`)), leave: jest.fn((param) => calls.push(`leave param ${param.name}`)), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 info: contact: {} license: {} paths: /pet: get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b post: operationId: post parameters: - $ref: '#/components/parameters/shared_a' components: parameters: shared_a: name: shared-a `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter operation: get", "enter operation get > param shared-a", "enter param shared-a", "leave param shared-a", "leave operation get > param shared-a", "enter operation get > param get_b", "enter param get_b", "leave param get_b", "leave operation get > param get_b", "leave operation: get", "enter operation: post", "enter operation post > param shared-a", "leave operation post > param shared-a", "leave operation: post", ] `); }); it('should visit with context same refs with gaps in visitor simple', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { PathItem: { Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`), ), }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: $ref: '#/components/fake_parameters_list' get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b /dog: id: dog post: operationId: post parameters: - $ref: '#/components/parameters/shared_a' components: fake_parameters_list: - name: path-param parameters: shared_a: name: shared-a `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter path pet > param path-param", "enter path pet > param shared-a", "enter path pet > param get_b", "enter path dog > param shared-a", ] `); }); it('should correctly visit more specific visitor', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { PathItem: { Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`), ), }, Operation: { Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push( `enter operation ${parents.Operation.operationId} > param ${param.name}`, ), ), }, }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: path-param get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b - name: get_c /dog: id: dog post: operationId: post parameters: - $ref: '#/components/parameters/shared_b' components: parameters: shared_a: name: shared-a shared_b: name: shared-b `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter path pet > param path-param", "enter operation get > param shared-a", "enter operation get > param get_b", "enter operation get > param get_c", "enter operation post > param shared-b", ] `); }); it('should visit with context same refs with gaps in visitor and nested rule', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { PathItem: { Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`), ), leave: jest.fn((param, _ctx, parents) => calls.push(`leave path ${parents.PathItem.id} > param ${param.name}`), ), }, Operation(op, _ctx, parents) { calls.push(`enter path ${parents.PathItem.id} > op ${op.operationId}`); }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: path-param get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b /dog: id: dog post: operationId: post parameters: - $ref: '#/components/parameters/shared_a' components: parameters: shared_a: name: shared-a `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter path pet > param path-param", "leave path pet > param path-param", "enter path pet > op get", "enter path pet > param shared-a", "leave path pet > param shared-a", "enter path pet > param get_b", "leave path pet > param get_b", "enter path dog > op post", "enter path dog > param shared-a", "leave path dog > param shared-a", ] `); }); it('should visit and do not recurse for circular refs top-level', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Schema: jest.fn((schema: any) => calls.push(`enter schema ${schema.id}`)), }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: path-param schema: $ref: "#/components/parameters/shared_a" components: parameters: shared_a: id: 'shared_a' allOf: - $ref: "#/components/parameters/shared_a" - id: 'nested' `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter schema shared_a", "enter schema nested", ] `); }); it('should visit and do not recurse for circular refs with context', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Parameter: { Schema: jest.fn((schema: any, _ctx, parents) => calls.push(`enter param ${parents.Parameter.name} > schema ${schema.id}`), ), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: a schema: $ref: "#/components/parameters/shared_a" - name: b schema: $ref: "#/components/parameters/shared_a" components: parameters: shared_a: id: 'shared_a' properties: a: id: a allOf: - $ref: "#/components/parameters/shared_a" - id: 'nested' `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter param a > schema shared_a", "enter param b > schema shared_a", ] `); }); it('should correctly skip top level', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Operation: { skip: (op) => op.operationId === 'put', enter: jest.fn((op) => calls.push(`enter operation ${op.operationId}`)), leave: jest.fn((op) => calls.push(`leave operation ${op.operationId}`)), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: get: operationId: get put: operationId: put `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter operation get", "leave operation get", ] `); }); it('should correctly skip nested levels', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Operation: { skip: (op) => op.operationId === 'put', Parameter: jest.fn((param, _ctx, parents) => calls.push(`enter operation ${parents.Operation.operationId} > param ${param.name}`), ), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b - name: get_c put: operationId: put parameters: - $ref: '#/components/parameters/shared_a' - name: get_b - name: get_c components: parameters: shared_a: name: shared-a `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter operation get > param shared-a", "enter operation get > param get_b", "enter operation get > param get_c", ] `); }); it('should correctly visit more specific visitor with skips', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { PathItem: { Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`), ), leave: jest.fn((param, _ctx, parents) => calls.push(`leave path ${parents.PathItem.id} > param ${param.name}`), ), }, Operation: { skip: (op) => op.operationId === 'put', Parameter: { enter: jest.fn((param, _ctx, parents) => calls.push( `enter operation ${parents.Operation.operationId} > param ${param.name}`, ), ), leave: jest.fn((param, _ctx, parents) => calls.push( `leave operation ${parents.Operation.operationId} > param ${param.name}`, ), ), }, }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: path-param get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b - name: get_c put: operationId: put parameters: - $ref: '#/components/parameters/shared_a' - name: get_b - name: get_c /dog: id: dog post: operationId: post parameters: - $ref: '#/components/parameters/shared_b' components: parameters: shared_a: name: shared-a shared_b: name: shared-b `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter path pet > param path-param", "leave path pet > param path-param", "enter operation get > param shared-a", "leave operation get > param shared-a", "enter operation get > param get_b", "leave operation get > param get_b", "enter operation get > param get_c", "leave operation get > param get_c", "enter operation post > param shared-b", "leave operation post > param shared-b", ] `); }); it('should correctly visit with nested rules', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Schema: { Schema: { enter: jest.fn((schema: any, _ctx, parents) => calls.push(`enter nested schema ${parents.Schema.id} > ${schema.id}`), ), leave: jest.fn((schema: any, _ctx, parents) => calls.push(`leave nested schema ${parents.Schema.id} > ${schema.id}`), ), }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: get: requestBody: content: application/json: schema: id: inline-top type: object properties: b: $ref: "#/components/schemas/b" a: type: object id: inline-nested-2 properties: a: id: inline-nested-nested-2 components: schemas: b: id: inline-top type: object properties: a: type: object id: inline-nested properties: a: id: inline-nested-nested `, 'foobar.yaml', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter nested schema inline-top > inline-top", "enter nested schema inline-top > inline-nested", "enter nested schema inline-nested > inline-nested-nested", "leave nested schema inline-nested > inline-nested-nested", "leave nested schema inline-top > inline-nested", "leave nested schema inline-top > inline-top", "enter nested schema inline-top > inline-nested-2", "enter nested schema inline-nested-2 > inline-nested-nested-2", "leave nested schema inline-nested-2 > inline-nested-nested-2", "leave nested schema inline-top > inline-nested-2", ] `); }); it('should correctly visit refs', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { ref(node, _, { node: target }) { calls.push(`enter $ref ${node.$ref} with target ${target?.name}`); }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: path-param get: operationId: get parameters: - $ref: '#/components/parameters/shared_b' put: operationId: put parameters: - $ref: '#/components/parameters/shared_a' /dog: id: dog post: operationId: post schema: example: $ref: 123 parameters: - $ref: '#/components/parameters/shared_a' components: parameters: shared_a: name: shared-a shared_b: name: shared-b schema: $ref: '#/components/parameters/shared_b' `, 'foobar.yaml', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter $ref #/components/parameters/shared_b with target shared-b", "enter $ref #/components/parameters/shared_b with target shared-b", "enter $ref #/components/parameters/shared_a with target shared-a", "enter $ref #/components/parameters/shared_a with target shared-a", ] `); }); it('should correctly visit refs', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { NamedSchemas: { Schema(node, { key }) { calls.push(`enter schema ${key}: ${node.type}`); }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 components: schemas: a: type: string b: type: number `, 'foobar.yaml', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter schema a: string", "enter schema b: number", ] `); }); it('should correctly visit any visitor', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { ref: { enter(ref: any) { calls.push(`enter ref ${ref.$ref}`); }, leave(ref) { calls.push(`leave ref ${ref.$ref}`); }, }, any: { enter(_node: any, { type }) { calls.push(`enter ${type.name}`); }, leave(_node, { type }) { calls.push(`leave ${type.name}`); }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: id: pet parameters: - name: path-param get: operationId: get parameters: - $ref: '#/components/parameters/shared_a' - name: get_b - name: get_c components: parameters: shared_a: name: shared-a schemas: a: type: object `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter DefinitionRoot", "enter PathMap", "enter PathItem", "enter Parameter_List", "enter Parameter", "leave Parameter", "leave Parameter_List", "enter Operation", "enter Parameter_List", "enter ref #/components/parameters/shared_a", "enter Parameter", "leave Parameter", "leave ref #/components/parameters/shared_a", "enter Parameter", "leave Parameter", "enter Parameter", "leave Parameter", "leave Parameter_List", "leave Operation", "leave PathItem", "leave PathMap", "enter Components", "enter NamedParameters", "leave NamedParameters", "enter NamedSchemas", "enter Schema", "leave Schema", "leave NamedSchemas", "leave Components", "leave DefinitionRoot", ] `); }); }); describe('context.report', () => { it('should report errors correctly', async () => { const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Parameter: { enter: jest.fn((param, ctx) => { if (param.name.indexOf('_') > -1) { ctx.report({ message: `Parameter name shouldn't contain '_: ${param.name}`, }); } }), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 info: contact: {} license: {} paths: /pet: parameters: - name: path-param get: operationId: get parameters: - name: get_a - name: get_b post: operationId: post parameters: - $ref: '#/components/parameters/shared_a' components: parameters: shared_a: name: shared_a `, 'foobar.yaml', ); const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(results).toHaveLength(3); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` Array [ Object { "location": Array [ Object { "pointer": "#/paths/~1pet/get/parameters/0", "reportOnKey": false, "source": "foobar.yaml", }, ], "message": "Parameter name shouldn't contain '_: get_a", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, Object { "location": Array [ Object { "pointer": "#/paths/~1pet/get/parameters/1", "reportOnKey": false, "source": "foobar.yaml", }, ], "message": "Parameter name shouldn't contain '_: get_b", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, Object { "location": Array [ Object { "pointer": "#/components/parameters/shared_a", "reportOnKey": false, "source": "foobar.yaml", }, ], "message": "Parameter name shouldn't contain '_: shared_a", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, ] `); }); it('should report errors correctly', async () => { const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Parameter: { enter: jest.fn((param, ctx) => { if (param.name.indexOf('_') > -1) { ctx.report({ message: `Parameter name shouldn't contain '_: ${param.name}`, }); } }), }, }; }), }; const cwd = path.join(__dirname, 'fixtures/refs'); const externalRefResolver = new BaseResolver(); const document = (await externalRefResolver.resolveDocument( null, `${cwd}/openapi-with-external-refs.yaml`, )) as Document; if (document === null) { throw 'Should never happen'; } const results = await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(results).toHaveLength(4); expect(replaceSourceWithRef(results, cwd)).toMatchInlineSnapshot(` Array [ Object { "location": Array [ Object { "pointer": "#/components/parameters/path-param", "reportOnKey": false, "source": "openapi-with-external-refs.yaml", }, ], "message": "Parameter name shouldn't contain '_: path_param", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, Object { "location": Array [ Object { "pointer": "#/components/parameters/param-a", "reportOnKey": false, "source": "openapi-with-external-refs.yaml", }, ], "message": "Parameter name shouldn't contain '_: param_a", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, Object { "location": Array [ Object { "pointer": "#/", "reportOnKey": false, "source": "param-c.yaml", }, ], "message": "Parameter name shouldn't contain '_: param_c", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, Object { "location": Array [ Object { "pointer": "#/", "reportOnKey": false, "source": "param-b.yaml", }, ], "message": "Parameter name shouldn't contain '_: param_b", "ruleId": "test/test", "severity": "error", "suggest": Array [], }, ] `); }); }); describe('context.resolve', () => { it('should resolve refs correctly', async () => { const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { Schema: jest.fn((schema, { resolve }) => { if (schema.properties) { expect(schema.properties.a.$ref).toBeDefined(); const { location, node } = resolve(schema.properties.a); expect(node).toMatchInlineSnapshot(` Object { "type": "string", } `); expect(location?.pointer).toEqual('#/components/schemas/b'); expect(location?.source).toStrictEqual(document.source); } }), }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 info: contact: {} license: {} paths: {} components: schemas: b: type: string a: type: object properties: a: $ref: '#/components/schemas/b' `, 'foobar.yaml', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); }); }); describe('type extensions', () => { each([ ['3.0.0', 'oas3_0'], ['3.1.0', 'oas3_1'], ]).it('should correctly visit OpenAPI %s extended types', async (openapi, oas) => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { test: jest.fn(() => { return { any: { enter(_node: any, { type }) { calls.push(`enter ${type.name}`); }, leave(_node, { type }) { calls.push(`leave ${type.name}`); }, }, XWebHooks: { enter(hook: any) { calls.push(`enter hook ${hook.name}`); }, leave(hook) { calls.push(`leave hook ${hook.name}`); }, }, }; }), }; const document = parseYamlToDocument( outdent` openapi: ${openapi} x-webhooks: name: test parameters: - name: a `, 'foobar.yaml', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet, { typeExtension: { oas3(types, version) { expect(version).toEqual(oas); return { ...types, XWebHooks: { properties: { parameters: listOf('Parameter'), }, }, DefinitionRoot: { ...types.DefinitionRoot, properties: { ...types.DefinitionRoot.properties, 'x-webhooks': 'XWebHooks', }, }, }; }, }, }), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter DefinitionRoot", "enter XWebHooks", "enter hook test", "enter Parameter_List", "enter Parameter", "leave Parameter", "leave Parameter_List", "leave hook test", "leave XWebHooks", "leave DefinitionRoot", ] `); }); }); describe('ignoreNextRules', () => { it('should correctly skip top level', async () => { const calls: string[] = []; const testRuleSet: Oas3RuleSet = { skip: jest.fn(() => { return { Operation: { enter: jest.fn((op, ctx) => { if (op.operationId === 'get') { ctx.ignoreNextVisitorsOnNode(); calls.push(`enter and skip operation ${op.operationId}`); } else { calls.push(`enter and not skip operation ${op.operationId}`); } }), leave: jest.fn((op) => { if (op.operationId === 'get') { calls.push(`leave skipped operation ${op.operationId}`); } else { calls.push(`leave not skipped operation ${op.operationId}`); } }), }, }; }), test: jest.fn(() => { return { Operation: { enter: jest.fn((op) => calls.push(`enter operation ${op.operationId}`)), leave: jest.fn((op) => calls.push(`leave operation ${op.operationId}`)), }, }; }), }; const document = parseYamlToDocument( outdent` openapi: 3.0.0 paths: /pet: get: operationId: get put: operationId: put `, '', ); await lintDocument({ externalRefResolver: new BaseResolver(), document, config: makeConfigForRuleset(testRuleSet), }); expect(calls).toMatchInlineSnapshot(` Array [ "enter and skip operation get", "leave skipped operation get", "enter and not skip operation put", "enter operation put", "leave not skipped operation put", "leave operation put", ] `); }); });