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);
});