@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
325 lines (273 loc) • 10.3 kB
JavaScript
/**
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { parameterCasingConvention } = require('../../src/rules');
const {
makeCopy,
rootDocument,
testRule,
severityCodes,
} = require('../test-utils');
const rule = parameterCasingConvention;
const ruleId = 'ibm-parameter-casing-convention';
const expectedSeverity = severityCodes.error;
const expectedMsgNoName = 'Parameters must have a name';
const expectedMsgNoIn = "Parameters must have a valid 'in' value";
const expectedMsgQuery = 'Query parameter names must be snake case';
const expectedMsgPath = 'Path parameter names must be snake case';
const expectedMsgHeader =
'Header parameter names must be kebab-separated pascal case';
describe(`Spectral rule: ${ruleId}`, () => {
describe('Should not yield errors', () => {
it('Clean spec', async () => {
const results = await testRule(ruleId, rule, rootDocument);
expect(results).toHaveLength(0);
});
it('Query parameter with correct case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
name: 'movie_rating',
required: false,
in: 'query',
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Query parameter with "." in name', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
name: 'movie_rating.value',
required: false,
in: 'query',
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Path parameter with correct case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].parameters = [
{
description: 'An optional movie rating to filter on.',
name: 'movie_rating',
required: false,
in: 'path',
schema: {
type: 'string',
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Header parameter with correct case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
name: 'X-Movie-Rating',
required: false,
in: 'header',
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
// Suite of standard header parameters
it.each([
'Content-Type',
'Accept',
'WWW-Authenticate',
'ETag',
'TE',
'X-Request-ID',
'X-RateLimit-Limit',
])('Standard header parameter: %s', async header => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'Some standard header',
name: header,
required: false,
in: 'header',
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Deprecated query parameter with incorrect case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
name: 'movieRating',
required: false,
in: 'query',
deprecated: true,
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Deprecated path parameter with incorrect case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
name: 'movieRating',
required: false,
in: 'path',
deprecated: true,
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Deprecated header parameter with incorrect case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
name: 'movie_rating',
required: false,
in: 'header',
deprecated: true,
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Should handle potential ReDoS patterns efficiently', async () => {
const testDocument = makeCopy(rootDocument);
// Pattern that would cause catastrophic backtracking in old regex
// Old pattern: /^[A-Z]+[a-z0-9]*-*([A-Z]+[a-z0-9]*-*)*$/
// This input would cause exponential time complexity
const maliciousHeader = 'A' + 'A-'.repeat(50);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'Header with ReDoS attack pattern',
name: maliciousHeader,
required: false,
in: 'header',
schema: {
type: 'string',
},
});
const startTime = Date.now();
const results = await testRule(ruleId, rule, testDocument);
const duration = Date.now() - startTime;
// Should complete quickly (well under 500ms) with fixed regex
// Note: Includes test overhead; actual regex execution is much faster
expect(duration).toBeLessThan(500);
// Should still validate correctly and reject the malformed header
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgHeader);
});
});
describe('Should yield errors', () => {
it('Parameter with no name', async () => {
const testDocument = makeCopy(rootDocument);
delete testDocument.components.parameters.DrinkIdParam.name;
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgNoName);
expect(results[0].severity).toBe(expectedSeverity);
expect(results[0].path.join('.')).toBe(
'paths./v1/drinks/{drink_id}.parameters.0'
);
});
it('Parameter with no "in" value', async () => {
const testDocument = makeCopy(rootDocument);
delete testDocument.components.parameters.DrinkIdParam.in;
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgNoIn);
expect(results[0].severity).toBe(expectedSeverity);
expect(results[0].path.join('.')).toBe(
'paths./v1/drinks/{drink_id}.parameters.0'
);
});
it('Query parameter with incorrect case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
required: false,
name: 'moveRating',
in: 'query',
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgQuery);
expect(results[0].severity).toBe(expectedSeverity);
expect(results[0].path.join('.')).toBe(
'paths./v1/movies.get.parameters.3'
);
});
it('Path parameter with incorrect case', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.components.parameters.DrinkIdParam.name = 'drinkId';
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgPath);
expect(results[0].severity).toBe(expectedSeverity);
expect(results[0].path.join('.')).toBe(
'paths./v1/drinks/{drink_id}.parameters.0'
);
});
it('Path parameter with "." in name', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.components.parameters.DrinkIdParam.name = 'drinkId.value';
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgPath);
expect(results[0].severity).toBe(expectedSeverity);
expect(results[0].path.join('.')).toBe(
'paths./v1/drinks/{drink_id}.parameters.0'
);
});
it.each(['movieRating', 'movierating', 'X-Movie-rating', 'x-moVie-Rating'])(
'Header parameter with incorrect case: %s',
async header => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].get.parameters.push({
description: 'An optional movie rating to filter on.',
required: false,
name: header,
in: 'header',
schema: {
type: 'string',
},
});
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ruleId);
expect(results[0].message).toBe(expectedMsgHeader);
expect(results[0].severity).toBe(expectedSeverity);
expect(results[0].path.join('.')).toBe(
'paths./v1/movies.get.parameters.3'
);
}
);
});
});