@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
469 lines (419 loc) • 14.3 kB
JavaScript
/**
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { integerAttributes } = require('../../src/rules');
const {
makeCopy,
rootDocument,
testRule,
severityCodes,
} = require('../test-utils');
const rule = integerAttributes;
const ruleId = 'ibm-integer-attributes';
const expectedSeverity = severityCodes.error;
// To enable debug logging in the rule function, copy this statement to an it() block:
// LoggerFactory.getInstance().addLoggerSetting(ruleId, 'debug');
// and uncomment this import statement:
// const LoggerFactory = require('../../src/utils/logger-factory');
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('Integer schema is in a not', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'filter',
in: 'query',
schema: {
type: 'string',
not: {
type: 'integer',
},
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Integer schema specifies minimum in oneOf schemas', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'ruleTester',
in: 'query',
schema: {
$ref: '#/components/schemas/RuleTester',
},
},
];
testDocument.components.schemas['RuleTester'] = {
description: 'Tests minimum field within oneOf',
type: 'integer',
format: 'int64',
maximum: 10,
oneOf: [
{
minimum: 0,
},
{
minimum: 1,
},
{
minimum: 2,
},
],
example: 7,
};
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
it('Integer schema specifies fields in allOf schemas', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'ruleTester1',
in: 'query',
schema: {
$ref: '#/components/schemas/RuleTester1',
},
},
{
name: 'ruleTester2',
in: 'query',
schema: {
$ref: '#/components/schemas/RuleTester2',
},
},
];
testDocument.components.schemas['RuleTester1'] = {
description: 'Tests integer fields within allOf',
type: 'integer',
allOf: [
{
minimum: 0,
},
{
maximum: 99,
},
{
format: 'int32',
},
],
example: 98,
};
testDocument.components.schemas['RuleTester2'] = {
description: 'Tests integer fields within allOf',
type: ['integer'],
allOf: [
{
minimum: 1,
},
{
maximum: 38,
},
],
example: 38,
};
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
});
describe('Should yield errors', () => {
it('Integer schema is missing a `maximum` field', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
minimum: 1,
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema is missing a `minimum` field', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
maximum: 720,
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema has invalid format', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
format: 'int128',
minimum: 0,
maximum: 720,
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema is missing a `minimum` field (allOf)', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
allOf: [{ maximum: 10 }, { maximum: 25 }],
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema is missing a `maximum` field (nested oneOf)', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
allOf: [
{
oneOf: [{ minimum: 0 }, { minimum: 3 }],
},
{ minimum: 2 },
],
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema w/invalid format (nested anyOf)', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
maximum: 38,
minimum: 0,
allOf: [
{
anyOf: [{ format: 'int8' }, { format: 'int16' }],
},
{ format: 'int99' },
],
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema w/minimum > maximum', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
allOf: [{ minimum: 10 }, { maximum: 9 }],
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].message).toBe(
`'minimum' cannot be greater than 'maximum'`
);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Invalid integer schema defined at path level', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].parameters = [
{
name: 'integer_param',
in: 'query',
schema: {
type: ['integer'],
minimum: 16,
maximum: 15,
},
},
];
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].message).toBe(
`'minimum' cannot be greater than 'maximum'`
);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema specifies maximum in only SOME oneOf schemas', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'ruleTester',
in: 'query',
schema: {
$ref: '#/components/schemas/RuleTester',
},
},
];
testDocument.components.schemas['RuleTester'] = {
description: 'Tests maximum field within oneOf',
type: 'integer',
format: 'int64',
minimum: 1,
oneOf: [
{
maximum: 38,
},
{
maximum: 42,
},
{
description: 'No maximum field in this schema',
},
],
example: 36,
};
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
const expectedPaths = ['paths./v1/movies.post.parameters.0.schema'];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].message).toBe(
`Integer schemas should define property 'maximum'`
);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Response body integer schema has no keywords', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies/{movie_id}'].get.responses['200'] = {
content: {
'application/json': {
schema: {
properties: {
size: {
type: 'integer',
description: 'no validation',
},
},
},
},
},
};
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(2);
const expectedPath =
'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.size';
const expectedMessages = [
`Integer schemas should define property 'minimum'`,
`Integer schemas should define property 'maximum'`,
];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toBe(expectedMessages[i]);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPath);
}
});
it('Response header integer schema has no keywords', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.components.responses['MovieWithETag'].headers[
'X-IBM-Something'
] = {
schema: {
type: 'integer',
description: 'no validation',
},
};
const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(4);
const expectedPaths = [
'paths./v1/movies/{movie_id}.get.responses.200.headers.X-IBM-Something.schema',
'paths./v1/movies/{movie_id}.get.responses.200.headers.X-IBM-Something.schema',
'paths./v1/movies/{movie_id}.put.responses.200.headers.X-IBM-Something.schema',
'paths./v1/movies/{movie_id}.put.responses.200.headers.X-IBM-Something.schema',
];
const expectedMessages = [
`Integer schemas should define property 'minimum'`,
`Integer schemas should define property 'maximum'`,
`Integer schemas should define property 'minimum'`,
`Integer schemas should define property 'maximum'`,
];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toBe(expectedMessages[i]);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
});
});