@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
136 lines (117 loc) • 4.76 kB
JavaScript
/**
* Copyright 2017 - 2025 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { getResolvedSpec } = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory, getResponseCodes } = require('../utils');
let ruleId;
let logger;
module.exports = function (operation, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return redirectResponseBody(
operation,
context.path,
getResolvedSpec(context)
);
};
/**
* This function performs a few checks on each operation's response field:
* 1. Only status codes 301, 302, 305, 307 should include a response body for 30x response codes.
* 2. Every response body for status codes 30x should contain a value for code that matches on of the following values:
* forwarded, resolved, moved, remote_region, remote_account, version_mismatch.
* 3. Every response body for status codes 30x should include "code" property.
* 4. Every response body for status codes 30x should include "message" property.
* 5. Every response body for status codes 30x should include "target" property.
* @param {*} operation an operation within the API definition.
* @param {*} path the array of path segments indicating the "location" of the operation within the API definition.
*/
function redirectResponseBody(operation, path) {
if (!operation.responses) {
return [];
}
logger.debug(
`${ruleId}: checking response bodies for operation at location: ${path.join(
'.'
)}`
);
const errors = [];
const [statusCodes] = getResponseCodes(operation.responses);
if (statusCodes.length) {
const responseCodesWithBody = ['301', '302', '305', '307'];
const redirectCodes = [
'forwarded',
'resolved',
'moved',
'remote_region',
'remote_account',
'version_mismatch',
];
const redirectResponses = statusCodes.filter(code => code.startsWith('3'));
if (redirectResponses.length) {
const responseCodes = redirectResponses.filter(
code => !responseCodesWithBody.includes(code)
);
// 1. Only status codes 301, 302, 305, 307 should include a response body for 30x response codes.
responseCodes.forEach(responseCode => {
const response = operation.responses[responseCode];
if (response && response.content) {
errors.push({
message:
'Only a 301, 302, 305, 307 response should include a response body',
path: [...path, 'responses'],
});
}
});
responseCodesWithBody.forEach(code => {
const response = operation.responses[code];
if (response && response.content) {
const applicationJson = response.content['application/json'];
if (!applicationJson) return;
const redirectCode = applicationJson['code'];
const message = applicationJson['message'];
const target = applicationJson['target'];
// 2. Every response body for status codes 30x should contain a value for code that matches one of the following values:
// forwarded, resolved, moved, remote_region, remote_account, version_mismatch.
// 3. Every response body for status codes 30x should include "code" property.
if (redirectCode) {
for (let i = 0; i < redirectCodes.length; i++) {
if (redirectCode == redirectCodes[i]) break;
else if (i === redirectCodes.length - 1) {
errors.push({
message: `Redirect code should match one of the following: ${redirectCodes.join(
' or '
)}`,
path: [...path, 'responses'],
});
}
}
} else {
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "code" field',
path: [...path, 'responses'],
});
}
// 4. Every response body for status codes 30x should include "message" property.
if (!message)
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "message" field',
path: [...path, 'responses'],
});
// 5. Every response body for status codes 30x should include "target" property.
if (!target)
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "target" field',
path: [...path, 'responses'],
});
}
});
}
}
return errors;
}