loopback-swagger
Version:
Integration between LoopBack and Swagger API specs
605 lines (549 loc) • 18.4 kB
JavaScript
// Copyright IBM Corp. 2015,2019. All Rights Reserved.
// Node module: loopback-swagger
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var routeHelper = require('../../lib/specgen/route-helper');
var TypeRegistry = require('../../lib/specgen/type-registry');
var expect = require('chai').expect;
var _defaults = require('lodash').defaults;
var loopback = require('loopback');
describe('route-helper', function() {
it('returns "object" when a route has multiple return values', function() {
const TestModel = loopback.createModel('TestModel', {street: String});
const remotingSpec = {
returns: [
{arg: 'max', type: 'number'},
{arg: 'min', type: 'number'},
{name: 'avg', type: 'number'},
{name: 'str', type: String},
{name: 'strArray', type: [String]},
{name: 'testModel', type: TestModel},
{name: 'testModelStr', type: 'TestModel'},
{name: 'testModelArray', type: [TestModel]},
{name: 'testModelArrayArray', type: [[TestModel]]},
{name: 'unknownModel', type: 'UnknownModel'},
{name: 'requiredStr', type: String, required: true},
],
};
const typeRegistry = new TypeRegistry();
typeRegistry.registerLoopbackType(TestModel.definition.name, TestModel.definition);
const entry = createAPIDoc(remotingSpec, null, typeRegistry);
const responseMessage = getResponseMessage(entry.operation);
(((responseMessage || {}).schema || {}).required || []).sort(); // sort the array for the comparison below
expect(responseMessage)
.to.have.property('schema').eql({
type: 'object',
properties: {
max: {type: 'number', format: 'double'},
min: {type: 'number', format: 'double'},
avg: {type: 'number', format: 'double'},
str: {type: 'string'},
strArray: {
items: {
type: 'string',
},
type: 'array',
},
testModel: {
$ref: '#/definitions/TestModel',
},
testModelArray: {
items: {
$ref: '#/definitions/TestModel',
},
type: 'array',
},
testModelArrayArray: {
items: {
items: {
$ref: '#/definitions/TestModel',
},
type: 'array',
},
type: 'array',
},
testModelStr: {
$ref: '#/definitions/TestModel',
},
unknownModel: {
type: 'object', // unknown model is converted to plain object
},
requiredStr: {type: 'string'},
},
required: [
'requiredStr',
],
});
});
it('does not include arguments having http.target set to header|status', function() {
var TestModel = loopback.createModel('TestModel', {street: String});
var entry = createAPIDoc({
returns: [
{name: 'changes', type: 'ReadableStream'},
{name: 'status', type: 'number', http: {target: 'status'}},
{name: 'header', type: 'string', http: {target: 'header'}},
],
});
var responseMessage = getResponseMessage(entry.operation);
expect(responseMessage)
.to.have.property('schema').eql({
type: 'file',
});
});
it('does not produce required array if no required property is defined', function() {
var TestModel = loopback.createModel('TestModel', {street: String});
var entry = createAPIDoc({
returns: [
{arg: 'max', type: 'number'},
{arg: 'min', type: 'number'},
],
});
var responseMessage = getResponseMessage(entry.operation);
expect(responseMessage.schema).to.not.have.property('required');
});
it('converts { type: ReadableStream\' } to { schema: { type: \'file\' } }', function() {
var TestModel = loopback.createModel('TestModel', {street: String});
var entry = createAPIDoc({
returns: [
{name: 'changes', type: 'ReadableStream'},
],
});
var responseMessage = getResponseMessage(entry.operation);
expect(responseMessage)
.to.have.property('schema').eql({
type: 'file',
});
});
it('converts { type: file\' } to { schema: { type: \'file\' } }', function() {
var TestModel = loopback.createModel('TestModel', {street: String});
var entry = createAPIDoc({
accepts: [
{name: 'changes', type: 'file'},
],
});
var paramDoc = entry.operation.parameters[0];
expect(paramDoc).to.have.property('type', 'file');
expect(paramDoc).to.have.property('in', 'formData');
expect(paramDoc).to.have.property('allowMultiple', false);
expect(paramDoc).to.have.property('description', 'File to upload');
});
it('converts path params when they exist in the route name', function() {
var entry = createAPIDoc({
accepts: [
{arg: 'id', type: 'string'},
],
path: '/test/:id',
});
var paramDoc = entry.operation.parameters[0];
expect(paramDoc).to.have.property('in', 'path');
expect(paramDoc).to.have.property('name', 'id');
expect(paramDoc).to.have.property('required', true);
});
it('sets required to be true for path params', function() {
var entry = createAPIDoc({
accepts: [
{arg: 'id', type: 'string', http: {source: 'path'}},
],
path: '/test/:id',
});
var paramDoc = entry.operation.parameters[0];
expect(paramDoc).to.have.property('in', 'path');
expect(paramDoc).to.have.property('name', 'id');
expect(paramDoc).to.have.property('required', true);
});
// FIXME need regex in routeHelper.acceptToParameter
xit('won\'t convert path params when they don\'t exist in the route name', function() {
var doc = createAPIDoc({
accepts: [
{arg: 'id', type: 'string'},
],
path: '/test/:identifier',
});
var paramDoc = doc.operation.parameters[0];
expect(paramDoc.in).to.equal('query');
});
it('correctly coerces param types', function() {
var doc = createAPIDoc({
accepts: [
{arg: 'binaryData', type: 'buffer'},
],
});
var paramDoc = doc.operation.parameters[0];
expect(paramDoc).to.have.property('in', 'query');
expect(paramDoc).to.have.property('type', 'string');
expect(paramDoc).to.have.property('format', 'byte');
});
it('correctly removes undocumented accepts', function() {
var doc = createAPIDoc({
accepts: [
{arg: 'id', type: 'string'},
{arg: 'undocumented', type: 'string', documented: false},
],
});
expect(doc.operation.parameters.length).to.equal(1);
var paramDoc = doc.operation.parameters[0];
expect(paramDoc).to.have.property('in', 'query');
expect(paramDoc).to.have.property('type', 'string');
expect(paramDoc).to.have.property('name', 'id');
});
it('correctly converts root return types (arrays)', function() {
var doc = createAPIDoc({
returns: [
{arg: 'data', type: ['customType'], root: true},
],
});
var opDoc = doc.operation;
var responseSchema = getResponseMessage(opDoc).schema;
expect(responseSchema).to.have.property('type', 'array');
expect(responseSchema).to.have.property('items')
.eql({$ref: '#/definitions/customType'});
});
it('correctly converts non root return types (arrays)', function() {
const doc = createAPIDoc({
returns: [
{arg: 'data', type: ['customType']},
],
});
const opDoc = doc.operation;
const responseSchema = getResponseMessage(opDoc).schema;
expect(responseSchema)
.to.eql({
type: 'object',
properties: {
data: {
type: 'array',
items: {
type: 'object',
},
},
},
});
});
it('correctly converts registered non root return types', function() {
const customType = loopback.createModel('customType', {foo: String});
const typeRegistry = new TypeRegistry();
typeRegistry.registerLoopbackType(customType.definition.name, customType.definition);
const remotingSpec = {
returns: [
{
arg: 'data',
type: ['customType'],
},
],
};
const doc = createAPIDoc(remotingSpec, null, typeRegistry);
const opDoc = doc.operation;
const responseSchema = getResponseMessage(opDoc).schema;
expect(responseSchema)
.to.eql({
type: 'object',
properties: {
data: {
type: 'array',
items: {
$ref: '#/definitions/customType',
},
},
},
});
});
it('correctly converts return types (format)', function() {
var doc = createAPIDoc({
returns: [
{arg: 'data', type: 'buffer', root: true},
],
});
var responseSchema = getResponseMessage(doc.operation).schema;
expect(responseSchema.type).to.equal('string');
expect(responseSchema.format).to.equal('byte');
});
it('includes `notes` metadata as `description`', function() {
var doc = createAPIDoc({
notes: 'some notes',
});
expect(doc.operation).to.have.property('description', 'some notes');
});
describe('#acceptToParameter', function() {
var A_CLASS_DEF = {name: 'TestModelName'};
it('returns fn converting description from array to string', function() {
var f = routeHelper.acceptToParameter(
{verb: 'get', path: 'path'},
A_CLASS_DEF,
new TypeRegistry()
);
var result = f({description: ['1', '2', '3']});
expect(result.description).to.eql('1\n2\n3');
});
it('coerces `form` to `formData` when checking http settings', function() {
var f = routeHelper.acceptToParameter(
{verb: 'put', path: 'path'},
A_CLASS_DEF,
new TypeRegistry()
);
var result = f({http: {source: 'form'}});
expect(result.in).to.equal('formData');
});
});
describe('#routeToPathEntry', function() {
it('converts route.description from array to string', function() {
var result = routeHelper.routeToPathEntry({
method: 'someMethod',
verb: 'get',
path: 'path',
description: ['1', '2', '3'],
}, null, new TypeRegistry(), Object.create(null));
expect(result.operation.summary).to.eql('1\n2\n3');
});
it('converts route.notes from array of string to string', function() {
var result = routeHelper.routeToPathEntry({
method: 'someMethod',
verb: 'get',
path: 'path',
notes: ['1', '2', '3'],
}, null, new TypeRegistry(), Object.create(null));
expect(result.operation.description).to.eql('1\n2\n3');
});
});
it('includes `deprecated` metadata', function() {
var doc = createAPIDoc({
deprecated: 'true',
});
expect(doc.operation).to.have.property('deprecated', true);
});
it('joins array description/summary', function() {
var doc = createAPIDoc({
description: ['line1', 'line2'],
});
expect(doc.operation.summary).to.equal('line1\nline2');
});
it('joins array notes', function() {
var doc = createAPIDoc({
notes: ['line1', 'line2'],
});
expect(doc.operation.description).to.equal('line1\nline2');
});
it('joins array description/summary of an input arg', function() {
var doc = createAPIDoc({
accepts: [{name: 'arg', description: ['line1', 'line2']}],
});
expect(doc.operation.parameters[0].description).to.equal('line1\nline2');
});
it('correctly does not include context params', function() {
var doc = createAPIDoc({
accepts: [
{arg: 'ctx', http: {source: 'context'}},
],
path: '/test',
});
var params = doc.operation.parameters;
expect(params.length).to.equal(0);
});
it('correctly does not include request params', function() {
var doc = createAPIDoc({
accepts: [
{arg: 'req', http: {source: 'req'}},
],
path: '/test',
});
var params = doc.operation.parameters;
expect(params.length).to.equal(0);
});
it('correctly does not include response params', function() {
var doc = createAPIDoc({
accepts: [
{arg: 'res', http: {source: 'res'}},
],
path: '/test',
});
var params = doc.operation.parameters;
expect(params.length).to.equal(0);
});
it('preserves `enum` accepts arg metadata', function() {
var doc = createAPIDoc({
accepts: [{name: 'arg', type: 'number', enum: [1, 2, 3]}],
});
expect(doc.operation.parameters[0])
.to.have.property('enum').eql([1, 2, 3]);
});
it('includes the default response message with code 200', function() {
var doc = createAPIDoc({
returns: [{name: 'result', type: 'object', root: true}],
});
expect(doc.operation.responses).to.eql({
200: {
description: 'Request was successful',
schema: {type: 'object'},
},
});
});
it('uses the response code 204 when `returns` is empty', function() {
var doc = createAPIDoc({
returns: [],
});
expect(doc.operation.responses).to.eql({
204: {
description: 'Request was successful',
schema: undefined,
},
});
});
it('includes custom error response in `responseMessages`', function() {
var doc = createAPIDoc({
errors: [{
code: 422,
message: 'Validation failed',
responseModel: 'ValidationError',
}],
});
expect(doc.operation.responses).to.have.property(422).eql({
description: 'Validation failed',
schema: {$ref: '#/definitions/ValidationError'},
});
});
it('allows setting null for `responseModel`', function() {
var doc = createAPIDoc({
errors: [{
code: 422,
message: 'Validation',
responseModel: null,
}],
});
expect(doc.schema).to.equal(undefined);
});
it('allows omitting the `responseModel`', function() {
var doc = createAPIDoc({
errors: [{
code: 422,
message: 'Validation',
}],
});
expect(doc.schema).to.equal(undefined);
});
it('includes custom http status code and override default ' +
'success code in `responseMessages`', function() {
var doc = createAPIDoc({
http: {
status: 201,
},
});
expect(doc.operation.responses).to.have.property(201).eql({
description: 'Request was successful',
schema: undefined,
});
expect(doc.operation.responses).to.not.have.property(200);
expect(doc.operation.responses).to.not.have.property(204);
});
it('includes custom http errorStatus', function() {
const doc = createAPIDoc({
http: {
status: 201,
errorStatus: 404,
},
});
const responses = doc.operation.responses;
expect(Object.keys(responses)).to.eql(['201', '404']);
expect(responses['404']).to.eql({
description: 'Unknown error',
});
});
it('does not include `undefined` error response in `responseMessages` when ' +
'http is set, but erorrStatus is not set', function() {
const doc = createAPIDoc({
http: {
status: 201,
},
});
expect(Object.keys(doc.operation.responses)).to.eql(['201']);
});
it('supports example responses', function() {
var doc = createAPIDoc({
returns: [
{arg: 'something', http: {source: 'body'}, example: {foo: 'bar'}},
],
path: '/test',
});
expect(doc.operation.responses['200']).to.property('examples');
expect(doc.operation.responses['200'].examples).to.have.property('application/json');
expect(doc.operation.responses['200'].examples['application/json']).to.eql({foo: 'bar'});
});
it('includes custom http error status code in `responseMessages`', function() {
var doc = createAPIDoc({
http: {
errorStatus: 508,
},
});
expect(doc.operation.responses).to.have.property(508).eql({
description: 'Unknown error',
});
});
it('route operationId DOES include model name.', function() {
var doc = createAPIDoc({method: 'User.login'});
expect(doc.operation.operationId).to.equal('User.login');
});
it('adds class name to `tags`', function() {
var doc = createAPIDoc(
{method: 'User.login'},
{name: 'User'}
);
expect(doc.operation.tags).to.contain('User');
});
it('converts non-primitive param types to JSON strings', function() {
var doc = createAPIDoc({
accepts: [{arg: 'filter', type: 'object', http: {source: 'query'}}],
});
var param = doc.operation.parameters[0];
expect(param).to.have.property('type', 'string');
expect(param).to.have.property('format', 'JSON');
});
it('converts single "data" body arg to Model type', function() {
var doc = createAPIDoc(
{
accepts: [{arg: 'data', type: 'object', http: {source: 'body'}}],
},
{name: 'User'}
);
var param = doc.operation.parameters[0];
expect(param)
.to.have.property('schema')
.eql({$ref: '#/definitions/User'});
});
it('supports `model` property', function() {
var doc = createAPIDoc(
{
accepts: [{
arg: 'result',
type: 'object',
model: 'User',
http: {source: 'body'},
}],
},
{name: 'User'}
);
var param = doc.operation.parameters[0];
expect(param)
.to.have.property('schema')
.eql({$ref: '#/definitions/User'});
});
it('allows a custom `tag` name to be set', function() {
var doc = createAPIDoc(
{},
{name: 'User', ctor: {settings: {swagger: {tag: {name: 'Member'}}}}}
);
expect(doc.operation.tags[0])
.to.eql('Member');
});
});
// Easy wrapper around createRoute
function createAPIDoc(def, classDef, typeRegistry) {
if (!typeRegistry) typeRegistry = new TypeRegistry();
return routeHelper.routeToPathEntry(_defaults(def || {}, {
path: '/test',
verb: 'GET',
method: 'test.get',
}), classDef, typeRegistry, Object.create(null));
}
function getResponseMessage(operationDoc) {
return operationDoc.responses[200] || operationDoc.responses[204] ||
operationDoc.responses.default;
}