diff --git a/.secrets.baseline b/.secrets.baseline index 6405ea55..f5fe46bc 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-05-20T16:10:39Z", + "generated_at": "2024-06-21T19:45:54Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -106,7 +106,7 @@ } ] }, - "version": "0.13.1+ibm.62.dss", + "version": "0.13.1+ibm.56.dss", "word_list": { "file": null, "hash": null diff --git a/docs/ibm-cloud-rules.md b/docs/ibm-cloud-rules.md index dd7158ed..8fe557dc 100644 --- a/docs/ibm-cloud-rules.md +++ b/docs/ibm-cloud-rules.md @@ -939,7 +939,7 @@ paths: Description: -Array schemas must define the items field, and should define the minItems and maxItems fields. +Array schemas must define the items field, and should define the minItems and maxItems fields. Non-arrays must not define array keywords. [1]. @@ -4911,6 +4911,9 @@ paths:
  • minimum must not be greater than maximum.
  • minimum must not be defined for a schema type other than integer or number.
  • maximum must not be defined for a schema type other than integer or number.
  • +
  • multipleOf must not be defined for a schema type other than integer or number.
  • +
  • exclusiveMaximum must not be defined for a schema type other than integer or number.
  • +
  • exclusiveMinimum must not be defined for a schema type other than integer or number.
  • Object schemas (type=object):
    @@ -4919,6 +4922,9 @@ paths:
  • minProperties must not be greater than maxProperties.
  • minProperties must not be defined for a schema type other than object.
  • maxProperties must not be defined for a schema type other than object.
  • +
  • additionalProperties must not be defined for a schema type other than object.
  • +
  • properties must not be defined for a schema type other than object.
  • +
  • required must not be defined for a schema type other than object.
  • diff --git a/packages/ruleset/src/functions/array-attributes.js b/packages/ruleset/src/functions/array-attributes.js index 706a49f0..2048b4d3 100644 --- a/packages/ruleset/src/functions/array-attributes.js +++ b/packages/ruleset/src/functions/array-attributes.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -64,6 +64,16 @@ function arrayAttributeErrors(schema, path) { }); } + // Is enum defined? Shouldn't be + const enm = getCompositeSchemaAttribute(schema, 'enum'); + if (isDefined(enm)) { + logger.debug('enum field is present!'); + errors.push({ + message: `Array schemas should not define an 'enum' field`, + path: [...path, 'enum'], + }); + } + // Is items defined? const items = getCompositeSchemaAttribute(schema, 'items'); if (!isDefined(items) || !isPlainObject(items)) { @@ -76,7 +86,7 @@ function arrayAttributeErrors(schema, path) { ]; } } else { - // minItems/maxItems should not be defined for a non-array schema + // minItems/maxItems/items should not be defined for a non-array schema if (schema.minItems) { errors.push({ message: `'minItems' should not be defined for a non-array schema`, @@ -89,6 +99,12 @@ function arrayAttributeErrors(schema, path) { path: [...path, 'maxItems'], }); } + if (schema.items) { + errors.push({ + message: `'items' should not be defined for a non-array schema`, + path: [...path, 'items'], + }); + } } return errors; diff --git a/packages/ruleset/src/functions/property-attributes.js b/packages/ruleset/src/functions/property-attributes.js index 58a462c9..aba15d1c 100644 --- a/packages/ruleset/src/functions/property-attributes.js +++ b/packages/ruleset/src/functions/property-attributes.js @@ -1,14 +1,13 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const { validateSubschemas, + isBooleanSchema, isNumberSchema, isIntegerSchema, - isFloatSchema, - isDoubleSchema, isObjectSchema, } = require('@ibm-cloud/openapi-ruleset-utilities'); const { LoggerFactory } = require('../utils'); @@ -26,11 +25,11 @@ module.exports = function (schema, _opts, context) { /** * This rule performs the following checks on each schema (and schema property) * found in the API definition: - * 1) minimum/maximum should not be defined for a non-numeric (number, integer) schema + * 1) Number-scope keywords should not be defined for a non-numeric (number, integer) schema * 2) minimum <= maximum - * 3) minItems/maxItems should not be defined for a non-array schema - * 4) minProperties/maxProperties should not be defined for a non-object schema + * 4) Object-scope keywords should not be defined for a non-object schema * 5) minProperties <= maxProperties + * 6) enum field should not be present for object or boolean schemas * * @param {*} schema the schema to check * @param {*} path the array of path segments indicating the "location" of the schema within the API definition @@ -47,26 +46,49 @@ function checkPropertyAttributes(schema, path) { logger.debug('schema is numeric'); // 2) minimum <= maximum - if (schema.minimum && schema.maximum && schema.minimum > schema.maximum) { + if ( + 'minimum' in schema && + 'maximum' in schema && + schema.minimum > schema.maximum + ) { errors.push({ message: `'minimum' cannot be greater than 'maximum'`, path: [...path, 'minimum'], }); } } else { - // 1) minimum/maximum should not be defined for a non-numeric (number, integer) schema - if (schema.minimum) { + // 1) minimum/maximum/multipleOf/exclusiveMaximum/exclusiveMinimum + // should not be defined for a non-numeric (number, integer) schema + if ('minimum' in schema) { errors.push({ message: `'minimum' should not be defined for non-numeric schemas`, path: [...path, 'minimum'], }); } - if (schema.maximum) { + if ('maximum' in schema) { errors.push({ message: `'maximum' should not be defined for non-numeric schemas`, path: [...path, 'maximum'], }); } + if ('multipleOf' in schema) { + errors.push({ + message: `'multipleOf' should not be defined for non-numeric schemas`, + path: [...path, 'multipleOf'], + }); + } + if ('exclusiveMaximum' in schema) { + errors.push({ + message: `'exclusiveMaximum' should not be defined for non-numeric schemas`, + path: [...path, 'exclusiveMaximum'], + }); + } + if ('exclusiveMinimum' in schema) { + errors.push({ + message: `'exclusiveMinimum' should not be defined for non-numeric schemas`, + path: [...path, 'exclusiveMinimum'], + }); + } } if (isObjectSchema(schema)) { @@ -74,8 +96,8 @@ function checkPropertyAttributes(schema, path) { // 5) minProperties <= maxProperties if ( - schema.minProperties && - schema.maxProperties && + 'minProperties' in schema && + 'maxProperties' in schema && schema.minProperties > schema.maxProperties ) { errors.push({ @@ -83,30 +105,62 @@ function checkPropertyAttributes(schema, path) { path: [...path, 'minProperties'], }); } + + // 6) enum should not be present + if ('enum' in schema) { + errors.push({ + message: `'enum' should not be defined for object schemas`, + path: [...path, 'enum'], + }); + } } else { - // 4) minProperties/maxProperties should not be defined for a non-object schema - if (schema.minProperties) { + // 4) minProperties/maxProperties/additionalProperties/properties/required + // should not be defined for a non-object schema + if ('minProperties' in schema) { errors.push({ message: `'minProperties' should not be defined for non-object schemas`, path: [...path, 'minProperties'], }); } - if (schema.maxProperties) { + if ('maxProperties' in schema) { errors.push({ message: `'maxProperties' should not be defined for non-object schemas`, path: [...path, 'maxProperties'], }); } + if ('additionalProperties' in schema) { + errors.push({ + message: `'additionalProperties' should not be defined for non-object schemas`, + path: [...path, 'additionalProperties'], + }); + } + if ('properties' in schema) { + errors.push({ + message: `'properties' should not be defined for non-object schemas`, + path: [...path, 'properties'], + }); + } + if ('required' in schema) { + errors.push({ + message: `'required' should not be defined for non-object schemas`, + path: [...path, 'required'], + }); + } + } + + if (isBooleanSchema(schema)) { + // 6) enum should not be present + if ('enum' in schema) { + errors.push({ + message: `'enum' should not be defined for boolean schemas`, + path: [...path, 'enum'], + }); + } } return errors; } function isNumericSchema(s) { - return ( - isNumberSchema(s) || - isIntegerSchema(s) || - isFloatSchema(s) || - isDoubleSchema(s) - ); + return isNumberSchema(s) || isIntegerSchema(s); } diff --git a/packages/ruleset/src/functions/string-attributes.js b/packages/ruleset/src/functions/string-attributes.js index cad7a009..e6aea44b 100644 --- a/packages/ruleset/src/functions/string-attributes.js +++ b/packages/ruleset/src/functions/string-attributes.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -9,7 +9,11 @@ const { validateNestedSchemas, } = require('@ibm-cloud/openapi-ruleset-utilities'); -const { getCompositeSchemaAttribute, LoggerFactory } = require('../utils'); +const { + getCompositeSchemaAttribute, + LoggerFactory, + pathMatchesRegexp, +} = require('../utils'); let ruleId; let logger; @@ -49,7 +53,9 @@ function stringBoundaryErrors(schema, path) { const errors = []; - if (isStringSchema(schema)) { + // Only check for the presence of validation keywords on input schemas + // (i.e. those used for parameters and request bodies). + if (isStringSchema(schema) && isInputSchema(path)) { logger.debug('schema is a string schema'); // Perform these checks only if enum is not defined. @@ -89,7 +95,10 @@ function stringBoundaryErrors(schema, path) { }); } } - } else { + } + + // Make sure string attributes aren't used for non-strings. + if (!isStringSchema(schema)) { // Make sure that string-related fields are not present in a non-string schema. if (schemaContainsAttribute(schema, 'pattern')) { errors.push({ @@ -110,6 +119,7 @@ function stringBoundaryErrors(schema, path) { }); } } + return errors; } @@ -125,3 +135,14 @@ function schemaContainsAttribute(schema, attrName) { s => attrName in s && isDefined(s[attrName]) ); } + +function isInputSchema(path) { + // Output schemas are much simpler to check for with regex. + // Use the inverse of that to determine input schemas. + const isOutputSchema = pathMatchesRegexp( + path, + /^paths,.*,responses,.+,(content|headers),.+,schema/ + ); + + return !isOutputSchema; +} diff --git a/packages/ruleset/src/rules/string-attributes.js b/packages/ruleset/src/rules/string-attributes.js index d73f8826..be19067e 100644 --- a/packages/ruleset/src/rules/string-attributes.js +++ b/packages/ruleset/src/rules/string-attributes.js @@ -1,8 +1,11 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ +const { + schemas, +} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections'); const { oas3 } = require('@stoplight/spectral-formats'); const { stringAttributes } = require('../functions'); @@ -12,13 +15,7 @@ module.exports = { severity: 'warn', formats: [oas3], resolved: true, - given: [ - '$.paths[*][parameters][*].schema', - '$.paths[*][parameters][*].content[*].schema', - '$.paths[*][*][parameters][*].schema', - '$.paths[*][*][parameters][*].content[*].schema', - '$.paths[*][*].requestBody.content[*].schema', - ], + given: schemas, then: { function: stringAttributes, }, diff --git a/packages/ruleset/test/array-attributes.test.js b/packages/ruleset/test/array-attributes.test.js index ae96cef9..f2b7472f 100644 --- a/packages/ruleset/test/array-attributes.test.js +++ b/packages/ruleset/test/array-attributes.test.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -162,6 +162,36 @@ describe(`Spectral rule: ${ruleId}`, () => { ); }); + it('enum defined for array schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_options'] = { + type: 'array', + maxItems: 3, + minItems: 1, + items: { + type: 'string', + }, + 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]); + } + }); + it('Inline response schema array property with only minItems', async () => { const testDocument = makeCopy(rootDocument); @@ -591,5 +621,31 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); + it('items defined for non-array schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'integer', + items: { + 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]); + } + }); }); }); diff --git a/packages/ruleset/test/property-attributes.test.js b/packages/ruleset/test/property-attributes.test.js index f54ccf50..9915adeb 100644 --- a/packages/ruleset/test/property-attributes.test.js +++ b/packages/ruleset/test/property-attributes.test.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -17,7 +17,9 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results).toHaveLength(0); }); - describe('Numeric schema tests', () => { + // !!! add other expected fields to existing represented schemas + + describe('Numeric schemas', () => { it('minimum defined by itself', async () => { const testDocument = makeCopy(rootDocument); @@ -54,7 +56,7 @@ describe(`Spectral rule: ${ruleId}`, () => { }); }); - describe('Object schema tests', () => { + describe('Object schemas', () => { it('minProperties defined by itself', async () => { const testDocument = makeCopy(rootDocument); @@ -90,17 +92,19 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results).toHaveLength(0); }); }); + + // !!! maybe add new tests for other schema types to verify proper behavior }); describe('Should yield errors', () => { - describe('Numeric schema tests', () => { + describe('Numeric schemas', () => { it('minimum > maximum', async () => { const testDocument = makeCopy(rootDocument); testDocument.components.schemas.Car.properties['wheel_count'] = { type: 'integer', minimum: 4, - maximum: 3, + maximum: 0, }; const results = await testRule(ruleId, rule, testDocument); @@ -119,6 +123,9 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); + }); + + describe('Non-numeric schemas', () => { it('minimum defined for non-numeric schema', async () => { const testDocument = makeCopy(rootDocument); @@ -167,16 +174,88 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); + it('multipleOf defined for non-numeric schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'object', + multipleOf: 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.multipleOf', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.multipleOf', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.multipleOf', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'multipleOf' should not be defined for non-numeric schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + it('exclusiveMaximum defined for non-numeric schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'object', + exclusiveMaximum: 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.exclusiveMaximum', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.exclusiveMaximum', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.exclusiveMaximum', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'exclusiveMaximum' should not be defined for non-numeric schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + it('exclusiveMinimum defined for non-numeric schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'object', + exclusiveMinimum: 0, + }; + + 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.exclusiveMinimum', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.exclusiveMinimum', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.exclusiveMinimum', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'exclusiveMinimum' should not be defined for non-numeric schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); }); - describe('Object schema tests', () => { + describe('Object schemas', () => { it('minProperties > maxProperties', async () => { const testDocument = makeCopy(rootDocument); testDocument.components.schemas.Car.properties['wheel_count'] = { type: 'object', minProperties: 5, - maxProperties: 4, + maxProperties: 0, }; const results = await testRule(ruleId, rule, testDocument); @@ -195,6 +274,35 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); + it('enum defined for object schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'object', + minProperties: 1, + maxProperties: 3, + enum: [{ foo: 'bar' }, { foo: 'bar', baz: 'bat' }], + }; + + 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.enum', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.enum', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.enum', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'enum' should not be defined for object schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('Non-object schemas', () => { it('minProperties defined for non-object schema', async () => { const testDocument = makeCopy(rootDocument); @@ -244,6 +352,108 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); + it('additionalProperties defined for non-object schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'number', + format: 'double', + additionalProperties: false, + }; + + 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.additionalProperties', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.additionalProperties', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.additionalProperties', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'additionalProperties' should not be defined for non-object schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + it('properties defined for non-object schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'number', + format: 'double', + properties: {}, + }; + + 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.properties', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.properties', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.properties', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'properties' should not be defined for non-object schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + it('required defined for non-object schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['wheel_count'] = { + type: 'number', + format: 'double', + required: true, + }; + + 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.required', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.wheel_count.required', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.wheel_count.required', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'required' should not be defined for non-object schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('Object schemas', () => { + it('enum defined for boolean schema', async () => { + const testDocument = makeCopy(rootDocument); + + testDocument.components.schemas.Car.properties['has_wheels'] = { + type: 'boolean', + enum: [true, false], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(3); + const expectedPaths = [ + 'paths./v1/cars.post.responses.201.content.application/json.schema.properties.has_wheels.enum', + 'paths./v1/cars/{car_id}.get.responses.200.content.application/json.schema.properties.has_wheels.enum', + 'paths./v1/cars/{car_id}.patch.responses.200.content.application/json.schema.properties.has_wheels.enum', + ]; + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe( + `'enum' should not be defined for boolean schemas` + ); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); }); }); }); diff --git a/packages/ruleset/test/string-attributes.test.js b/packages/ruleset/test/string-attributes.test.js index 5a48c7b3..68c60f48 100644 --- a/packages/ruleset/test/string-attributes.test.js +++ b/packages/ruleset/test/string-attributes.test.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -211,6 +211,42 @@ describe(`Spectral rule: ${ruleId}`, () => { const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); + + it('Response body string schema has no keywords', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/movies/{movie_id}'].get.responses['200'] = { + content: { + 'application/json': { + schema: { + properties: { + name: { + type: 'string', + description: 'no validation', + }, + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('Response header string schema has no keywords', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/movies/{movie_id}'].get.responses.headers = { + 'X-IBM-Something': { + schema: { + type: 'string', + description: 'no validation', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); }); describe('Should yield errors', () => { @@ -350,13 +386,14 @@ describe(`Spectral rule: ${ruleId}`, () => { it('Non-string schema defines a `minLength` field', async () => { const testDocument = makeCopy(rootDocument); - testDocument.paths['/v1/movies'].post.requestBody.content['text/plain'] = - { - schema: { - type: ['integer', 'null', 'boolean'], - minLength: 15, - }, - }; + testDocument.paths['/v1/movies'].post.responses['201'].content[ + 'text/plain' + ] = { + schema: { + type: ['integer', 'null', 'boolean'], + minLength: 15, + }, + }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); @@ -366,7 +403,7 @@ describe(`Spectral rule: ${ruleId}`, () => { `'minLength' should not be defined for non-string schemas` ); expect(validation.path.join('.')).toBe( - 'paths./v1/movies.post.requestBody.content.text/plain.schema.minLength' + 'paths./v1/movies.post.responses.201.content.text/plain.schema.minLength' ); expect(validation.severity).toBe(expectedSeverity); });