ibm-openapi-validator
Version:
Configurable and extensible validator/linter for OpenAPI documents
253 lines (215 loc) • 9.51 kB
JavaScript
/**
* Copyright 2017 - 2023 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { getCapturedText, testValidator } = require('../../test-utils');
describe('Expected output tests', function () {
let consoleSpy;
const originalWarn = console.warn;
const originalError = console.error;
const originalInfo = console.info;
beforeEach(() => {
consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
console.warn = console.log;
console.error = console.log;
console.info = console.log;
});
afterEach(() => {
consoleSpy.mockRestore();
console.warn = originalWarn;
console.error = originalError;
console.info = originalInfo;
});
describe('OpenAPI 3', function () {
it.each(['oas3', 'oas31'])(
'should not produce any errors or warnings from a clean file',
async function (oasVersion) {
const filename = `./test/cli-validator/mock-files/${oasVersion}/clean.yml`;
const exitCode = await testValidator([filename]);
expect(exitCode).toEqual(0);
const capturedText = getCapturedText(consoleSpy.mock.calls);
const allOutput = capturedText.join('');
expect(allOutput).toContain(`${filename} passed the validator`);
// Confirm that Markdown reports are not generated by default.
expect(allOutput).not.toMatch(
'Successfully wrote Markdown report to file'
);
}
);
it.each(['oas3', 'oas31'])(
'should produce errors and warnings from err-and-warn.yaml',
async function (oasVersion) {
const filename = `./test/cli-validator/mock-files/${oasVersion}/err-and-warn.yaml`;
const exitCode = await testValidator([filename]);
const capturedText = getCapturedText(consoleSpy.mock.calls);
// originalError('Captured text:\n', capturedText);
expect(exitCode).toEqual(1);
const whichProblems = [];
capturedText.forEach(function (line) {
if (line.includes('errors')) {
whichProblems.push('errors');
}
if (line.includes('warnings')) {
whichProblems.push('warnings');
}
});
expect(whichProblems[0]).toEqual('errors');
expect(whichProblems[1]).toEqual('warnings');
}
);
it.each(['oas3', 'oas31'])(
'should print the correct line numbers for each error/warning',
async function (oasVersion) {
const filename = `./test/cli-validator/mock-files/${oasVersion}/err-and-warn.yaml`;
const exitCode = await testValidator([filename]);
expect(exitCode).toEqual(1);
const capturedText = getCapturedText(consoleSpy.mock.calls);
// This can be uncommented to display the output when adjustments to
// the expect statements below are needed.
// let textOutput = '';
// capturedText.forEach((elem, index) => {
// textOutput += `[${index}]: ${elem}\n`;
// });
// originalError(textOutput);
// errors
const errorStart = 3;
expect(capturedText[errorStart + 5].match(/\S+/g)[2]).toEqual('52');
expect(capturedText[errorStart + 10].match(/\S+/g)[2]).toEqual('96');
expect(capturedText[errorStart + 15].match(/\S+/g)[2]).toEqual('103');
// Specifically verify that the no-$ref-siblings error occurred.
// We do this because this rule is inherited from Spectral's oas ruleset,
// but we modify the rule definition in ibmoas.js so that it is run
// against both OpenAPI 3.0 and 3.1 documents.
expect(capturedText[errorStart + 17].split(':')[1].trim()).toEqual(
'$ref must not be placed next to any other properties'
);
expect(capturedText[errorStart + 18].split(':')[1].trim()).toEqual(
'no-$ref-siblings'
);
expect(capturedText[errorStart + 19].split(':')[1].trim()).toEqual(
'components.schemas.Pet.properties.category.description'
);
expect(capturedText[errorStart + 20].match(/\S+/g)[2]).toEqual('184');
// warnings
const warningStart = 25;
expect(capturedText[warningStart + 5].match(/\S+/g)[2]).toEqual('22');
expect(capturedText[warningStart + 10].match(/\S+/g)[2]).toEqual('24');
expect(capturedText[warningStart + 15].match(/\S+/g)[2]).toEqual('40');
expect(capturedText[warningStart + 20].match(/\S+/g)[2]).toEqual('41');
expect(capturedText[warningStart + 25].match(/\S+/g)[2]).toEqual('52');
expect(capturedText[warningStart + 30].match(/\S+/g)[2]).toEqual('56');
expect(capturedText[warningStart + 35].match(/\S+/g)[2]).toEqual('57');
expect(capturedText[warningStart + 40].match(/\S+/g)[2]).toEqual('59');
expect(capturedText[warningStart + 45].match(/\S+/g)[2]).toEqual('61');
expect(capturedText[warningStart + 50].match(/\S+/g)[2]).toEqual('96');
// Skip a few, then verify the last one.
expect(capturedText[warningStart + 145].match(/\S+/g)[2]).toEqual(
'210'
);
}
);
it('should catch problems in a multi-file spec from an outside directory', async function () {
const exitCode = await testValidator([
'./test/cli-validator/mock-files/multi-file-spec/main.yaml',
]);
expect(exitCode).toEqual(1);
const capturedText = getCapturedText(consoleSpy.mock.calls);
const allOutput = capturedText.join('');
expect(allOutput).toContain('errors');
expect(allOutput).toContain('must have required property "info"');
expect(allOutput).toContain('warnings');
expect(allOutput).toContain('Operation must have "operationId"');
expect(allOutput).toContain(
'Operation "description" must be present and non-empty string'
);
});
it.each(['oas3', 'oas31'])(
'should handle an array of file names',
async function (oasVersion) {
const args = [
`./test/cli-validator/mock-files/${oasVersion}/err-and-warn.yaml`,
'notAFile.json',
`./test/cli-validator/mock-files/${oasVersion}/clean.yml`,
];
const exitCode = await testValidator(args);
expect(exitCode).toEqual(1);
const capturedText = getCapturedText(consoleSpy.mock.calls);
const allOutput = capturedText.join('');
expect(
allOutput.includes('[WARN] Skipping non-existent file: notAFile.json')
).toEqual(true);
expect(
allOutput.includes(
`Validation Results for ./test/cli-validator/mock-files/${oasVersion}/err-and-warn.yaml:`
)
).toEqual(true);
expect(
allOutput.includes(
`Validation Results for ./test/cli-validator/mock-files/${oasVersion}/clean.yml:`
)
).toEqual(true);
}
);
it('should not produce any errors or warnings from mock-files/oas3/clean-with-tabs.yml', async function () {
const exitCode = await testValidator([
'./test/cli-validator/mock-files/oas3/clean-with-tabs.yml',
]);
expect(exitCode).toEqual(0);
const capturedText = getCapturedText(consoleSpy.mock.calls);
// originalError('Captured text:\n', capturedText);
expect(capturedText.length).toEqual(4);
expect(capturedText[3].trim()).toEqual(
'./test/cli-validator/mock-files/oas3/clean-with-tabs.yml passed the validator'
);
});
it.each(['-j', '--json'])(
'should include the associated rule with each error and warning in JSON output',
async function (option) {
const exitCode = await testValidator([
option,
'./test/cli-validator/mock-files/oas3/err-and-warn.yaml',
]);
expect(exitCode).toEqual(1);
const capturedText = getCapturedText(consoleSpy.mock.calls);
const jsonOutput = JSON.parse(capturedText);
const msgsByType = Object.assign(
{},
jsonOutput.errors,
jsonOutput.warnings,
jsonOutput.infos,
jsonOutput.hints
);
const allMessages = Object.entries(msgsByType).reduce(
(newObj, val) => newObj.concat(val[1]),
[]
);
allMessages.forEach(msg => expect(msg).toHaveProperty('rule'));
}
);
it('should return exit code of 0 if there are only warnings', async function () {
const exitCode = await testValidator([
'./test/cli-validator/mock-files/oas3/just-warn.yml',
]);
expect(exitCode).toEqual(0);
const capturedText = getCapturedText(consoleSpy.mock.calls);
const allOutput = capturedText.join('');
expect(allOutput.includes('warnings')).toEqual(true);
});
it.each(['oas3', 'oas31'])(
'should include the path to the component (if it exists) when in json mode',
async function (oasVersion) {
const exitCode = await testValidator([
'-j',
`./test/cli-validator/mock-files/${oasVersion}/component-path-example.yaml`,
]);
expect(exitCode).toEqual(0);
const capturedText = getCapturedText(consoleSpy.mock.calls);
const jsonOutput = JSON.parse(capturedText);
const warningToCheck = jsonOutput.warning.results[0];
expect(warningToCheck.rule).toEqual('ibm-prefer-token-pagination');
expect(warningToCheck.path.join('.')).toBe('paths./letters.get');
expect(warningToCheck.line).toEqual(20);
}
);
});
});