Add function to check properties of LRO response schema

This commit is contained in:
Mike Kistler 2023-11-19 09:36:02 -06:00
Родитель b925a9491d
Коммит ec4547768e
3 изменённых файлов: 242 добавлений и 3 удалений

Просмотреть файл

@ -0,0 +1,104 @@
// Check conformance to Azure guidelines for 202 responses:
// - A 202 response should have a response body schema
// - The response body schema should contain `id`, `status`, and `error` properties.
// - The `id`, `status`, and `error` properties should be required.
// - The `id` property should be type: string.
// - The `status` property should be type: string and enum with values:
// - "Running", "Succeeded", "Failed", "Cancelled".
// - The `error` property should be type: object and not required.
// Rule target is a 202 response
module.exports = (lroResponse, _opts, context) => {
// defensive programming - make sure we have an object
if (lroResponse === null || typeof lroResponse !== 'object') {
return [];
}
const lroResponseSchema = lroResponse.schema;
// A 202 response should include a schema for the operation status monitor.
if (!lroResponseSchema) {
return [{
message: 'A 202 response should include a schema for the operation status monitor.',
path: context.path || [],
}];
}
const path = [...(context.path || []), 'schema'];
const errors = [];
// - The `id`, `status`, and `error` properties should be required.
const requiredProperties = new Set(lroResponseSchema.required || []);
const checkRequiredProperty = (prop) => {
if (!requiredProperties.has(prop)) {
errors.push({
message: `\`${prop}\` property in LRO response should be required`,
path: [...path, 'required'],
});
}
};
// Check id property
if (lroResponseSchema.properties && 'id' in lroResponseSchema.properties) {
if (lroResponseSchema.properties.id.type !== 'string') {
errors.push({
message: '\'id\' property in LRO response should be type: string',
path: [...path, 'properties', 'id', 'type'],
});
}
checkRequiredProperty('id');
} else {
errors.push({
message: 'LRO response should contain top-level property `id`',
path: [...path, 'properties'],
});
}
// Check status property
if (lroResponseSchema.properties && 'status' in lroResponseSchema.properties) {
if (lroResponseSchema.properties.status.type !== 'string') {
errors.push({
message: '`status` property in LRO response should be type: string',
path: [...path, 'properties', 'status', 'type'],
});
}
checkRequiredProperty('status');
const statusValues = new Set(lroResponseSchema.properties.status.enum || []);
const requiredStatusValues = ['Running', 'Succeeded', 'Failed', 'Canceled'];
if (!requiredStatusValues.every((value) => statusValues.has(value))) {
errors.push({
message: `'status' property enum in LRO response should contain values: ${requiredStatusValues.join(', ')}`,
path: [...path, 'properties', 'status', 'enum'],
});
}
} else {
errors.push({
message: 'LRO response should contain top-level property `status`',
path: [...path, 'properties'],
});
}
// Check error property
if (lroResponseSchema.properties && 'error' in lroResponseSchema.properties) {
if (lroResponseSchema.properties.error.type !== 'object') {
errors.push({
message: '`error` property in LRO response should be type: object',
path: [...path, 'properties', 'error', 'type'],
});
}
if (requiredProperties.has('error')) {
errors.push({
message: '`error` property in LRO response should not be required',
path: [...path, 'required'],
});
}
} else {
errors.push({
message: 'LRO response should contain top-level property `error`',
path: [...path, 'properties'],
});
}
return errors;
};

Просмотреть файл

@ -4,6 +4,7 @@ functions:
- consistent-response-body
- error-response
- has-header
- lro-response-schema
- naming-convention
- operation-id
- pagination-parameters
@ -236,13 +237,12 @@ rules:
az-lro-response-schema:
description: A 202 response should include a schema for the operation status monitor.
message: A 202 response should include a schema for the operation status monitor.
message: '{{error}}'
severity: warn
formats: ['oas2']
given: $.paths[*][*].responses[?(@property == '202')]
then:
field: schema
function: truthy
function: lro-response-schema
az-204-no-response-body:
description: A 204 response should have no response body.

Просмотреть файл

@ -0,0 +1,135 @@
const { linterForRule } = require('./utils');
let linter;
beforeAll(async () => {
linter = await linterForRule('az-lro-response-schema');
return linter;
});
test('az-lro-response-schema should find errors', () => {
const oasDoc = {
swagger: '2.0',
paths: {
'/test1': {
post: {
responses: {
202: {
description: 'Accepted',
},
},
},
},
'/test2': {
post: {
responses: {
202: {
description: 'Accepted',
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
status: {
type: 'string',
enum: ['Running', 'Succeeded', 'Failed', 'Canceled'],
},
},
},
},
},
},
},
'/test3': {
post: {
responses: {
202: {
description: 'Accepted',
schema: {
type: 'object',
properties: {
id: {
type: 'uuid',
},
status: {
type: 'string',
enum: ['InProgress', 'Succeeded', 'Failed', 'Canceled'],
},
error: {
type: 'string',
},
},
required: ['id', 'status', 'error'],
},
},
},
},
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results).toHaveLength(8);
expect(results[0].path.join('.')).toBe('paths./test1.post.responses.202');
expect(results[0].message).toBe('A 202 response should include a schema for the operation status monitor.');
expect(results[1].path.join('.')).toBe('paths./test2.post.responses.202.schema');
expect(results[1].message).toBe('`id` property in LRO response should be required');
expect(results[2].path.join('.')).toBe('paths./test2.post.responses.202.schema');
expect(results[2].message).toBe('`status` property in LRO response should be required');
expect(results[3].path.join('.')).toBe('paths./test2.post.responses.202.schema.properties');
expect(results[3].message).toBe('LRO response should contain top-level property `error`');
expect(results[4].path.join('.')).toBe('paths./test3.post.responses.202.schema.properties.id.type');
expect(results[4].message).toBe('\'id\' property in LRO response should be type: string');
expect(results[5].path.join('.')).toBe('paths./test3.post.responses.202.schema.properties.status.enum');
expect(results[5].message).toBe('\'status\' property enum in LRO response should contain values: Running, Succeeded, Failed, Canceled');
expect(results[6].path.join('.')).toBe('paths./test3.post.responses.202.schema.properties.error.type');
expect(results[6].message).toBe('`error` property in LRO response should be type: object');
expect(results[7].path.join('.')).toBe('paths./test3.post.responses.202.schema.required');
expect(results[7].message).toBe('`error` property in LRO response should not be required');
});
});
test('az-lro-response-schema should find no errors', () => {
const oasDoc = {
swagger: '2.0',
paths: {
'/test1': {
post: {
responses: {
202: {
description: 'Accepted',
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
status: {
type: 'string',
enum: ['Running', 'Succeeded', 'Failed', 'Canceled'],
},
error: {
type: 'object',
properties: {
code: {
type: 'string',
},
message: {
type: 'string',
},
},
required: ['code', 'message'],
},
},
required: ['id', 'status'],
},
},
},
},
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results).toHaveLength(0);
});
});