Add function to check properties of LRO response schema
This commit is contained in:
Родитель
b925a9491d
Коммит
ec4547768e
|
@ -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);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче