Add az-property-default-not-allowed rule with test & docs
This commit is contained in:
Родитель
e3a2c8151c
Коммит
7a8adfd389
|
@ -202,6 +202,9 @@ the characters that can be be used in the parameter value.
|
|||
|
||||
Using post for a create operation is discouraged. Use put or patch instead.
|
||||
|
||||
### az-property-default-not-allowed
|
||||
|
||||
A required property should not specify a default value.
|
||||
### az-property-description
|
||||
|
||||
All schema properties should have a description.
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// Check that required properties of a schema do not have a default.
|
||||
|
||||
// `input` is the schema of a request or response body
|
||||
module.exports = function propertyDefaultNotAllowed(schema, options, { path }) {
|
||||
if (schema === null || typeof schema !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const prop of schema.required || []) {
|
||||
if (schema.properties[prop]?.default) {
|
||||
errors.push({
|
||||
message: `Schema property "${prop}" is required and cannot have a default`,
|
||||
path: [...path, 'properties', prop, 'default'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.properties && typeof schema.properties === 'object') {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(schema.properties)) {
|
||||
errors.push(
|
||||
...propertyDefaultNotAllowed(value, options, { path: [...path, 'properties', key] }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.items) {
|
||||
errors.push(
|
||||
...propertyDefaultNotAllowed(schema.items, options, { path: [...path, 'items'] }),
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.allOf && Array.isArray(schema.allOf)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [index, value] of schema.allOf.entries()) {
|
||||
errors.push(
|
||||
...propertyDefaultNotAllowed(value, options, { path: [...path, 'allOf', index] }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
|
@ -15,6 +15,7 @@ functions:
|
|||
- patch-content-type
|
||||
- path-param-schema
|
||||
- path-param-names
|
||||
- property-default-not-allowed
|
||||
- readonly-in-response-schema
|
||||
- security-definitions
|
||||
- security-requirements
|
||||
|
@ -409,6 +410,17 @@ rules:
|
|||
field: '201'
|
||||
function: falsy
|
||||
|
||||
az-property-default-not-allowed:
|
||||
description: A required property should not specify a default value.
|
||||
message: '{{error}}'
|
||||
severity: warn
|
||||
formats: ['oas2']
|
||||
given:
|
||||
- $.paths[*].[put,post,patch].parameters.[?(@.in == 'body')].schema
|
||||
- $.paths[*].[get,put,post,patch,delete].responses[*].schema
|
||||
then:
|
||||
function: property-default-not-allowed
|
||||
|
||||
az-property-description:
|
||||
description: All schema properties should have a description.
|
||||
message: Property should have a description.
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
const { linterForRule } = require('./utils');
|
||||
|
||||
let linter;
|
||||
|
||||
beforeAll(async () => {
|
||||
linter = await linterForRule('az-property-default-not-allowed');
|
||||
return linter;
|
||||
});
|
||||
|
||||
test('az-property-default-not-allowed should find errors', () => {
|
||||
const myOpenApiDocument = {
|
||||
swagger: '2.0',
|
||||
paths: {
|
||||
'/path1': {
|
||||
put: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
in: 'body',
|
||||
schema: {
|
||||
$ref: '#/definitions/MyBodyModel',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'OK',
|
||||
schema: {
|
||||
$ref: '#/definitions/MyResponseModel',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
definitions: {
|
||||
MyBodyModel: {
|
||||
type: 'object',
|
||||
required: ['prop1'],
|
||||
properties: {
|
||||
prop1: {
|
||||
type: 'string',
|
||||
default: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
MyResponseModel: {
|
||||
type: 'object',
|
||||
required: ['prop2'],
|
||||
properties: {
|
||||
prop2: {
|
||||
type: 'string',
|
||||
default: 'bar',
|
||||
},
|
||||
prop3: {
|
||||
$ref: '#/definitions/MyNestedResponseModel',
|
||||
},
|
||||
},
|
||||
allOf: [
|
||||
{
|
||||
required: ['prop5'],
|
||||
properties: {
|
||||
prop5: {
|
||||
type: 'string',
|
||||
default: 'qux',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MyNestedResponseModel: {
|
||||
type: 'object',
|
||||
required: ['prop4'],
|
||||
properties: {
|
||||
prop4: {
|
||||
type: 'string',
|
||||
default: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return linter.run(myOpenApiDocument).then((results) => {
|
||||
expect(results.length).toBe(4);
|
||||
expect(results[0].path.join('.')).toBe('paths./path1.put.parameters.0.schema.properties.prop1.default');
|
||||
expect(results[0].message).toBe('Schema property "prop1" is required and cannot have a default');
|
||||
expect(results[1].path.join('.')).toBe('paths./path1.put.responses.200.schema.allOf.0.properties.prop5.default');
|
||||
expect(results[1].message).toBe('Schema property "prop5" is required and cannot have a default');
|
||||
expect(results[2].path.join('.')).toBe('paths./path1.put.responses.200.schema.properties.prop2.default');
|
||||
expect(results[2].message).toBe('Schema property "prop2" is required and cannot have a default');
|
||||
expect(results[3].path.join('.')).toBe('paths./path1.put.responses.200.schema.properties.prop3.properties.prop4.default');
|
||||
expect(results[3].message).toBe('Schema property "prop4" is required and cannot have a default');
|
||||
});
|
||||
});
|
||||
|
||||
test('az-property-default-not-allowed should find no errors', () => {
|
||||
const myOpenApiDocument = {
|
||||
swagger: '2.0',
|
||||
paths: {
|
||||
'/path1': {
|
||||
put: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
in: 'body',
|
||||
schema: {
|
||||
$ref: '#/definitions/MyBodyModel',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'OK',
|
||||
schema: {
|
||||
$ref: '#/definitions/MyResponseModel',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
definitions: {
|
||||
MyBodyModel: {
|
||||
type: 'object',
|
||||
required: ['prop1'],
|
||||
properties: {
|
||||
prop1: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
MyResponseModel: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prop2: {
|
||||
type: 'string',
|
||||
default: 'bar',
|
||||
},
|
||||
prop3: {
|
||||
$ref: '#/definitions/MyNestedResponseModel',
|
||||
},
|
||||
},
|
||||
allOf: [
|
||||
{
|
||||
required: ['prop5'],
|
||||
properties: {
|
||||
prop5: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MyNestedResponseModel: {
|
||||
type: 'object',
|
||||
required: ['prop4'],
|
||||
properties: {
|
||||
prop4: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return linter.run(myOpenApiDocument).then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче