Improvements to az-path-parameter-schema

This commit is contained in:
Mike Kistler 2022-10-28 21:44:14 -05:00
Родитель fd2e2ce038
Коммит 0bc674b808
3 изменённых файлов: 373 добавлений и 55 удалений

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

@ -1,12 +1,12 @@
const URL_MAX_LENGTH = 2083;
// `given` is a (resolved) parameter entry at the path or operation level
module.exports = (param, _opts, paths) => {
module.exports = (param, _opts, context) => {
if (param === null || typeof param !== 'object') {
return [];
}
const path = paths.path || paths.target || [];
const path = context.path || context.target || [];
// These errors will be caught elsewhere, so silently ignore here
if (!param.in || !param.name) {
@ -30,6 +30,21 @@ module.exports = (param, _opts, paths) => {
});
}
// Only check constraints for the final path parameter on a put or patch that returns a 201
const apiPath = path[1] ?? '';
if (!apiPath.endsWith(`{${param.name}}`)) {
return errors;
}
if (!['put', 'patch'].includes(path[2] ?? '')) {
return errors;
}
const oasDoc = context.document.data;
const { responses } = oasDoc.paths[apiPath][path[2]];
if (!responses || !responses['201']) {
return errors;
}
if (!schema.maxLength && !schema.pattern) {
errors.push({
message: 'Path parameter should specify a maximum length (maxLength) and characters allowed (pattern).',
@ -43,7 +58,7 @@ module.exports = (param, _opts, paths) => {
} else if (schema.maxLength && schema.maxLength >= URL_MAX_LENGTH) {
errors.push({
message: `Path parameter maximum length should be less than ${URL_MAX_LENGTH}`,
path,
path: [...path, 'maxLength'],
});
} else if (!schema.pattern) {
errors.push({

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

@ -347,7 +347,7 @@ rules:
formats: ['oas2', 'oas3']
given:
- $.paths[*].parameters[?(@.in == 'path')]
- $.paths.*[get,put,post,patch,delete,options,head].parameters[?(@.in == 'path')]
- $.paths[*][get,put,post,patch,delete,options,head].parameters[?(@.in == 'path')]
then:
function: path-param-schema
@ -524,7 +524,8 @@ rules:
description: All success responses except 202 & 204 should define a response body.
severity: warn
formats: ['oas2']
given: $.paths[*][*].responses[?(@property >= 200 && @property < 300 && @property != '202' && @property != '204')]
# list http methods explicitly to exclude head
given: $.paths[*][get,put,post,patch,delete].responses[?(@property >= 200 && @property < 300 && @property != '202' && @property != '204')]
then:
field: schema
function: truthy

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

@ -15,6 +15,7 @@ test('az-path-parameter-schema should find errors', () => {
const oasDoc = {
swagger: '2.0',
paths: {
// 0: should be defined as type: string
'/foo/{p1}': {
parameters: [
{
@ -24,49 +25,167 @@ test('az-path-parameter-schema should find errors', () => {
},
],
},
'/bar/{p2}/baz/{p3}/foo/{p4}': {
get: {
// 1: should specify a maximum length (maxLength) and characters allowed (pattern) -- p2
'/bar/{p2}': {
put: {
parameters: [
{
$ref: '#/parameters/Param2',
name: 'p2',
in: 'path',
type: 'string',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 2: should specify characters allowed (pattern) -- p4
'/baz/{p3}/qux/{p4}': {
put: {
parameters: [
{
name: 'p3',
in: 'path',
type: 'string',
maxLength: 50,
},
{
name: 'p4',
$ref: '#/parameters/Param4',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 3: should be less than
'/foobar/{p5}': {
put: {
parameters: [
{
name: 'p5',
in: 'path',
type: 'string',
maxLength: 2083,
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
},
parameters: {
Param2: {
name: 'p2',
Param4: {
name: 'p4',
in: 'path',
type: 'string',
maxLength: 64,
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(5);
expect(results[0].path.join('.')).toBe('paths./foo/{p1}.parameters.0');
expect(results.length).toBe(4);
expect(results[0].path.join('.')).toBe('paths./foo/{p1}.parameters.0.type');
expect(results[0].message).toContain('should be defined as type: string');
expect(results[1].path.join('.')).toBe('paths./bar/{p2}.put.parameters.0');
expect(results[1].message).toContain('should specify a maximum length');
expect(results[1].message).toContain('and characters allowed');
expect(results[2].path.join('.')).toBe('paths./baz/{p3}/qux/{p4}.put.parameters.1');
expect(results[2].message).toContain('should specify characters allowed');
expect(results[3].path.join('.')).toBe('paths./foobar/{p5}.put.parameters.0.maxLength');
expect(results[3].message).toContain('should be less than');
});
});
test('az-path-parameter-schema should find errors in patch operations', () => {
// Test path parameter in 3 different places:
// 1. parameter at path level
// 2. inline parameter at operation level
// 3. referenced parameter at operation level
const oasDoc = {
swagger: '2.0',
paths: {
// 0: should specify a maximum length (maxLength) and characters allowed (pattern) -- p2
'/bar/{p2}': {
patch: {
parameters: [
{
name: 'p2',
in: 'path',
type: 'string',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 1: should specify characters allowed (pattern) -- p4
'/baz/{p3}/qux/{p4}': {
patch: {
parameters: [
{
name: 'p3',
in: 'path',
type: 'string',
},
{
$ref: '#/parameters/Param4',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 2: should be less than
'/foobar/{p5}': {
patch: {
parameters: [
{
name: 'p5',
in: 'path',
type: 'string',
maxLength: 2083,
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
},
parameters: {
Param4: {
name: 'p4',
in: 'path',
type: 'string',
maxLength: 64,
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(3);
expect(results[0].path.join('.')).toBe('paths./bar/{p2}.patch.parameters.0');
expect(results[0].message).toContain('should specify a maximum length');
expect(results[0].message).toContain('and characters allowed');
expect(results[1].path.join('.')).toBe('paths./foo/{p1}.parameters.0.type');
expect(results[1].message).toContain('should be defined as type: string');
expect(results[2].path.join('.')).toBe('paths./bar/{p2}/baz/{p3}/foo/{p4}.get.parameters.0');
expect(results[2].message).toContain('should specify a maximum length');
expect(results[3].path.join('.')).toBe('paths./bar/{p2}/baz/{p3}/foo/{p4}.get.parameters.1');
expect(results[3].message).toContain('should specify characters allowed');
expect(results[4].path.join('.')).toBe('paths./bar/{p2}/baz/{p3}/foo/{p4}.get.parameters.2');
expect(results[4].message).toContain('should be less than');
expect(results[1].path.join('.')).toBe('paths./baz/{p3}/qux/{p4}.patch.parameters.1');
expect(results[1].message).toContain('should specify characters allowed');
expect(results[2].path.join('.')).toBe('paths./foobar/{p5}.patch.parameters.0.maxLength');
expect(results[2].message).toContain('should be less than');
});
});
@ -74,6 +193,7 @@ test('az-path-parameter-schema should find no errors', () => {
const oasDoc = {
swagger: '2.0',
paths: {
// 0: should be defined as type: string
'/foo/{p1}': {
parameters: [
{
@ -85,29 +205,117 @@ test('az-path-parameter-schema should find no errors', () => {
},
],
},
'/bar/{p2}/baz/{p3}': {
get: {
'/bar/{p2}': {
put: {
parameters: [
{
$ref: '#/parameters/Param2',
},
{
name: 'p3',
name: 'p2',
in: 'path',
type: 'string',
maxLength: 50,
pattern: '/[a-z]+/',
},
],
responses: {
201: {
description: 'Created',
},
},
},
patch: {
parameters: [
{
name: 'p2',
in: 'path',
type: 'string',
maxLength: 50,
pattern: '/[a-z]+/',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
'/baz/{p3}/qux/{p4}': {
put: {
parameters: [
{
name: 'p3',
in: 'path',
type: 'string',
},
{
$ref: '#/parameters/Param4',
},
],
responses: {
201: {
description: 'Created',
},
},
},
patch: {
parameters: [
{
name: 'p3',
in: 'path',
type: 'string',
},
{
$ref: '#/parameters/Param4',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
'/foobar/{p5}': {
put: {
parameters: [
{
name: 'p5',
in: 'path',
type: 'string',
maxLength: 50,
pattern: '/[a-z]+/',
},
],
responses: {
201: {
description: 'Created',
},
},
},
patch: {
parameters: [
{
name: 'p5',
in: 'path',
type: 'string',
maxLength: 50,
pattern: '/[a-z]+/',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
},
parameters: {
Param2: {
name: 'p2',
Param4: {
name: 'p4',
in: 'path',
type: 'string',
maxLength: 50,
maxLength: 64,
pattern: '/[a-z]+/',
},
},
@ -121,6 +329,7 @@ test('az-path-parameter-schema should find oas3 errors', () => {
const oasDoc = {
openapi: '3.0',
paths: {
// 0: should be defined as type: string
'/foo/{p1}': {
parameters: [
{
@ -132,31 +341,74 @@ test('az-path-parameter-schema should find oas3 errors', () => {
},
],
},
'/bar/{p2}/baz/{p3}': {
get: {
// 1: should specify a maximum length (maxLength) and characters allowed (pattern) -- p2
'/bar/{p2}': {
put: {
parameters: [
{
$ref: '#/components/parameters/Param2',
name: 'p2',
in: 'path',
schema: {
type: 'string',
},
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 2: should specify characters allowed (pattern) -- p4
'/baz/{p3}/qux/{p4}': {
put: {
parameters: [
{
name: 'p3',
in: 'path',
schema: {
type: 'string',
maxLength: 50,
},
},
{
$ref: '#/components/parameters/Param4',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 3: should be less than
'/foobar/{p5}': {
put: {
parameters: [
{
name: 'p5',
in: 'path',
type: 'string',
maxLength: 2083,
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
},
components: {
parameters: {
Param2: {
name: 'p2',
Param4: {
name: 'p4',
in: 'path',
schema: {
type: 'string',
maxLength: 64,
},
},
},
@ -164,15 +416,15 @@ test('az-path-parameter-schema should find oas3 errors', () => {
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(4);
expect(results[0].path.join('.')).toBe('paths./foo/{p1}.parameters.0.schema');
expect(results[0].message).toContain('should specify a maximum length');
expect(results[0].message).toContain('and characters allowed');
expect(results[1].path.join('.')).toBe('paths./foo/{p1}.parameters.0.schema.type');
expect(results[1].message).toContain('should be defined as type: string');
expect(results[2].path.join('.')).toBe('paths./bar/{p2}/baz/{p3}.get.parameters.0.schema');
expect(results[2].message).toContain('should specify a maximum length');
expect(results[3].path.join('.')).toBe('paths./bar/{p2}/baz/{p3}.get.parameters.1.schema');
expect(results[3].message).toContain('should specify characters allowed');
expect(results[0].path.join('.')).toBe('paths./foo/{p1}.parameters.0.schema.type');
expect(results[0].message).toContain('should be defined as type: string');
expect(results[1].path.join('.')).toBe('paths./bar/{p2}.put.parameters.0.schema');
expect(results[1].message).toContain('should specify a maximum length');
expect(results[1].message).toContain('and characters allowed');
expect(results[2].path.join('.')).toBe('paths./baz/{p3}/qux/{p4}.put.parameters.1.schema');
expect(results[2].message).toContain('should specify characters allowed');
expect(results[3].path.join('.')).toBe('paths./foobar/{p5}.put.parameters.0.maxLength');
expect(results[3].message).toContain('should be less than');
});
});
@ -180,6 +432,7 @@ test('az-path-parameter-schema should find no oas3 errors', () => {
const oasDoc = {
openapi: '3.0',
paths: {
// 0: should be defined as type: string
'/foo/{p1}': {
parameters: [
{
@ -192,15 +445,18 @@ test('az-path-parameter-schema should find no oas3 errors', () => {
},
},
],
responses: {
201: {
description: 'Created',
},
},
},
'/bar/{p2}/baz/{p3}': {
get: {
// 1: should specify a maximum length (maxLength) and characters allowed (pattern) -- p2
'/bar/{p2}': {
put: {
parameters: [
{
$ref: '#/components/parameters/Param2',
},
{
name: 'p3',
name: 'p2',
in: 'path',
schema: {
type: 'string',
@ -209,17 +465,63 @@ test('az-path-parameter-schema should find no oas3 errors', () => {
},
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 2: should specify characters allowed (pattern) -- p4
'/baz/{p3}/qux/{p4}': {
put: {
parameters: [
{
name: 'p3',
in: 'path',
schema: {
type: 'string',
},
},
{
$ref: '#/components/parameters/Param4',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
// 3: should be less than
'/foobar/{p5}': {
put: {
parameters: [
{
name: 'p5',
in: 'path',
type: 'string',
maxLength: 50,
pattern: '/[a-z]+/',
},
],
responses: {
201: {
description: 'Created',
},
},
},
},
},
components: {
parameters: {
Param2: {
name: 'p2',
Param4: {
name: 'p4',
in: 'path',
schema: {
type: 'string',
maxLength: 50,
maxLength: 64,
pattern: '/[a-z]+/',
},
},