Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import testRule from './__helpers__/testRule';
import { DiagnosticSeverity } from '@stoplight/types';

const components = {
responses: {
badRequest: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
description: 'Bad Request.',
},
notFound: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
description: 'Not Found.',
},
internalServerError: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ApiError',
},
},
},
description: 'Internal Error.',
},
},
schemas: {
ApiError: {
type: 'object',
properties: {
error: {
type: 'string',
},
},
},
},
};

testRule('xgen-IPA-114-error-responses-refer-to-api-error', [
{
name: 'valid error responses with ApiError schema',
document: {
paths: {
'/resources': {
get: {
responses: {
400: {
$ref: '#/components/responses/badRequest',
},
404: {
$ref: '#/components/responses/notFound',
},
500: {
$ref: '#/components/responses/internalServerError',
},
},
},
},
},
components: components,
},
errors: [],
},
{
name: 'invalid error responses missing schema',
document: {
paths: {
'/resources': {
get: {
responses: {
400: {
content: {
'application/json': {},
},
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-114-error-responses-refer-to-api-error',
message: '400 response must define a schema referencing ApiError.',
path: ['paths', '/resources', 'get', 'responses', '400', 'content', 'application/json'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid error responses missing content',
document: {
paths: {
'/resources': {
get: {
responses: {
400: {},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-114-error-responses-refer-to-api-error',
message: '400 response must define content with ApiError schema reference.',
path: ['paths', '/resources', 'get', 'responses', '400'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid error responses referencing wrong schema',
document: {
paths: {
'/resources': {
get: {
responses: {
500: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
},
},
},
},
components: {
schemas: {
Error: {
type: 'object',
properties: {
message: {
type: 'string',
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-114-error-responses-refer-to-api-error',
message: '500 response must reference ApiError schema.',
path: ['paths', '/resources', 'get', 'responses', '500', 'content', 'application/json'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid error responses with inline schema',
document: {
paths: {
'/resources': {
get: {
responses: {
404: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
error: {
type: 'string',
},
},
},
},
},
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-114-error-responses-refer-to-api-error',
message: '404 response must reference ApiError schema.',
path: ['paths', '/resources', 'get', 'responses', '404', 'content', 'application/json'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'error responses with exception',
document: {
paths: {
'/resources': {
get: {
responses: {
400: {
content: {
'application/json': {
schema: {
type: 'object',
},
'x-xgen-IPA-exception': {
'xgen-IPA-114-error-responses-refer-to-api-error': 'Reason',
},
},
},
},
500: {
'x-xgen-IPA-exception': {
'xgen-IPA-114-error-responses-refer-to-api-error': 'Reason',
},
},
},
},
},
},
},
errors: [],
},
]);
1 change: 1 addition & 0 deletions tools/spectral/ipa/ipa-spectral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extends:
- ./rulesets/IPA-110.yaml
- ./rulesets/IPA-112.yaml
- ./rulesets/IPA-113.yaml
- ./rulesets/IPA-114.yaml
- ./rulesets/IPA-117.yaml
- ./rulesets/IPA-123.yaml
- ./rulesets/IPA-125.yaml
Expand Down
18 changes: 18 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-114.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# IPA-114: Errors
# http://go/ipa/114

functions:
- IPA114ErrorResponsesReferToApiError

rules:
xgen-IPA-114-error-responses-refer-to-api-error:
description: |
APIs must return ApiError when errors occur

##### Implementation details
This rule checks that all 4xx and 5xx error responses reference the ApiError schema.
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-114-error-responses-refer-to-api-error'
severity: warn
given: '$.paths[*][*].responses[?(@property.match(/^[45]\d\d$/))]'
then:
function: 'IPA114ErrorResponsesReferToApiError'
14 changes: 14 additions & 0 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@ Rule checks for the following conditions:



### IPA-114

Rules are based on [http://go/ipa/IPA-114](http://go/ipa/IPA-114).

#### xgen-IPA-114-error-responses-refer-to-api-error

![warn](https://img.shields.io/badge/warning-yellow)
APIs must return ApiError when errors occur

##### Implementation details
This rule checks that all 4xx and 5xx error responses reference the ApiError schema.



### IPA-117

Rules are based on [http://go/ipa/IPA-117](http://go/ipa/IPA-117).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { hasException } from './utils/exceptions.js';
import {
collectAdoption,
collectAndReturnViolation,
collectException,
handleInternalError,
} from './utils/collectionUtils.js';
import { resolveObject } from './utils/componentUtils.js';
import { getSchemaNameFromRef } from './utils/methodUtils.js';

const RULE_NAME = 'xgen-IPA-114-error-responses-refer-to-api-error';

/**
* Verifies that 4xx and 5xx responses reference the ApiError schema
*
* @param {object} input - The response object to check
* @param {object} _ - Rule options (unused)
* @param {object} context - The context object containing path and document information
*/
export default (input, _, { path, documentInventory }) => {
const oas = documentInventory.unresolved;
const apiResponseObject = resolveObject(oas, path);
const errorCode = path[path.length - 1];

// Check for exception at response level
if (hasException(apiResponseObject, RULE_NAME)) {
collectException(apiResponseObject, RULE_NAME, path);
return;
}

const errors = checkViolationsAndReturnErrors(apiResponseObject, oas, path, errorCode);
if (errors.length !== 0) {
return collectAndReturnViolation(path, RULE_NAME, errors);
}

collectAdoption(path, RULE_NAME);
};

function checkViolationsAndReturnErrors(apiResponseObject, oas, path, errorCode) {
try {
const errors = [];
let content;

if (apiResponseObject.content) {
content = apiResponseObject.content;
} else if (apiResponseObject.$ref) {
const schemaName = getSchemaNameFromRef(apiResponseObject.$ref);
const responseSchema = resolveObject(oas, ['components', 'responses', schemaName]);
content = responseSchema.content;
} else {
return [{ path, message: `${errorCode} response must define content with ApiError schema reference.` }];
}

for (const [mediaType, mediaTypeObj] of Object.entries(content)) {
if (!mediaType.endsWith('json')) {
continue;
}

if (hasException(mediaTypeObj, RULE_NAME)) {
collectException(mediaTypeObj, RULE_NAME, [...path, 'content', mediaType]);
continue;
}

const contentPath = [...path, 'content', mediaType];

// Check if schema exists
if (!mediaTypeObj.schema) {
errors.push({
path: contentPath,
message: `${errorCode} response must define a schema referencing ApiError.`,
});
continue;
}

// Check if schema references ApiError
const schema = mediaTypeObj.schema;

if (!schema.$ref || getSchemaNameFromRef(schema.$ref) !== 'ApiError') {
errors.push({
path: contentPath,
message: `${errorCode} response must reference ApiError schema.`,
});
}
}
return errors;
} catch (e) {
handleInternalError(RULE_NAME, path, e);
}
}
Loading