@redocly/openapi-core
Version:
See https://github.com/Redocly/openapi-cli
1,486 lines (1,378 loc) • 40.1 kB
text/typescript
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",
]
`);
});
});