From 9c8e78c28d81c98a74e9167de0f209e002f6904f Mon Sep 17 00:00:00 2001 From: Dan Hudlow Date: Thu, 10 Apr 2025 11:23:14 -0500 Subject: [PATCH] test: proof of concept for streamlined unit test Signed-off-by: Dan Hudlow --- .../test/rules/array-attributes.test.js | 576 +++++++----------- packages/ruleset/test/test-utils/index.js | 2 + .../ruleset/test/test-utils/unit-test-rule.js | 42 ++ 3 files changed, 255 insertions(+), 365 deletions(-) create mode 100644 packages/ruleset/test/test-utils/unit-test-rule.js diff --git a/packages/ruleset/test/rules/array-attributes.test.js b/packages/ruleset/test/rules/array-attributes.test.js index c21728470..ef280e934 100644 --- a/packages/ruleset/test/rules/array-attributes.test.js +++ b/packages/ruleset/test/rules/array-attributes.test.js @@ -5,9 +5,7 @@ const { arrayAttributes } = require('../../src/rules'); const { - makeCopy, - rootDocument, - testRule, + unitTestRule, severityCodes, } = require('../test-utils'); @@ -17,20 +15,16 @@ const expectedSeverity = severityCodes.warning; const expectedMsgMin = `Array schemas should define a numeric 'minItems' field`; const expectedMsgMax = `Array schemas should define a numeric 'maxItems' field`; const expectedMsgItems = `Array schemas must specify the 'items' field`; +const expectedMsgMinMax = `'minItems' cannot be greater than 'maxItems'`; +const expectedMsgEnumArray = `Array schemas should not define an 'enum' field`; +const expectedMsgMinNonArray = `'minItems' should not be defined for a non-array schema`; +const expectedMsgMaxNonArray = `'maxItems' should not be defined for a non-array schema`; +const expectedMsgItemsNonArray = `'items' should not be defined for a non-array schema`; describe(`Spectral rule: ${ruleId}`, () => { describe('Should not yield errors', () => { - it('Clean spec', async () => { - const results = await testRule(ruleId, rule, rootDocument); - expect(results).toHaveLength(0); - }); - it('Array property with min/maxItems in allOf', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { + const input = { description: 'Drink response schema', properties: { main_prop: { @@ -52,16 +46,13 @@ describe(`Spectral rule: ${ruleId}`, () => { }, }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(0); }); it('Schema property uses nested allOf/oneOf with mix/maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { + const input = { description: 'Drink response schema', properties: { main_prop: { @@ -93,14 +84,13 @@ describe(`Spectral rule: ${ruleId}`, () => { }, }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(0); }); it('minItems and maxItems defined', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.components.schemas.Car.properties['wheel_count'] = { + const input = { type: 'array', minItems: 3, maxItems: 4, @@ -109,14 +99,13 @@ describe(`Spectral rule: ${ruleId}`, () => { }, }; - const results = await testRule(ruleId, rule, rootDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(0); }); it('minItems <= maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.components.schemas.Car.properties['wheel_count'] = { + const input = { type: 'array', minItems: 3, maxItems: 3, @@ -125,52 +114,38 @@ describe(`Spectral rule: ${ruleId}`, () => { }, }; - const results = await testRule(ruleId, rule, rootDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(0); }); }); describe('Should yield errors', () => { it('Array property with no min/maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - // Make a copy of Movie named Movie2, and make Movie2 the response schema - // for the create operation only. - const movie2 = makeCopy(testDocument.components.schemas['Movie']); - movie2.properties['production_crew'] = { + const input = { type: 'array', items: { type: 'string', }, }; - testDocument.components.schemas['Movie2'] = movie2; - testDocument.paths['/v1/movies'].post.responses['201'].content[ - 'application/json' - ].schema = { - $ref: '#/components/schemas/Movie2', - }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(2); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMin); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.production_crew' - ); + expect(results[0].path).toStrictEqual([]); expect(results[1].code).toBe(ruleId); expect(results[1].message).toBe(expectedMsgMax); expect(results[1].severity).toBe(expectedSeverity); - expect(results[1].path.join('.')).toBe( - 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.production_crew' - ); + expect(results[1].path).toStrictEqual([]); }); it('enum defined for array schema', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.components.schemas.Car.properties['wheel_options'] = { + const input = { type: 'array', maxItems: 3, minItems: 1, @@ -180,29 +155,18 @@ describe(`Spectral rule: ${ruleId}`, () => { enum: [['circle'], ['circle', 'square', 'triangle']], }; - const results = await testRule(ruleId, rule, testDocument); - expect(results).toHaveLength(3); - const expectedPaths = [ - 'paths./v1/cars.post.responses.201.content.application/json.schema.properties.wheel_options.enum', - 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_options.enum', - 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_options.enum', - ]; - for (let i = 0; i < results.length; i++) { - expect(results[i].code).toBe(ruleId); - expect(results[i].message).toBe( - `Array schemas should not define an 'enum' field` - ); - expect(results[i].severity).toBe(expectedSeverity); - expect(results[i].path.join('.')).toBe(expectedPaths[i]); - } + const results = await unitTestRule(ruleId, rule, input); + + expect(results).toHaveLength(1); + + expect(results[0].code).toBe(ruleId); + expect(results[0].message).toBe(expectedMsgEnumArray); + expect(results[0].severity).toBe(expectedSeverity); + expect(results[0].path).toStrictEqual(['enum']); }); it('Inline response schema array property with only minItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/movies'].post.responses['400'].content[ - 'application/json' - ].schema = { + const input = { description: 'An error response.', type: 'object', properties: { @@ -212,7 +176,7 @@ describe(`Spectral rule: ${ruleId}`, () => { description: 'The array of error entries associated with the error response', items: { - $ref: '#/components/schemas/Error', + type: 'object', }, }, trace: { @@ -223,336 +187,249 @@ describe(`Spectral rule: ${ruleId}`, () => { }, }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(1); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMax); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/movies.post.responses.400.content.application/json.schema.properties.errors' - ); + expect(results[0].path).toStrictEqual(['properties', 'errors']); }); it('Schema property uses allOf without min/maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { - description: 'a description', - properties: { - main_prop: { - allOf: [ - { - type: 'array', - items: { - type: 'string', - }, - }, - { - type: 'array', - }, - ], + const input = { + allOf: [ + { + type: 'array', + items: { + type: 'string', + }, }, - }, + { + type: 'array', + }, + ], }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(2); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMin); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[0].path).toStrictEqual([]); expect(results[1].code).toBe(ruleId); expect(results[1].message).toBe(expectedMsgMax); expect(results[1].severity).toBe(expectedSeverity); - expect(results[1].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[1].path).toStrictEqual([]); }); it('Schema property uses oneOf without min/maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { - description: 'a description', - properties: { - main_prop: { - oneOf: [ - { - type: 'array', - items: { - type: 'string', - }, - minItems: 0, - }, - { - type: 'array', - items: { - type: 'string', - }, - maxItems: 5, - }, - ], + const input = { + oneOf: [ + { + type: 'array', + items: { + type: 'string', + }, + minItems: 0, }, - }, + { + type: 'array', + items: { + type: 'string', + }, + maxItems: 5, + }, + ], }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(2); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMin); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[0].path).toStrictEqual([]); expect(results[1].code).toBe(ruleId); expect(results[1].message).toBe(expectedMsgMax); expect(results[1].severity).toBe(expectedSeverity); - expect(results[1].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[1].path).toStrictEqual([]); }); it('Schema property uses anyOf without min/maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { - description: 'a description', - properties: { - main_prop: { - anyOf: [ - { - type: 'array', - items: { - type: 'string', - }, - minItems: 0, - }, - { - type: 'array', - items: { - type: 'string', - }, - maxItems: 5, - }, - ], + const input = { + anyOf: [ + { + type: 'array', + items: { + type: 'string', + }, + minItems: 0, }, - }, + { + type: 'array', + items: { + type: 'string', + }, + maxItems: 5, + }, + ], }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(2); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMin); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[0].path).toStrictEqual([]); expect(results[1].code).toBe(ruleId); expect(results[1].message).toBe(expectedMsgMax); expect(results[1].severity).toBe(expectedSeverity); - expect(results[1].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[1].path).toStrictEqual([]); }); it('Schema property uses nested allOf/oneOf without maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { - description: 'a description', - properties: { - main_prop: { - oneOf: [ + const input = { + oneOf: [ + { + allOf: [ { - allOf: [ - { - type: 'array', - minItems: 0, - maxItems: 4, - }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], + type: 'array', + minItems: 0, + maxItems: 4, }, { type: 'array', - minItems: 0, items: { type: 'string', }, }, ], }, - }, + { + type: 'array', + minItems: 0, + items: { + type: 'string', + }, + }, + ], }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(1); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMax); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[0].path).toStrictEqual([]); }); it('Schema property uses nested allOf/anyOf without minItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/drinks'].post.responses['201'].content[ - 'application/json' - ].schema = { - description: 'a description', - properties: { - main_prop: { - anyOf: [ + const input = { + anyOf: [ + { + allOf: [ { - allOf: [ - { - type: 'array', - minItems: 0, - maxItems: 4, - }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], + type: 'array', + minItems: 0, + maxItems: 4, }, { type: 'array', - maxItems: 1600, items: { type: 'string', }, }, ], }, - }, + { + type: 'array', + maxItems: 1600, + items: { + type: 'string', + }, + }, + ], }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(1); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgMin); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/drinks.post.responses.201.content.application/json.schema.properties.main_prop' - ); + expect(results[0].path).toStrictEqual([]); }); - it('allOf element without items property', async () => { - const testDocument = makeCopy(rootDocument); - delete testDocument.components.schemas.DrinkCollection.allOf[1].properties - .drinks.items; + it('Schema without items property', async () => { + const input = { + type: 'array', + minItems: 0, + maxItems: 50, + } + + const results = await unitTestRule(ruleId, rule, input); - const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgItems); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/drinks.get.responses.200.content.application/json.schema.allOf.1.properties.drinks' - ); + expect(results[0].path).toStrictEqual([]); }); - it('Response schema without items property', async () => { - const testDocument = makeCopy(rootDocument); - testDocument.components.schemas.MovieCollection = { - type: 'array', - }; + it('allOf element without items property', async () => { + const input = { + allOf: [ + { + type: 'array', + minItems: 0, + maxItems: 50, + }, + ], + } + + const results = await unitTestRule(ruleId, rule, input); - const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgItems); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/movies.get.responses.200.content.application/json.schema' - ); + expect(results[0].path).toStrictEqual([]); }); - it('Request schema without items property', async () => { - const testDocument = makeCopy(rootDocument); - testDocument.paths['/v1/movies'].post.requestBody.content[ - 'application/json' - ].schema = { - type: 'array', + it('additionalProperties schema without items property', async () => { + const input = { + type: 'object', + additionalProperties: { + type: 'array', + }, }; - const results = await testRule(ruleId, rule, testDocument); + const results = await unitTestRule(ruleId, rule, input); + expect(results).toHaveLength(1); + expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgItems); expect(results[0].severity).toBe(expectedSeverity); - expect(results[0].path.join('.')).toBe( - 'paths./v1/movies.post.requestBody.content.application/json.schema' - ); - }); - - it('Request schema with non-object items property', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.paths['/v1/movies'].post.requestBody.content[ - 'application/json' - ].schema = { - type: 'array', - items: 'not a schema!', - }; - - // If the API definition contains an array schema with the items - // field set to something other than an object (e.g. a string), - // then spectral will throw an exception and we want to verify that - // behavior here. - await expect( - testRule(ruleId, rule, testDocument, true) - ).rejects.toThrow(); - }); - it('additionalProperties schema without items property', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.components.schemas.Movie.additionalProperties = { - type: 'array', - }; - - const results = await testRule(ruleId, rule, testDocument); - expect(results).toHaveLength(4); - const expectedPaths = [ - 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.additionalProperties', - 'paths./v1/movies.post.responses.201.content.application/json.schema.additionalProperties', - 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.additionalProperties', - 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.additionalProperties', - ]; - for (let i = 0; i < results.length; i++) { - expect(results[i].code).toBe(ruleId); - expect(results[i].message).toBe(expectedMsgItems); - expect(results[i].severity).toBe(expectedSeverity); - expect(results[i].path.join('.')).toBe(expectedPaths[i]); - } + expect(results[0].path).toStrictEqual(['additionalProperties']); }); it('minItems > maxItems', async () => { - const testDocument = makeCopy(rootDocument); - - testDocument.components.schemas.Car.properties['wheel_count'] = { + const input = { type: 'array', items: { @@ -562,95 +439,64 @@ describe(`Spectral rule: ${ruleId}`, () => { maxItems: 4, }; - const results = await testRule(ruleId, rule, testDocument); - expect(results).toHaveLength(3); - const expectedPaths = [ - 'paths./v1/cars.post.responses.201.content.application/json.schema.properties.wheel_count', - 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count', - 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count', - ]; - for (let i = 0; i < results.length; i++) { - expect(results[i].code).toBe(ruleId); - expect(results[i].message).toBe( - `'minItems' cannot be greater than 'maxItems'` - ); - expect(results[i].severity).toBe(expectedSeverity); - expect(results[i].path.join('.')).toBe(expectedPaths[i]); - } + const results = await unitTestRule(ruleId, rule, input); + + expect(results).toHaveLength(1); + + expect(results[0].code).toBe(ruleId); + expect(results[0].message).toBe(expectedMsgMinMax); + expect(results[0].severity).toBe(expectedSeverity); + expect(results[0].path).toStrictEqual([]); }); - it('minItems defined for non-array schema', async () => { - const testDocument = makeCopy(rootDocument); - testDocument.components.schemas.Car.properties['wheel_count'] = { + it('minItems defined for non-array schema', async () => { + const input = { type: 'object', minItems: 3, }; - const results = await testRule(ruleId, rule, testDocument); - expect(results).toHaveLength(3); - const expectedPaths = [ - 'paths./v1/cars.post.responses.201.content.application/json.schema.properties.wheel_count.minItems', - 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.minItems', - 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.minItems', - ]; - for (let i = 0; i < results.length; i++) { - expect(results[i].code).toBe(ruleId); - expect(results[i].message).toBe( - `'minItems' should not be defined for a non-array schema` - ); - expect(results[i].severity).toBe(expectedSeverity); - expect(results[i].path.join('.')).toBe(expectedPaths[i]); - } + const results = await unitTestRule(ruleId, rule, input); + + expect(results).toHaveLength(1); + + expect(results[0].code).toBe(ruleId); + expect(results[0].message).toBe(expectedMsgMinNonArray); + expect(results[0].severity).toBe(expectedSeverity); + expect(results[0].path).toStrictEqual(['minItems']); }); - it('maxItems defined for non-array schema', async () => { - const testDocument = makeCopy(rootDocument); - testDocument.components.schemas.Car.properties['wheel_count'] = { - type: 'integer', + it('maxItems defined for non-array schema', async () => { + const input = { + type: 'object', maxItems: 3, }; - const results = await testRule(ruleId, rule, testDocument); - expect(results).toHaveLength(3); - const expectedPaths = [ - 'paths./v1/cars.post.responses.201.content.application/json.schema.properties.wheel_count.maxItems', - 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.maxItems', - 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.maxItems', - ]; - for (let i = 0; i < results.length; i++) { - expect(results[i].code).toBe(ruleId); - expect(results[i].message).toBe( - `'maxItems' should not be defined for a non-array schema` - ); - expect(results[i].severity).toBe(expectedSeverity); - expect(results[i].path.join('.')).toBe(expectedPaths[i]); - } + const results = await unitTestRule(ruleId, rule, input); + + expect(results).toHaveLength(1); + + expect(results[0].code).toBe(ruleId); + expect(results[0].message).toBe(expectedMsgMaxNonArray); + expect(results[0].severity).toBe(expectedSeverity); + expect(results[0].path).toStrictEqual(['maxItems']); }); - it('items defined for non-array schema', async () => { - const testDocument = makeCopy(rootDocument); - testDocument.components.schemas.Car.properties['wheel_count'] = { - type: 'integer', + it('items defined for non-array schema', async () => { + const input = { + type: 'object', items: { - type: 'integer', + type: 'integer' }, }; - const results = await testRule(ruleId, rule, testDocument); - expect(results).toHaveLength(3); - const expectedPaths = [ - 'paths./v1/cars.post.responses.201.content.application/json.schema.properties.wheel_count.items', - 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.items', - 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.items', - ]; - for (let i = 0; i < results.length; i++) { - expect(results[i].code).toBe(ruleId); - expect(results[i].message).toBe( - `'items' should not be defined for a non-array schema` - ); - expect(results[i].severity).toBe(expectedSeverity); - expect(results[i].path.join('.')).toBe(expectedPaths[i]); - } + const results = await unitTestRule(ruleId, rule, input); + + expect(results).toHaveLength(1); + + expect(results[0].code).toBe(ruleId); + expect(results[0].message).toBe(expectedMsgItemsNonArray); + expect(results[0].severity).toBe(expectedSeverity); + expect(results[0].path).toStrictEqual(['items']); }); }); }); diff --git a/packages/ruleset/test/test-utils/index.js b/packages/ruleset/test/test-utils/index.js index a753b6a07..b80ce86ff 100644 --- a/packages/ruleset/test/test-utils/index.js +++ b/packages/ruleset/test/test-utils/index.js @@ -6,6 +6,7 @@ const allSchemasDocument = require('./all-schemas-document'); const makeCopy = require('./make-copy'); const testRule = require('./test-rule'); +const unitTestRule = require('./unit-test-rule'); const rootDocument = require('./root-document'); const severityCodes = require('./severity-codes'); const helperArtifacts = require('./helper-artifacts'); @@ -15,6 +16,7 @@ module.exports = { makeCopy, rootDocument, testRule, + unitTestRule, severityCodes, helperArtifacts, }; diff --git a/packages/ruleset/test/test-utils/unit-test-rule.js b/packages/ruleset/test/test-utils/unit-test-rule.js new file mode 100644 index 000000000..8fde5cc94 --- /dev/null +++ b/packages/ruleset/test/test-utils/unit-test-rule.js @@ -0,0 +1,42 @@ +/** + * Copyright 2025 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { Spectral } = require('@stoplight/spectral-core'); + +/** + * This is a test utility function that uses spectral to invoke the specified rule + * on the specified API definition. + * @param {string} ruleName the name of the rule (e.g. `ibm-string-attributes`) + * @param {object} rule the rule object + * @param {any} one of the input values expected by the rule after applying its `given` + * @param {boolean} exceptionIsExpected if `true`, exceptions are not displayed on console + */ +async function unitTestRule(ruleName, rule, input, exceptionIsExpected = false) { + const ruleDef = { + ...rule, + given: ['$'], + }; + + delete ruleDef.formats; + + try { + const spectral = new Spectral(); + spectral.setRuleset({ + rules: { + [ruleName]: ruleDef, + }, + }); + + const results = await spectral.run(input); + return results; + } catch (err) { + if (!exceptionIsExpected) { + console.error(err); + } + throw err; + } +} + +module.exports = unitTestRule;