diff --git a/tools/spectral/ipa/__tests__/IPA114ApiErrorHasBadRequestDetail.test.js b/tools/spectral/ipa/__tests__/IPA114ApiErrorHasBadRequestDetail.test.js new file mode 100644 index 0000000000..ea853a382b --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA114ApiErrorHasBadRequestDetail.test.js @@ -0,0 +1,168 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const validDocument = { + components: { + schemas: { + ApiError: { + properties: { + badRequestDetail: { + $ref: '#/components/schemas/BadRequestDetail', + }, + detail: { type: 'string' }, + }, + }, + BadRequestDetail: { + properties: { + fields: { + type: 'array', + items: { + $ref: '#/components/schemas/FieldViolation', + }, + }, + }, + }, + FieldViolation: { + properties: { + description: { type: 'string' }, + field: { type: 'string' }, + }, + required: ['description', 'field'], + }, + }, + }, +}; + +testRule('xgen-IPA-114-api-error-has-bad-request-detail', [ + { + name: 'valid ApiError schema passes validation', + document: validDocument, + errors: [], + }, + { + name: 'missing properties in ApiError fails', + document: { + components: { + schemas: { + ApiError: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-114-api-error-has-bad-request-detail', + message: 'ApiError schema must have badRequestDetail field.', + path: ['components', 'schemas', 'ApiError'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'missing badRequestDetail field fails', + document: { + components: { + schemas: { + ApiError: { + properties: { + detail: { type: 'string' }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-114-api-error-has-bad-request-detail', + message: 'ApiError schema must have badRequestDetail field.', + path: ['components', 'schemas', 'ApiError'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'badRequestDetail without fields property fails', + document: { + components: { + schemas: { + ApiError: { + properties: { + badRequestDetail: { + properties: { + someOtherProperty: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-114-api-error-has-bad-request-detail', + message: 'badRequestDetail must include an array of fields.', + path: ['components', 'schemas', 'ApiError'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'fields not being an array fails', + document: { + components: { + schemas: { + ApiError: { + properties: { + badRequestDetail: { + properties: { + fields: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-114-api-error-has-bad-request-detail', + message: 'badRequestDetail must include an array of fields.', + path: ['components', 'schemas', 'ApiError'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'missing description or field properties fails', + document: { + components: { + schemas: { + ApiError: { + properties: { + badRequestDetail: { + properties: { + fields: { + type: 'array', + properties: { + // Missing description and field properties + otherProperty: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-114-api-error-has-bad-request-detail', + message: 'Each field must include description and field properties.', + path: ['components', 'schemas', 'ApiError'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-114.yaml b/tools/spectral/ipa/rulesets/IPA-114.yaml index 1fe9d56b58..e6fc26815f 100644 --- a/tools/spectral/ipa/rulesets/IPA-114.yaml +++ b/tools/spectral/ipa/rulesets/IPA-114.yaml @@ -3,6 +3,7 @@ functions: - IPA114ErrorResponsesReferToApiError + - IPA114ApiErrorHasBadRequestDetail rules: xgen-IPA-114-error-responses-refer-to-api-error: @@ -16,3 +17,18 @@ rules: given: '$.paths[*][*].responses[?(@property.match(/^[45]\d\d$/))]' then: function: 'IPA114ErrorResponsesReferToApiError' + xgen-IPA-114-api-error-has-bad-request-detail: + description: | + ApiError schema should have badRequestDetail field with proper structure. + + ##### Implementation details + Rule checks that: + - ApiError schema has badRequestDetail field + - badRequestDetail must include an array of fields + - Each field must include description and field properties + - This rule does not allow exceptions + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-114-api-error-has-bad-request-detail' + severity: warn + given: $.components.schemas.ApiError + then: + function: 'IPA114ApiErrorHasBadRequestDetail' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index e5036fde09..48d8505100 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -615,6 +615,18 @@ APIs must return ApiError when errors occur ##### Implementation details This rule checks that all 4xx and 5xx error responses reference the ApiError schema. +#### xgen-IPA-114-api-error-has-bad-request-detail + + ![warn](https://img.shields.io/badge/warning-yellow) +ApiError schema should have badRequestDetail field with proper structure. + +##### Implementation details +Rule checks that: +- ApiError schema has badRequestDetail field +- badRequestDetail must include an array of fields +- Each field must include description and field properties +- This rule does not allow exceptions + ### IPA-117 diff --git a/tools/spectral/ipa/rulesets/functions/IPA114ApiErrorHasBadRequestDetail.js b/tools/spectral/ipa/rulesets/functions/IPA114ApiErrorHasBadRequestDetail.js new file mode 100644 index 0000000000..b1ac069135 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA114ApiErrorHasBadRequestDetail.js @@ -0,0 +1,68 @@ +import { collectAdoption, collectAndReturnViolation, handleInternalError } from './utils/collectionUtils.js'; + +const RULE_NAME = 'xgen-IPA-114-api-error-has-bad-request-detail'; + +/** + * Verifies that ApiError schema has badRequestDetail field with proper structure + * + * @param {object} input - The ApiError schema object + * @param {object} _ - Rule options (unused) + * @param {object} context - The context object containing path and document information + */ +export default (input, _, { path, documentInventory }) => { + const errors = checkViolationsAndReturnErrors(input, documentInventory, path); + if (errors.length > 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; + +/** + * Check for violations in ApiError schema structure + * + * @param {object} apiErrorSchema - The ApiError schema object to validate + * @param {object} documentInventory - Contains document information + * @param {Array} path - Path to the schema in the document + * @returns {Array} - Array of error objects + */ +function checkViolationsAndReturnErrors(apiErrorSchema, documentInventory, path) { + try { + // ApiError should have badRequestDetail property + if (!apiErrorSchema.properties?.badRequestDetail) { + return [ + { + path, + message: 'ApiError schema must have badRequestDetail field.', + }, + ]; + } + + //badRequestDetail must include an array of fields + const badRequestDetail = apiErrorSchema.properties.badRequestDetail; + if (badRequestDetail.properties?.fields?.type !== 'array') { + return [ + { + path, + message: 'badRequestDetail must include an array of fields.', + }, + ]; + } + + //Each field must include description and field properties + const fields = badRequestDetail.properties.fields; + if (!fields.items?.properties?.description && !fields.items?.properties?.field) { + return [ + { + path, + message: 'Each field must include description and field properties.', + }, + ]; + } + + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + return [{ path, message: `Internal error during validation: ${e.message}` }]; + } +}