Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function to check properties of LRO response schema #133

Merged
merged 1 commit into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions functions/lro-response-schema.js
Original file line number Diff line number Diff line change
@@ -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;
};
6 changes: 3 additions & 3 deletions spectral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ functions:
- consistent-response-body
- error-response
- has-header
- lro-response-schema
- naming-convention
- operation-id
- pagination-parameters
Expand Down Expand Up @@ -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.
Expand Down
135 changes: 135 additions & 0 deletions test/lro-response-schema.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading