@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
746 lines (697 loc) • 20.4 kB
JavaScript
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const {
getSuccessResponseSchemaForOperation,
getRequestBodySchemaForOperation,
getCanonicalSchemaForPath,
} = require('../../src/utils');
describe('Schema finding utility functions', () => {
describe('getSuccessResponseSchemaForOperation()', () => {
const pathToOp = 'paths./v1/resources.get';
it('should return an empty object if operation is undefined', () => {
const operation = undefined;
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should return an empty object if operation has no responses', () => {
const operation = {};
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should return an empty object if operation has no success responses', () => {
const operation = {
responses: {
400: {},
},
};
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should return an empty object if operation success response has no content', () => {
const operation = {
responses: {
200: {},
},
};
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should return an empty object if operation success response has no json content', () => {
const operation = {
responses: {
200: {
content: {
'application/pdf': {},
},
},
},
};
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should return an empty object if operation success response content is not an object', () => {
const operation = {
responses: {
200: {
content: {
'application/json': 42,
},
},
},
};
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should return an empty object if operation success response content has no schema', () => {
const operation = {
responses: {
200: {
content: {
'application/json': {},
},
},
},
};
expect(getSuccessResponseSchemaForOperation(operation, pathToOp)).toEqual(
{}
);
});
it('should prioritize 200 responses over other success responses', () => {
const schema = {
type: 'object',
};
const operation = {
responses: {
200: {
content: {
'application/json': {
schema,
},
},
},
202: {
content: {
'application/json': {
schema: {
type: 'object',
description: 'for 202s',
},
},
},
},
},
};
const { schemaObject, schemaPath } = getSuccessResponseSchemaForOperation(
operation,
pathToOp
);
expect(schemaObject).toEqual(schema);
expect(schemaPath).toEqual(
`${pathToOp}.responses.200.content.application/json.schema`
);
});
it('should use a non-200 success response if there is no 200', () => {
const schema = {
type: 'object',
};
const operation = {
responses: {
202: {
content: {
'application/json; charset=utf-8': {
schema,
},
},
},
},
};
const { schemaObject, schemaPath } = getSuccessResponseSchemaForOperation(
operation,
pathToOp
);
expect(schemaObject).toEqual(schema);
expect(schemaPath).toEqual(
`${pathToOp}.responses.202.content.application/json; charset=utf-8.schema`
);
});
});
describe('getRequestBodySchemaForOperation()', () => {
const pathToPostOp = 'paths./v1/resources.post';
const pathToPatchOp = 'paths./v1/resources.patch';
it('should return empty object if operation is undefined', () => {
const operation = undefined;
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if operation has no request body', () => {
const operation = {};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if operation request body has no content', () => {
const operation = {
requestBody: {},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if operation request body content is not an object', () => {
const operation = {
requestBody: {
content: 42,
},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if operation request body content is not json', () => {
const operation = {
requestBody: {
content: {
'application/pdf': {},
},
},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if operation request body json content is not an object', () => {
const operation = {
requestBody: {
content: {
'application/json': 42,
},
},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if operation request body json content does not have a schema', () => {
const operation = {
requestBody: {
content: {
'application/json': {},
},
},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if non-patch operation uses json patch content', () => {
const operation = {
requestBody: {
content: {
'application/json-patch+json': {
schema: {
type: 'object',
},
},
},
},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if non-patch operation uses merge patch content', () => {
const operation = {
requestBody: {
content: {
'application/merge-patch+json': {
schema: {
type: 'object',
},
},
},
},
};
expect(getRequestBodySchemaForOperation(operation, pathToPostOp)).toEqual(
{}
);
});
it('should return empty object if patch operation uses non-patch content', () => {
const operation = {
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
},
},
},
},
};
expect(
getRequestBodySchemaForOperation(operation, pathToPatchOp)
).toEqual({});
});
it('should return schema info for json content on non-patch operation', () => {
const schema = {
type: 'object',
};
const operation = {
requestBody: {
content: {
'application/json': {
schema,
},
},
},
};
const { schemaObject, schemaPath } = getRequestBodySchemaForOperation(
operation,
pathToPostOp
);
expect(schemaObject).toEqual(schema);
expect(schemaPath).toEqual(
`${pathToPostOp}.requestBody.content.application/json.schema`
);
});
it('should return schema info for json patch content on patch operation', () => {
const schema = {
type: 'object',
};
const operation = {
requestBody: {
content: {
'application/json-patch+json': {
schema,
},
},
},
};
const { schemaObject, schemaPath } = getRequestBodySchemaForOperation(
operation,
pathToPatchOp
);
expect(schemaObject).toEqual(schema);
expect(schemaPath).toEqual(
`${pathToPatchOp}.requestBody.content.application/json-patch+json.schema`
);
});
it('should return schema info for merge patch content on patch operation', () => {
const schema = {
type: 'object',
};
const operation = {
requestBody: {
content: {
'application/merge-patch+json': {
schema,
},
},
},
};
const { schemaObject, schemaPath } = getRequestBodySchemaForOperation(
operation,
pathToPatchOp
);
expect(schemaObject).toEqual(schema);
expect(schemaPath).toEqual(
`${pathToPatchOp}.requestBody.content.application/merge-patch+json.schema`
);
});
});
describe('getCanonicalSchemaForPath()', () => {
const mockLogger = jest.fn();
const logInfo = {
logger: { debug: mockLogger },
ruleId: 'test',
};
afterEach(() => {
mockLogger.mockClear();
});
it('should return empty object if path is not defined', () => {
const path = undefined;
const apidef = getMockApiDef();
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
expect(result).toEqual({});
});
it('should return empty object if path is not a string', () => {
const path = 42;
const apidef = getMockApiDef();
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
expect(result).toEqual({});
});
it('should return empty object if apidef is not defined', () => {
const path = '/v1/resources/{id}';
const apidef = undefined;
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
expect(result).toEqual({});
});
it('should return empty object if apidef is not an object', () => {
const path = '/v1/resources/{id}';
const apidef = 'api';
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
expect(result).toEqual({});
});
it('should return empty object if apidef paths field is not an object', () => {
const path = '/v1/resources/{id}';
const apidef = { paths: 42 };
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
expect(result).toEqual({});
});
it('should return empty object if apidef is not defined', () => {
const path = '/v1/resources/{id}';
const apidef = getMockApiDef();
const result = getCanonicalSchemaForPath(
path,
apidef,
undefined,
logInfo
);
expect(result).toEqual({});
});
it('should return empty object if apidef is not an object', () => {
const path = '/v1/resources/{id}';
const apidef = getMockApiDef();
const result = getCanonicalSchemaForPath(path, apidef, 42, logInfo);
expect(result).toEqual({});
});
it('should return empty object if operation has no success response', () => {
const path = '/v1/resources/{id}';
const apidef = {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
400: {},
},
},
},
},
};
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
expect(result).toEqual({});
expect(mockLogger.mock.calls[0][0]).toBe(
'test: found the path to the canonical schema to be undefined'
);
expect(mockLogger.mock.calls[1][0]).toBe(
'test: found the name of the canonical schema to be undefined'
);
});
it('should return undefined schema and name if schema name is not found in the refs map', () => {
const schema = {
type: 'object',
};
const path = '/v1/resources/{id}';
const apidef = {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
200: {
content: {
'application/json': {
schema,
},
},
},
},
},
},
},
components: {
schemas: {
Resource: schema,
},
},
};
const result = getCanonicalSchemaForPath(
path,
apidef,
{ 'fake.path': 'FakeResource' },
logInfo
);
const { canonicalSchema, canonicalSchemaName, canonicalSchemaPath } =
result;
expect(canonicalSchema).toEqual(undefined);
expect(canonicalSchemaName).toBe(undefined);
expect(canonicalSchemaPath).toBe(
'paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[0][0]).toBe(
'test: found the path to the canonical schema to be paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[1][0]).toBe(
'test: found the name of the canonical schema to be undefined'
);
});
it('should return undefined schema if there are no components', () => {
const schema = {
type: 'object',
};
const path = '/v1/resources/{id}';
const apidef = {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
200: {
content: {
'application/json': {
schema,
},
},
},
},
},
},
},
};
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
const { canonicalSchema, canonicalSchemaName, canonicalSchemaPath } =
result;
expect(canonicalSchema).toEqual(undefined);
expect(canonicalSchemaName).toBe('Resource');
expect(canonicalSchemaPath).toBe(
'paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[0][0]).toBe(
'test: found the path to the canonical schema to be paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[1][0]).toBe(
'test: found the name of the canonical schema to be Resource'
);
});
it('should return undefined schema if there are no schemas in components', () => {
const schema = {
type: 'object',
};
const path = '/v1/resources/{id}';
const apidef = {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
200: {
content: {
'application/json': {
schema,
},
},
},
},
},
},
},
components: {},
};
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
const { canonicalSchema, canonicalSchemaName, canonicalSchemaPath } =
result;
expect(canonicalSchema).toEqual(undefined);
expect(canonicalSchemaName).toBe('Resource');
expect(canonicalSchemaPath).toBe(
'paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[0][0]).toBe(
'test: found the path to the canonical schema to be paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[1][0]).toBe(
'test: found the name of the canonical schema to be Resource'
);
});
it('should return undefined schema if resource is not in schemas', () => {
const schema = {
type: 'object',
};
const path = '/v1/resources/{id}';
const apidef = {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
200: {
content: {
'application/json': {
schema,
},
},
},
},
},
},
},
components: {
schemas: {},
},
};
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
const { canonicalSchema, canonicalSchemaName, canonicalSchemaPath } =
result;
expect(canonicalSchema).toEqual(undefined);
expect(canonicalSchemaName).toBe('Resource');
expect(canonicalSchemaPath).toBe(
'paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[0][0]).toBe(
'test: found the path to the canonical schema to be paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[1][0]).toBe(
'test: found the name of the canonical schema to be Resource'
);
});
it('should find the canonical schema and log the process', () => {
const schema = {
type: 'object',
};
const path = '/v1/resources/{id}';
const apidef = {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
200: {
content: {
'application/json': {
schema,
},
},
},
},
},
},
},
components: {
schemas: {
Resource: schema,
},
},
};
const result = getCanonicalSchemaForPath(
path,
apidef,
getPathToRefsMap(),
logInfo
);
const { canonicalSchema, canonicalSchemaName, canonicalSchemaPath } =
result;
expect(canonicalSchema).toEqual(schema);
expect(canonicalSchemaName).toBe('Resource');
expect(canonicalSchemaPath).toBe(
'paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[0][0]).toBe(
'test: found the path to the canonical schema to be paths./v1/resources/{id}.get.responses.200.content.application/json.schema'
);
expect(mockLogger.mock.calls[1][0]).toBe(
'test: found the name of the canonical schema to be Resource'
);
});
});
});
// Used for negative tests, to make sure that problems with the API definition
// aren't the cause of the test returning an empty object.
function getMockApiDef() {
return {
paths: {
'/v1/resources/{id}': {
get: {
responses: {
200: {
content: {
'application/json': {
type: 'object',
},
},
},
},
},
},
},
components: {
schemas: {
Resource: {
type: 'object',
},
},
},
};
}
function getPathToRefsMap() {
return {
'paths./v1/resources/{id}.get.responses.200.content.application/json.schema':
'components.schemas.Resource',
};
}