Skip to content

Commit

Permalink
Fix tests to be resilient to message order
Browse files Browse the repository at this point in the history
  • Loading branch information
mikekistler committed Jan 24, 2025
1 parent 4edf1bf commit 3e2c92f
Show file tree
Hide file tree
Showing 10 changed files with 799 additions and 220 deletions.
9 changes: 2 additions & 7 deletions functions/param-names-unique.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@ module.exports = (pathItem, _opts, paths) => {
pathDups.forEach((dup) => {
// get the index of all names that match dup
const dupKeys = [...pathParams.keys()].filter((k) => canonical(pathParams[k]) === dup);
// Refer back to the first one
const first = `parameters.${dupKeys[0]}`;
// Report errors for all the others
dupKeys.slice(1).forEach((key) => {
errors.push({
message: `Duplicate parameter name (ignoring case) with ${first}.`,
message: `Duplicate parameter name (ignoring case): ${dup}.`,
path: [...path, 'parameters', key, 'name'],
});
});
Expand All @@ -61,13 +59,10 @@ module.exports = (pathItem, _opts, paths) => {
dups.forEach((dup) => {
// get the index of all names that match dup
const dupKeys = [...allParams.keys()].filter((k) => canonical(allParams[k]) === dup);
// Refer back to the first one - could be path or method
const first = dupKeys[0] < pathParams.length ? `parameters.${dupKeys[0]}`
: `${method}.parameters.${dupKeys[0] - pathParams.length}`;
// Report errors for any others that are method parameters
dupKeys.slice(1).filter((k) => k >= pathParams.length).forEach((key) => {
errors.push({
message: `Duplicate parameter name (ignoring case) with ${first}.`,
message: `Duplicate parameter name (ignoring case): ${dup}.`,
path: [...path, method, 'parameters', key - pathParams.length, 'name'],
});
});
Expand Down
712 changes: 577 additions & 135 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"url": "https://github.com/Azure/azure-api-style-guide"
},
"dependencies": {
"@jest/globals": "^29.7.0",
"@stoplight/spectral-functions": "^1.7.2"
},
"devDependencies": {
Expand Down
61 changes: 46 additions & 15 deletions test/datetime-naming-convention.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { linterForRule } = require('./utils');
require('./matchers');

let linter;

Expand Down Expand Up @@ -160,13 +161,27 @@ test('az-datetime-naming-convention should find errors', () => {
};
return linter.run(myOpenApiDocument).then((results) => {
expect(results.length).toBe(7);
expect(results[0].path.join('.')).toBe('paths./path1.put.parameters.0.schema.properties.foo');
expect(results[1].path.join('.')).toBe('paths./path1.put.responses.200.schema.properties.bar');
expect(results[2].path.join('.')).toBe('paths./path2.put.parameters.0.schema.properties.foo');
expect(results[3].path.join('.')).toBe('paths./path2.put.responses.200.schema.properties.bar');
expect(results[4].path.join('.')).toBe('paths./path3.get.responses.200.schema.items.properties.foo');
expect(results[5].path.join('.')).toBe('paths./path4.post.responses.200.schema.allOf.0.properties.foo');
expect(results[6].path.join('.')).toBe('paths./path4.post.responses.200.schema.allOf.1.properties.bar');
expect(results).toContainMatch({
path: ['paths', '/path1', 'put', 'parameters', '0', 'schema', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path1', 'put', 'responses', '200', 'schema', 'properties', 'bar'],
});
expect(results).toContainMatch({
path: ['paths', '/path2', 'put', 'parameters', '0', 'schema', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path2', 'put', 'responses', '200', 'schema', 'properties', 'bar'],
});
expect(results).toContainMatch({
path: ['paths', '/path3', 'get', 'responses', '200', 'schema', 'items', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path4', 'post', 'responses', '200', 'schema', 'allOf', '0', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path4', 'post', 'responses', '200', 'schema', 'allOf', '1', 'properties', 'bar'],
});
});
});

Expand Down Expand Up @@ -336,14 +351,30 @@ test('az-datetime-naming-convention should find oas3 errors', () => {
};
return linter.run(myOpenApiDocument).then((results) => {
expect(results.length).toBe(8);
expect(results[0].path.join('.')).toBe('paths./path1.put.requestBody.content.application/json.schema.properties.foo');
expect(results[1].path.join('.')).toBe('paths./path1.put.responses.200.content.application/json.schema.properties.bar');
expect(results[2].path.join('.')).toBe('paths./path2.put.requestBody.content.application/json.schema.properties.foo');
expect(results[3].path.join('.')).toBe('paths./path2.put.responses.200.content.application/json.schema.properties.bar');
expect(results[4].path.join('.')).toBe('paths./path3.get.responses.200.content.application/json.schema.anyOf.0.properties.foo');
expect(results[5].path.join('.')).toBe('paths./path3.get.responses.200.content.application/json.schema.anyOf.1.properties.bar');
expect(results[6].path.join('.')).toBe('paths./path4.post.responses.200.content.application/json.schema.oneOf.0.properties.foo');
expect(results[7].path.join('.')).toBe('paths./path4.post.responses.200.content.application/json.schema.oneOf.1.properties.bar');
expect(results).toContainMatch({
path: ['paths', '/path1', 'put', 'requestBody', 'content', 'application/json', 'schema', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path1', 'put', 'responses', '200', 'content', 'application/json', 'schema', 'properties', 'bar'],
});
expect(results).toContainMatch({
path: ['paths', '/path2', 'put', 'requestBody', 'content', 'application/json', 'schema', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path2', 'put', 'responses', '200', 'content', 'application/json', 'schema', 'properties', 'bar'],
});
expect(results).toContainMatch({
path: ['paths', '/path3', 'get', 'responses', '200', 'content', 'application/json', 'schema', 'anyOf', '0', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path3', 'get', 'responses', '200', 'content', 'application/json', 'schema', 'anyOf', '1', 'properties', 'bar'],
});
expect(results).toContainMatch({
path: ['paths', '/path4', 'post', 'responses', '200', 'content', 'application/json', 'schema', 'oneOf', '0', 'properties', 'foo'],
});
expect(results).toContainMatch({
path: ['paths', '/path4', 'post', 'responses', '200', 'content', 'application/json', 'schema', 'oneOf', '1', 'properties', 'bar'],
});
});
});

Expand Down
13 changes: 10 additions & 3 deletions test/header-disallowed.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { linterForRule } = require('./utils');
require('./matchers');

let linter;

Expand Down Expand Up @@ -47,9 +48,15 @@ test('az-header-disallowed should find errors', () => {
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(3);
expect(results[0].path.join('.')).toBe('paths./test1.parameters.0.name');
expect(results[1].path.join('.')).toBe('paths./test1.get.parameters.0.name');
expect(results[2].path.join('.')).toBe('paths./test1.get.parameters.1.name');
expect(results).toContainMatch({
path: ['paths', '/test1', 'parameters', '0', 'name'],
});
expect(results).toContainMatch({
path: ['paths', '/test1', 'get', 'parameters', '0', 'name'],
});
expect(results).toContainMatch({
path: ['paths', '/test1', 'get', 'parameters', '1', 'name'],
});
});
});

Expand Down
23 changes: 23 additions & 0 deletions test/matchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const { expect } = require('@jest/globals');

// Extend jest matchers with a method to check if an array contains an object
// that matches the expected object. Matching means that actual object contains
// all properties of the expected object with the same values.
function toContainMatch(actual, expected) {
if (!Array.isArray(actual)) {
throw new TypeError('Actual value must be an array!');
}

const index = actual.findIndex((item) =>
// eslint-disable-next-line implicit-arrow-linebreak
Object.keys(expected).every((key) => this.equals(item[key], expected[key])));

const pass = index !== -1;
const message = () => `expected ${this.utils.printReceived(actual)} to contain object ${this.utils.printExpected(expected)}`;

return { message, pass };
}

expect.extend({
toContainMatch,
});
38 changes: 29 additions & 9 deletions test/parameter-names-convention.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { linterForRule } = require('./utils');
require('./matchers');

let linter;

Expand Down Expand Up @@ -70,15 +71,34 @@ test('az-parameter-names-convention should find errors', () => {
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(7);
expect(results[0].path.join('.')).toBe('paths./test1/{test-id}.parameters.0.name');
expect(results[1].path.join('.')).toBe('paths./test1/{test-id}.parameters.1.name');
expect(results[2].path.join('.')).toBe('paths./test1/{test-id}.parameters.2.name');
expect(results[3].path.join('.')).toBe('paths./test1/{test-id}.parameters.3.name');
expect(results[3].message).toContain("should not begin with '$' or '@'");
expect(results[4].path.join('.')).toBe('paths./test1/{test-id}.parameters.4.name');
expect(results[4].message).toContain("should not begin with '$' or '@'");
expect(results[5].path.join('.')).toBe('paths./test1/{test-id}.get.parameters.0.name');
expect(results[6].path.join('.')).toBe('paths./test1/{test-id}.get.parameters.1.name');
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'parameters', '0', 'name'],
message: 'Parameter name "test-id" should be camel case.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'parameters', '1', 'name'],
message: 'Parameter name "foo_bar" should be camel case.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'parameters', '2', 'name'],
message: 'header parameter name "fooBar" should be kebab case.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'parameters', '3', 'name'],
message: 'Parameter name "$foo-bar" should not begin with \'$\' or \'@\'.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'parameters', '4', 'name'],
message: 'Parameter name "@foo-bar" should not begin with \'$\' or \'@\'.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'get', 'parameters', '0', 'name'],
message: 'Parameter name "resource-id" should be camel case.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{test-id}', 'get', 'parameters', '1', 'name'],
message: 'Parameter name "$skip" should not begin with \'$\' or \'@\'.',
});
});
});

Expand Down
21 changes: 17 additions & 4 deletions test/parameter-names-unique.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { linterForRule } = require('./utils');
require('./matchers');

let linter;

Expand Down Expand Up @@ -68,10 +69,22 @@ test('az-parameter-names-unique should find errors', () => {
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(4);
expect(results[0].path.join('.')).toBe('paths./test1/{p1}.parameters.1.name');
expect(results[1].path.join('.')).toBe('paths./test1/{p1}.get.parameters.0.name');
expect(results[2].path.join('.')).toBe('paths./test1/{p1}.get.parameters.1.name');
expect(results[3].path.join('.')).toBe('paths./test1/{p1}.get.parameters.3.name');
expect(results).toContainMatch({
path: ['paths', '/test1/{p1}', 'parameters', '1', 'name'],
message: 'Duplicate parameter name (ignoring case): p1.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{p1}', 'get', 'parameters', '0', 'name'],
message: 'Duplicate parameter name (ignoring case): p1.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{p1}', 'get', 'parameters', '1', 'name'],
message: 'Duplicate parameter name (ignoring case): p2.',
});
expect(results).toContainMatch({
path: ['paths', '/test1/{p1}', 'get', 'parameters', '3', 'name'],
message: 'Duplicate parameter name (ignoring case): p3.',
});
});
});

Expand Down
70 changes: 45 additions & 25 deletions test/path-param-schema.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { linterForRule } = require('./utils');
require('./matchers');

let linter;

Expand Down Expand Up @@ -92,15 +93,22 @@ test('az-path-parameter-schema should find errors', () => {
};
return linter.run(oasDoc).then((results) => {
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');
expect(results).toContainMatch({
path: ['paths', '/foo/{p1}', 'parameters', '0', 'type'],
message: 'Path parameter should be defined as type: string.',
});
expect(results).toContainMatch({
path: ['paths', '/bar/{p2}', 'put', 'parameters', '0'],
message: 'Path parameter should specify a maximum length (maxLength) and characters allowed (pattern).',
});
expect(results).toContainMatch({
path: ['paths', '/baz/{p3}/qux/{p4}', 'put', 'parameters', '1'],
message: 'Path parameter should specify characters allowed (pattern).',
});
expect(results).toContainMatch({
path: ['paths', '/foobar/{p5}', 'put', 'parameters', '0', 'maxLength'],
message: 'Path parameter maximum length should be less than 2083',
});
});
});

Expand Down Expand Up @@ -179,13 +187,18 @@ test('az-path-parameter-schema should find errors in patch operations', () => {
};
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./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');
expect(results).toContainMatch({
path: ['paths', '/bar/{p2}', 'patch', 'parameters', '0'],
message: 'Path parameter should specify a maximum length (maxLength) and characters allowed (pattern).',
});
expect(results).toContainMatch({
path: ['paths', '/baz/{p3}/qux/{p4}', 'patch', 'parameters', '1'],
message: 'Path parameter should specify characters allowed (pattern).',
});
expect(results).toContainMatch({
path: ['paths', '/foobar/{p5}', 'patch', 'parameters', '0', 'maxLength'],
message: 'Path parameter maximum length should be less than 2083',
});
});
});

Expand Down Expand Up @@ -416,15 +429,22 @@ 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.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');
expect(results).toContainMatch({
path: ['paths', '/foo/{p1}', 'parameters', '0', 'schema', 'type'],
message: 'Path parameter should be defined as type: string.',
});
expect(results).toContainMatch({
path: ['paths', '/bar/{p2}', 'put', 'parameters', '0', 'schema'],
message: 'Path parameter should specify a maximum length (maxLength) and characters allowed (pattern).',
});
expect(results).toContainMatch({
path: ['paths', '/baz/{p3}/qux/{p4}', 'put', 'parameters', '1', 'schema'],
message: 'Path parameter should specify characters allowed (pattern).',
});
expect(results).toContainMatch({
path: ['paths', '/foobar/{p5}', 'put', 'parameters', '0', 'maxLength'],
message: 'Path parameter maximum length should be less than 2083',
});
});
});

Expand Down
Loading

0 comments on commit 3e2c92f

Please sign in to comment.