UNPKG

@ibm-cloud/openapi-ruleset

Version:

Spectral ruleset for validating IBM Cloud services

198 lines (174 loc) 6.88 kB
/** * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const { LoggerFactory, isCreateOperation, isOperationOfType, getResourceSpecificSiblingPath, getResponseCodes, pathHasMinimallyRepresentedResource, } = require('../utils'); let ruleId; let logger; module.exports = function (operation, _opts, context) { if (!logger) { ruleId = context.rule.name; logger = LoggerFactory.getInstance().getLogger(ruleId); } return responseStatusCodes( operation, context.path, context.documentInventory.resolved ); }; /** * This function performs a few checks on each operation's responses field: * 1. Status code 400 should be used instead of 422. * 2. Status code 303 or 307 should be used instead of 302. * 3. Operation responses should include at least one successful (2xx) status code. * 4. Operation responses should not include status code 101 when successful (2xx) status codes are present. * 5. A 204 response must not have content. * 6. A "create" operation must return either a 201 or a 202 (or a 204, if the corresponding GET request returns a 204). * An operation is considered to be a "create" operation if the operationId starts with "create" * OR it's a POST request and there is a similar path but with a trailing path parameter reference. * 7. A "PUT" operation must return either a 200, 201, or 202 * 8. A "PATCH" operation must return either a 200 or a 202 * 9. If an operation returns status code 202, it should not return any other 2xx status codes. * @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 * @param {*} apidef the resolved API spec * @returns an array containing the violations found or [] if no violations */ function responseStatusCodes(operation, path, apidef) { if (!operation.responses) { return []; } logger.debug( `${ruleId}: checking status codes for operation at location: ${path.join( '.' )}` ); const errors = []; const [statusCodes, successCodes] = getResponseCodes(operation.responses); if (statusCodes.length) { // 1. Check for a 422. if (statusCodes.includes('422')) { errors.push({ message: 'Operation responses should use status code 400 instead of 422 for invalid request payloads', path: [...path, 'responses', '422'], }); } // 2. Check for a 302. if (statusCodes.includes('302')) { errors.push({ message: 'Operation responses should use status code 303 or 307 instead of 302', path: [...path, 'responses', '302'], }); } // 3. Make sure we have at least one success code. if (!successCodes.length && !('101' in operation.responses)) { errors.push({ message: 'Operation responses should include at least one success status code (2xx)', path: [...path, 'responses'], }); } // 4. Make sure we don't have a 101 status code AND success codes. if (successCodes.length && '101' in operation.responses) { errors.push({ message: 'Operation responses should not include status code 101 when success status codes (2xx) are present', path: [...path, 'responses', '101'], }); } // 5. Make sure a 204 response (if present) doesn't have content. const response204 = operation.responses['204']; if (response204 && response204.content) { errors.push({ message: 'A 204 response must not include a response body; use a different status code for responses with content', path: [...path, 'responses', '204', 'content'], }); } // 6. For a 'create' operation (a POST operation or operationId starts with "create"), // make sure that there's either a 201 or a 202 status code - the exception is that a // 'create' is allowed to define a 204 if the corresponding GET request also defines // a 204, indicating there is no canonical body representation. if (isCreateOperation(operation, path, apidef)) { // If the create has a 204, and the GET has a 204, it's okay - no error if ( !['201', '202'].find(code => successCodes.includes(code)) && !(successCodes.includes('204') && !hasBodyRepresentation(path, apidef)) ) { errors.push({ message: `A 201 or 202 status code should be returned by a 'create' operation`, path: [...path, 'responses'], }); } } // 7. A "PUT" operation must return either a 200, 201, or 202. // Exception: we'll also allow a 204 status code if there's a corresponding GET // operation that also returns a 204. // Note: we've already checked for lack of any success codes - no need to double-report that. if (isOperationOfType('put', path) && successCodes.length) { if ( !['200', '201', '202'].find(code => successCodes.includes(code)) && !( successCodes.includes('204') && pathHasMinimallyRepresentedResource(path.at(-2), apidef) ) ) { errors.push({ message: `PUT operations should return a 200, 201, or 202 status code`, path: [...path, 'responses'], }); } } // 8. A "PATCH" operation must return either a 200 or a 202 // Note: we've already checked for lack of any success codes - no need to double-report that. if (isOperationOfType('patch', path) && successCodes.length) { if (!['200', '202'].find(code => successCodes.includes(code))) { errors.push({ message: `PATCH operations should return a 200 or 202 status code`, path: [...path, 'responses'], }); } } // 9. If an operation returns 202, it should not return any other success status codes. if (successCodes.includes('202') && successCodes.length > 1) { errors.push({ message: 'An operation that returns a 202 status code should not return any other 2xx status codes', path: [...path, 'responses'], }); } } if (errors.length) { logger.debug( `${ruleId}: found these errors:\n${JSON.stringify(errors, null, 2)}` ); } else { logger.debug(`${ruleId}: PASSED!`); } return errors; } function hasBodyRepresentation(path, apidef) { const resourceSpecificPath = getResourceSpecificSiblingPath( path.at(-2), apidef ); logger.debug( `${ruleId}: calculated resource-specific path to be "${resourceSpecificPath}"` ); if (!resourceSpecificPath || !apidef.paths[resourceSpecificPath]) { logger.debug( `${ruleId}: resource-specific path "${resourceSpecificPath}" does not exist` ); return; } return !pathHasMinimallyRepresentedResource(resourceSpecificPath, apidef); }