-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add function to check properties of LRO response schema
- Loading branch information
1 parent
b925a94
commit ec45477
Showing
3 changed files
with
242 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |