UNPKG

ibm-openapi-validator

Version:

Configurable and extensible validator/linter for OpenAPI documents

405 lines (344 loc) 15 kB
/** * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const { existsSync, unlinkSync } = require('fs'); const { extractValuesFromTable, getCapturedText, getCapturedTextWithColor, stripAnsi, testValidator, } = require('../../test-utils'); const { getCopyrightString, readYaml, validateSchema, } = require('../../../src/cli-validator/utils'); describe('cli tool - test option handling', function () { let consoleSpy; const originalWarn = console.warn; const originalError = console.error; const originalInfo = console.info; const copyrightString = getCopyrightString(); 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; }); it('should colorize output by default @skip-ci', async function () { await testValidator([ './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedTextWithColor(consoleSpy.mock.calls); // originalError('Captured text:\n', capturedText); capturedText.forEach(function (line) { if (line && line !== copyrightString) { expect(line).not.toEqual(stripAnsi(line)); } }); }); it.each(['-n', '--no-colors'])( 'should not colorize output when -n/--no-colors option is specified', async function (option) { await testValidator([ option, './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); capturedText.forEach(function (line) { expect(line).toEqual(stripAnsi(line)); }); } ); it.each(['-e', '--errors-only'])( 'should print only errors when the -e/--errors-only option is specified', async function (option) { await testValidator([ option, './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); // originalError(`Captured text: ${JSON.stringify(capturedText, null, 2)}`); capturedText.forEach(function (line) { expect(line.includes('warnings')).toEqual(false); }); } ); it.each(['-s', '--summary-only'])( 'should print only the summary when -s/--summary-only option is specified', async function (option) { await testValidator([ option, './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); 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); let summaryReported = false; capturedText.forEach(function (line) { if (line.includes('Summary:')) { summaryReported = true; } }); // .match(/\S+/g) returns an array of all non-whitespace strings // example output would be [ '33%', ':', 'operationIds', 'must', 'be', 'unique' ] expect(summaryReported).toEqual(true); const sumSection = capturedText.findIndex(x => x.includes('Summary:')); expect(sumSection).toBe(3); // totals expect(capturedText[sumSection + 2].match(/\S+/g)[5]).toEqual('4'); expect(capturedText[sumSection + 3].match(/\S+/g)[5]).toEqual('29'); // errors const errorSection = 8; expect(capturedText[errorSection + 1].match(/\S+/g)[0]).toEqual('1'); expect(capturedText[errorSection + 1].match(/\S+/g)[1]).toEqual('(25%)'); expect(capturedText[errorSection + 2].match(/\S+/g)[0]).toEqual('2'); expect(capturedText[errorSection + 2].match(/\S+/g)[1]).toEqual('(50%)'); // warnings const warningSection = 13; expect(capturedText[warningSection + 1].match(/\S+/g)[0]).toEqual('2'); expect(capturedText[warningSection + 1].match(/\S+/g)[1]).toEqual('(7%)'); expect(capturedText[warningSection + 2].match(/\S+/g)[0]).toEqual('2'); expect(capturedText[warningSection + 2].match(/\S+/g)[1]).toEqual('(7%)'); expect(capturedText[warningSection + 3].match(/\S+/g)[0]).toEqual('5'); expect(capturedText[warningSection + 3].match(/\S+/g)[1]).toEqual( '(17%)' ); expect(capturedText[warningSection + 4].match(/\S+/g)[0]).toEqual('1'); expect(capturedText[warningSection + 4].match(/\S+/g)[1]).toEqual('(3%)'); expect(capturedText[warningSection + 5].match(/\S+/g)[0]).toEqual('2'); expect(capturedText[warningSection + 5].match(/\S+/g)[1]).toEqual('(7%)'); expect(capturedText[warningSection + 6].match(/\S+/g)[0]).toEqual('1'); expect(capturedText[warningSection + 6].match(/\S+/g)[1]).toEqual('(3%)'); expect(capturedText[warningSection + 7].match(/\S+/g)[0]).toEqual('1'); expect(capturedText[warningSection + 7].match(/\S+/g)[1]).toEqual('(3%)'); expect(capturedText[warningSection + 8].match(/\S+/g)[0]).toEqual('2'); expect(capturedText[warningSection + 8].match(/\S+/g)[1]).toEqual('(7%)'); expect(capturedText[warningSection + 9].match(/\S+/g)[0]).toEqual('4'); expect(capturedText[warningSection + 9].match(/\S+/g)[1]).toEqual( '(14%)' ); expect(capturedText[warningSection + 10].match(/\S+/g)[0]).toEqual('4'); expect(capturedText[warningSection + 10].match(/\S+/g)[1]).toEqual( '(14%)' ); expect(capturedText[warningSection + 11].match(/\S+/g)[0]).toEqual('4'); expect(capturedText[warningSection + 11].match(/\S+/g)[1]).toEqual( '(14%)' ); expect(capturedText[warningSection + 12].match(/\S+/g)[0]).toEqual('1'); expect(capturedText[warningSection + 12].match(/\S+/g)[1]).toEqual( '(3%)' ); } ); it.each(['-j', '--json'])( 'should print json output when -j/--json option is specified', async function (option) { await testValidator([ option, './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); // originalError(`Captured text: ${capturedText}`); // capturedText should be JSON object. convert to json and check fields const outputObject = JSON.parse(capturedText); // Representative sample of the rule violations. expect(outputObject.error.results[0]['line']).toEqual(52); expect(outputObject.error.results[0]['message']).toEqual( 'Every operation must have unique "operationId".' ); // Representative sample of the impact score information. expect( outputObject.impactScore.categorizedSummary.usability ).toBeTruthy(); expect(outputObject.impactScore.categorizedSummary.security).toBeTruthy(); expect( outputObject.impactScore.categorizedSummary.robustness ).toBeTruthy(); expect( outputObject.impactScore.categorizedSummary.evolution ).toBeTruthy(); expect(outputObject.impactScore.categorizedSummary.overall).toBeTruthy(); expect(outputObject.impactScore.scoringData.length).toBeGreaterThan(0); // json output should comply with written schema const outputSchemaPath = __dirname + '/../../../src/schemas/results-object.yaml'; const outputSchema = await readYaml(outputSchemaPath); const results = validateSchema(outputObject, outputSchema); expect(results).toHaveLength(0); } ); it('should print only errors as json output when -j and -e options are specified together', async function () { await testValidator([ '-j', '-e', './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); // capturedText should be JSON object. convert to json and check fields const outputObject = JSON.parse(capturedText); ['warning', 'info', 'hint'].forEach(severity => { expect(outputObject[severity].results.length).toEqual(0); expect(outputObject[severity].summary.total).toEqual(0); }); }); it('should not include results in json output when -j and -s options are specified together', async function () { await testValidator([ '-j', '-s', './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); // capturedText should be JSON object. convert to json and check fields const outputObject = JSON.parse(capturedText); ['error', 'warning', 'info', 'hint'].forEach(severity => { expect(outputObject[severity].results.length).toEqual(0); }); expect(outputObject.error.summary.total).toBeGreaterThan(0); expect(outputObject.warning.summary.total).toBeGreaterThan(0); }); it.each(['-q', '--impact-score'])( 'should print two scoring tables when the -q/--impact-score option is specified', async function (option) { await testValidator([ option, './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); // Most of the data is more fragile, but this proves we printed // the first table and the correct values in the first column. const firstTable = extractValuesFromTable(capturedText.at(-3)); const firstColumn = 0; expect(firstTable.at(1)).toEqual(['category', 'max score']); expect(firstTable.at(3).at(firstColumn)).toBe('usability'); expect(firstTable.at(4).at(firstColumn)).toBe('security'); expect(firstTable.at(5).at(firstColumn)).toBe('robustness'); expect(firstTable.at(6).at(firstColumn)).toBe('evolution'); expect(firstTable.at(7).at(firstColumn)).toBe('overall (mean)'); const secondTable = extractValuesFromTable(capturedText.at(-2)); // The rest of the data is more fragile, but this proves we printed the second table. expect(secondTable.at(1)).toEqual([ 'rule', 'count', 'func', 'usability impact', 'security impact', 'robustness impact', 'evolution impact', 'rule total', ]); } ); it('should only include errors in scoring tables when -q and -e options are specified together', async function () { await testValidator([ '-q', '-e', './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); const secondTable = extractValuesFromTable(capturedText.at(-2)); const firstColumn = 0; expect(secondTable.length).toBe(7); expect(secondTable.at(1).at(firstColumn)).toBe('rule'); expect(secondTable.at(3).at(firstColumn)).toBe( 'operation-operationId-unique' ); expect(secondTable.at(4).at(firstColumn)).toBe('ibm-no-array-responses'); expect(secondTable.at(5).at(firstColumn)).toBe('no-$ref-siblings'); }); it('should only include summary scoring table when -q and -s options are specified together', async function () { await testValidator([ '-q', '-s', './test/cli-validator/mock-files/oas3/err-and-warn.yaml', ]); const capturedText = getCapturedText(consoleSpy.mock.calls); const firstAndOnlyTable = extractValuesFromTable(capturedText.at(-2)); const firstColumn = 0; expect(firstAndOnlyTable.at(1)).toEqual(['category', 'max score']); expect(firstAndOnlyTable.at(3).at(firstColumn)).toBe('usability'); expect(firstAndOnlyTable.at(4).at(firstColumn)).toBe('security'); expect(firstAndOnlyTable.at(5).at(firstColumn)).toBe('robustness'); expect(firstAndOnlyTable.at(6).at(firstColumn)).toBe('evolution'); expect(firstAndOnlyTable.at(7).at(firstColumn)).toBe('overall (mean)'); }); it('should print out the version strings with the --version option', async function () { await testValidator(['--version']); const capturedText = getCapturedText(consoleSpy.mock.calls); expect(capturedText).toHaveLength(1); expect(capturedText[0]).toMatch(/validator:.*ruleset:.*(default)/); }); describe('test unknown option handling', function () { it('should return an error and help text when there is an unknown option', async function () { let caughtException = false; let exception; try { await testValidator(['--unknown-option']); } catch (err) { // originalError(`caught exception: ${err}`); caughtException = true; exception = err; } const capturedText = getCapturedText(consoleSpy.mock.calls); // originalError(`Captured text: ${JSON.stringify(capturedText, null, 2)}`); expect(caughtException).toBe(true); expect(exception).toBe(2); expect(capturedText[0]).toMatch( /error: unknown option '--unknown-option'/ ); expect(capturedText[2]).toMatch( /IBM OpenAPI Validator.*Copyright IBM Corporation/ ); expect(capturedText[3]).toMatch(/Usage: lint-openapi/); }); }); describe('test markdown report option handling', function () { // Use this API for all of the tests so the expected name // of the created file stays consistent. const fileToTest = './test/cli-validator/mock-files/oas3/clean.yml'; const expectedFile = 'clean-validator-report.md'; beforeEach(function () { // Make sure there is no markdown report file before the test is executed. expect(existsSync(expectedFile)).toBe(false); }); afterEach(function () { // Delete the markdown report we've just created. unlinkSync(expectedFile); }); it.each(['-m', '--markdown-report'])( 'should write a markdown file when the -m/--markdown-report option is specified', async function (option) { await testValidator([option, fileToTest]); const capturedText = getCapturedText(consoleSpy.mock.calls); expect(capturedText.at(-1)).toMatch( new RegExp( 'Successfully wrote Markdown report to file: .*/openapi-validator/packages/validator/clean-validator-report.md' ) ); expect(existsSync(expectedFile)).toBe(true); } ); it('should print file but not confirmation message when output is json', async function () { await testValidator(['-m', '-j', fileToTest]); const capturedText = getCapturedText(consoleSpy.mock.calls); expect(existsSync(expectedFile)).toBe(true); expect(capturedText.join('')).not.toMatch( 'Successfully wrote Markdown report to file' ); }); }); });