From 6838087a5c79e29a63f141ce96df42cbe6b00071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 10:07:10 +0200 Subject: [PATCH 1/7] swagger: use X-Forwarded-Host for basePath --- lib/swagger.js | 10 ++++++---- test/swagger.test.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 0edd55f..b879a18 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -114,12 +114,14 @@ function addRoute(app, uri, doc, opts) { // can't guarantee this path is either reachable or desirable if it's set // as a part of the options. // - // The simplest way around this is to reflect the value of the `Host` HTTP - // header as the `basePath`. Because we pre-build the Swagger data, we don't - // know that header at the time the data is built. + // The simplest way around this is to reflect the value of the `Host` and/or + // `X-Forwarded-Host` HTTP headers as the `basePath`. + // Because we pre-build the Swagger data, we don't know that header at + // the time the data is built. if (hasBasePath) { var headers = req.headers; - var host = headers.Host || headers.host; + // NOTE header names (keys) are always all-lowercase + var host = headers['x-forwarded-host'] || headers.host; doc.basePath = (opts.protocol || req.protocol) + '://' + host + initialPath; } diff --git a/test/swagger.test.js b/test/swagger.test.js index c6f0987..c967ad6 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -81,6 +81,18 @@ describe('swagger definition', function() { done(); }); }); + + it('respects X-Forwarded-Host header (behind a proxy)', function(done) { + var app = mountSwagger(); + getAPIDeclaration(app, 'products') + .set('X-Forwarded-Host', 'example.com') + .end(function(err, res) { + if (err) return done(err); + var baseUrl = url.parse(res.body.basePath); + expect(baseUrl.hostname).to.equal('example.com'); + done(); + }); + }); }); describe('Model definition attributes', function() { From 2decdcc234a0ff5aa9cf2620eaaf78ced2fa620d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 10:12:21 +0200 Subject: [PATCH 2/7] swagger: Deprecate `opts.swaggerVersion` Users of loopback-explorer should not override the swagger version, as it's the explorer who decides what version of the Swagger Spec it implements. --- lib/swagger.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/swagger.js b/lib/swagger.js index b879a18..23fbff0 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -23,6 +23,9 @@ var cors = require('cors'); * @param {Object} opts Options. */ function Swagger(loopbackApplication, swaggerApp, opts) { + if (opts && opts.swaggerVersion) + console.warn('loopback-explorer\'s options.swaggerVersion is deprecated.'); + opts = _defaults(opts || {}, { swaggerVersion: '1.2', basePath: loopbackApplication.get('restApiRoot') || '/api', From d212741638588a81adb112a53f09ec1b717617cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 10:25:57 +0200 Subject: [PATCH 3/7] loopbackStyles: improve spacing in small window Improve spacing of page elements when the browser window is small. --- public/css/loopbackStyles.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/public/css/loopbackStyles.css b/public/css/loopbackStyles.css index 04d6894..5a56782 100644 --- a/public/css/loopbackStyles.css +++ b/public/css/loopbackStyles.css @@ -34,7 +34,12 @@ color: #080; } -/* -FIXME: Separate the overrides from the rest of the styles, rather than override screen.css entirely. -*/ +/* Improve spacing when the browser window is small */ +#message-bar, #swagger-ui-container { + padding-left: 30px; + padding-right: 30px; +} +#api_selector { + padding: 0px 20px; +} From aa7cb0b118baedd469aba7eab59b325b6c43c0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 13:06:01 +0200 Subject: [PATCH 4/7] swagger: include models from accepts/returns args Models not attached to the app are included too. --- lib/swagger.js | 36 ++++++++++++++++ test/swagger.test.js | 99 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 23fbff0..c994bf8 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -12,6 +12,7 @@ var urlJoin = require('./url-join'); var _defaults = require('lodash.defaults'); var classHelper = require('./class-helper'); var routeHelper = require('./route-helper'); +var modelHelper = require('./model-helper'); var cors = require('cors'); /** @@ -84,6 +85,41 @@ function Swagger(loopbackApplication, swaggerApp, opts) { routeHelper.addRouteToAPIDeclaration(route, classDef, doc); }); + // Add models referenced from routes (e.g. accepts/returns) + Object.keys(apiDocs).forEach(function(className) { + var classDoc = apiDocs[className]; + classDoc.apis.forEach(function(api) { + api.operations.forEach(function(routeDoc) { + routeDoc.parameters.forEach(function(param) { + addTypeToModels(param.type); + }); + + addTypeToModels(routeDoc.type); + + // TODO(bajtos) handle types used by responseMessages + + function addTypeToModels(name) { + if (!name || name === 'void') return; + + var model = loopbackApplication.models[name]; + if (!model) { + var loopback = loopbackApplication.loopback; + if (!loopback) return; + + if (loopback.findModel) { + model = loopback.findModel(name); // LoopBack 2.x + } else { + model = loopback.getModel(name); // LoopBack 1.x + } + } + if (!model) return; + + modelHelper.generateModelDefinition(model, classDoc.models); + } + }); + }); + }); + /** * The topmost Swagger resource is a description of all (non-Swagger) * resources available on the system, and where to find more diff --git a/test/swagger.test.js b/test/swagger.test.js index c967ad6..2cab310 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -13,7 +13,7 @@ describe('swagger definition', function() { describe('basePath', function() { // No basepath on resource doc in 1.2 it('no longer exists on resource doc', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); var getReq = getSwaggerResources(app); getReq.end(function(err, res) { @@ -24,7 +24,7 @@ describe('swagger definition', function() { }); it('is "http://{host}/api" by default', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -35,7 +35,7 @@ describe('swagger definition', function() { }); it('is "http://{host}/{basePath}" when basePath is a path', function(done){ - var app = mountSwagger({ basePath: '/api-root'}); + var app = givenAppWithSwagger({ basePath: '/api-root'}); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -47,7 +47,7 @@ describe('swagger definition', function() { }); it('infers API basePath from app', function(done){ - var app = mountSwagger({}, {apiRoot: '/custom-api-root'}); + var app = givenAppWithSwagger({}, {apiRoot: '/custom-api-root'}); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -60,7 +60,7 @@ describe('swagger definition', function() { it('is reachable when explorer mounting location is changed', function(done){ var explorerRoot = '/erforscher'; - var app = mountSwagger({}, {explorerRoot: explorerRoot}); + var app = givenAppWithSwagger({}, {explorerRoot: explorerRoot}); var getReq = getSwaggerResources(app, explorerRoot, 'products'); getReq.end(function(err, res) { @@ -71,7 +71,7 @@ describe('swagger definition', function() { }); it('respects a hardcoded protocol (behind SSL terminator)', function(done){ - var app = mountSwagger({protocol: 'https'}); + var app = givenAppWithSwagger({protocol: 'https'}); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -83,7 +83,7 @@ describe('swagger definition', function() { }); it('respects X-Forwarded-Host header (behind a proxy)', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); getAPIDeclaration(app, 'products') .set('X-Forwarded-Host', 'example.com') .end(function(err, res) { @@ -97,7 +97,7 @@ describe('swagger definition', function() { describe('Model definition attributes', function() { it('Properly defines basic attributes', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -118,7 +118,7 @@ describe('swagger definition', function() { }); it('includes `consumes`', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); getAPIDeclaration(app, 'products').end(function(err, res) { if (err) return done(err); expect(res.body.consumes).to.have.members([ @@ -131,7 +131,7 @@ describe('swagger definition', function() { }); it('includes `produces`', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); getAPIDeclaration(app, 'products').end(function(err, res) { if (err) return done(err); expect(res.body.produces).to.have.members([ @@ -143,11 +143,53 @@ describe('swagger definition', function() { done(); }); }); + + it('includes models from `accepts` args', function(done) { + var app = createLoopbackAppWithModel(); + givenPrivateAppModel(app, 'Image'); + givenSharedMethod(app.models.Product, 'setImage', { + accepts: { name: 'image', type: 'Image' } + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('Image'); + done(); + }); + }); + + it('includes models from `returns` args', function(done) { + var app = createLoopbackAppWithModel(); + givenPrivateAppModel(app, 'Image'); + givenSharedMethod(app.models.Product, 'getImage', { + returns: { name: 'image', type: 'Image' } + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('Image'); + done(); + }); + }); + + it('includes `accepts` models not attached to the app', function(done) { + var app = createLoopbackAppWithModel(); + loopback.createModel('Image'); + givenSharedMethod(app.models.Product, 'setImage', { + accepts: { name: 'image', type: 'Image' } + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('Image'); + done(); + }); + }); }); describe('Cross-origin resource sharing', function() { it('allows cross-origin requests by default', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); request(app) .options('/explorer/resources') .set('Origin', 'http://example.com/') @@ -157,7 +199,7 @@ describe('swagger definition', function() { }); it('can be disabled by configuration', function(done) { - var app = mountSwagger({}, { remoting: { cors: { origin: false } } }); + var app = givenAppWithSwagger({}, { remoting: { cors: { origin: false } } }); request(app) .options('/explorer/resources') .end(function(err, res) { @@ -182,26 +224,35 @@ describe('swagger definition', function() { return getSwaggerResources(app, '', urlJoin('/', className)); } - function mountSwagger(options, addlOptions) { - addlOptions = addlOptions || {}; - var app = createLoopbackAppWithModel(addlOptions.apiRoot); + function givenAppWithSwagger(swaggerOptions, appConfig) { + appConfig = appConfig || {}; + var app = createLoopbackAppWithModel(appConfig.apiRoot); + + if (appConfig.remoting) app.set('remoting', appConfig.remoting); + if (appConfig.explorerRoot) app.set('explorerRoot', appConfig.explorerRoot); + + mountExplorer(app, swaggerOptions); + return app; + } + + function mountExplorer(app, options) { var swaggerApp = express(); - if (addlOptions.remoting) app.set('remoting', addlOptions.remoting); swagger(app, swaggerApp, options); - app.use(addlOptions.explorerRoot || '/explorer', swaggerApp); + app.use(app.get('explorerRoot') || '/explorer', swaggerApp); return app; } function createLoopbackAppWithModel(apiRoot) { var app = loopback(); + app.dataSource('db', { connector: 'memory' }); + var Product = loopback.Model.extend('product', { foo: {type: 'string', required: true}, bar: 'string', aNum: {type: 'number', min: 1, max: 10, required: true, default: 5} }); - Product.attachTo(loopback.memory()); - app.model(Product); + app.model(Product, { dataSource: 'db'}); // Simulate a restApiRoot set in config app.set('restApiRoot', apiRoot || '/api'); @@ -209,4 +260,14 @@ describe('swagger definition', function() { return app; } + + function givenSharedMethod(model, name, metadata) { + model[name] = function(){}; + loopback.remoteMethod(model[name], metadata); + } + + function givenPrivateAppModel(app, name) { + var model = loopback.createModel(name); + app.model(model, { dataSource: 'db', public: false} ); + } }); From 9a6bd35df704a6dc4de1c17f8c2e8231987261ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 16 Oct 2014 13:49:58 +0200 Subject: [PATCH 5/7] model-helper: support anonymous object types Accepts/returns arguments allow anonymous object types, e.g. { 'arg': 'kvp', type: { 'name': 'string', 'value': 'string' } } As of this commit, these types are converted to Swagger type 'object'. --- lib/model-helper.js | 9 ++++++--- test/model-helper.test.js | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 51fbf53..598f0f1 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -121,9 +121,12 @@ var modelHelper = module.exports = { if (typeof propType === 'function') { // See https://github.com/strongloop/loopback-explorer/issues/32 // The type can be a model class - propType = propType.modelName || propType.name.toLowerCase(); - } else if(Array.isArray(propType)) { - propType = 'array'; + return propType.modelName || propType.name.toLowerCase(); + } else if (Array.isArray(propType)) { + return 'array'; + } else if (typeof propType === 'object') { + // Anonymous objects, they are allowed e.g. in accepts/returns definitions + return 'object'; } return propType; }, diff --git a/test/model-helper.test.js b/test/model-helper.test.js index b183400..1fb6414 100644 --- a/test/model-helper.test.js +++ b/test/model-helper.test.js @@ -214,6 +214,13 @@ describe('model-helper', function() { expect(def.properties).to.have.property('visibleProperty'); }); }); + + describe('getPropType', function() { + it('converts anonymous object types', function() { + var type = modelHelper.getPropType({ name: 'string', value: 'string' }); + expect(type).to.eql('object'); + }); + }); }); // Simulates the format of a remoting class. From d05dcb71df55998851c7e822fcf0b6c7520f161c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 13:42:09 +0200 Subject: [PATCH 6/7] route-helper: add `responseMessages` Add a default "success" response message, the status code is 200 or 204 depending on whether the method returns any data. Append any error messages as specified in the `errors` property of method's remoting metadata. Move the description of operation's return type to the "success" response message. Include error message models in the API models. --- lib/route-helper.js | 33 +++++++++++++++----- lib/swagger.js | 4 ++- test/route-helper.test.js | 65 +++++++++++++++++++++++++++++---------- test/swagger.test.js | 18 +++++++++++ 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/lib/route-helper.js b/lib/route-helper.js index d148cea..98ea610 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -123,21 +123,40 @@ var routeHelper = module.exports = { debug('route %j', route); + var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns); + + // Note: Swagger Spec does not provide a way how to specify + // that the responseModel is "array of X". However, + // Swagger UI converts Arrays to the item types anyways, + // therefore it should be ok to do the same here. + var responseModel = responseDoc.type === 'array' ? + responseDoc.items.type : responseDoc.type; + + var responseMessages = [{ + code: route.returns && route.returns.length ? 200 : 204, + message: 'Request was successful', + responseModel: responseModel + }]; + + if (route.errors) { + responseMessages.push.apply(responseMessages, route.errors); + } + var apiDoc = { path: routeHelper.convertPathFragments(route.path), - // Create the operation doc. Use `extendWithType` to add the necessary - // `items` and `format` fields. - operations: [routeHelper.extendWithType({ + // Create the operation doc. + // Note that we are not calling `extendWithType`, as the response type + // is specified in the first response message. + operations: [{ method: routeHelper.convertVerb(route.verb), // [rfeng] Swagger UI doesn't escape '.' for jQuery selector - nickname: route.method.replace(/\./g, '_'), + nickname: route.method.replace(/\./g, '_'), parameters: accepts, - // TODO(schoon) - We don't have descriptions for this yet. - responseMessages: [], + responseMessages: responseMessages, summary: typeConverter.convertText(route.description), notes: typeConverter.convertText(route.notes), deprecated: route.deprecated - }, returns)] + }] }; return apiDoc; diff --git a/lib/swagger.js b/lib/swagger.js index c994bf8..3889166 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -96,7 +96,9 @@ function Swagger(loopbackApplication, swaggerApp, opts) { addTypeToModels(routeDoc.type); - // TODO(bajtos) handle types used by responseMessages + routeDoc.responseMessages.forEach(function(msg) { + addTypeToModels(msg.responseModel); + }); function addTypeToModels(name) { if (!name || name === 'void') return; diff --git a/test/route-helper.test.js b/test/route-helper.test.js index e5a85e8..cfe2010 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -13,7 +13,8 @@ describe('route-helper', function() { { arg: 'avg', type: 'number' } ] }); - expect(doc.operations[0].type).to.equal('object'); + expect(doc.operations[0].type).to.equal(undefined); + expect(getResponseType(doc.operations[0])).to.equal('object'); }); it('converts path params when they exist in the route name', function() { @@ -60,19 +61,12 @@ describe('route-helper', function() { ] }); var opDoc = doc.operations[0]; - expect(opDoc.type).to.equal('array'); - expect(opDoc.items).to.eql({type: 'customType'}); - }); + // Note: swagger-ui treat arrays of X the same way as object X + expect(getResponseType(opDoc)).to.equal('customType'); - it('correctly converts return types (format)', function() { - var doc = createAPIDoc({ - returns: [ - {arg: 'data', type: 'buffer'} - ] - }); - var opDoc = doc.operations[0]; - expect(opDoc.type).to.equal('string'); - expect(opDoc.format).to.equal('byte'); + // NOTE(bajtos) this would be the case if there was a single response type + // expect(opDoc.type).to.equal('array'); + // expect(opDoc.items).to.eql({type: 'customType'}); }); it('includes `notes` metadata', function() { @@ -151,12 +145,45 @@ describe('route-helper', function() { .to.have.property('enum').eql([1,2,3]); }); - it('preserves `enum` returns arg metadata', function() { + it('includes the default response message with code 200', function() { var doc = createAPIDoc({ - returns: [{ name: 'arg', root: true, type: 'number', enum: [1,2,3] }] + returns: [{ name: 'result', type: 'object', root: true }] + }); + expect(doc.operations[0].responseMessages).to.eql([ + { + code: 200, + message: 'Request was successful', + responseModel: 'object' + } + ]); + }); + + it('uses the response code 204 when `returns` is empty', function() { + var doc = createAPIDoc({ + returns: [] + }); + expect(doc.operations[0].responseMessages).to.eql([ + { + code: 204, + message: 'Request was successful', + responseModel: 'void' + } + ]); + }); + + it('includes custom error response in `responseMessages`', function() { + var doc = createAPIDoc({ + errors: [{ + code: 422, + message: 'Validation failed', + responseModel: 'ValidationError' + }] + }); + expect(doc.operations[0].responseMessages[1]).to.eql({ + code: 422, + message: 'Validation failed', + responseModel: 'ValidationError' }); - expect(doc.operations[0]) - .to.have.property('enum').eql([1,2,3]); }); }); @@ -168,3 +195,7 @@ function createAPIDoc(def) { method: 'test.get' })); } + +function getResponseType(operationDoc) { + return operationDoc.responseMessages[0].responseModel; +} diff --git a/test/swagger.test.js b/test/swagger.test.js index 2cab310..77a4542 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -185,6 +185,24 @@ describe('swagger definition', function() { done(); }); }); + + it('includes `responseMessages` models', function(done) { + var app = createLoopbackAppWithModel(); + loopback.createModel('ValidationError'); + givenSharedMethod(app.models.Product, 'setImage', { + errors: [{ + code: '422', + message: 'Validation failed', + responseModel: 'ValidationError' + }] + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('ValidationError'); + done(); + }); + }); }); describe('Cross-origin resource sharing', function() { From 6fb81c279b498f119bd70ef819296b9e4a7e8085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 16 Oct 2014 14:35:55 +0200 Subject: [PATCH 7/7] Add integration tests for included models Add tests verifying that Swagger docs include model description for recursively nested references to Models and Arrays of Models in properties, modelTo and modelThrough relations, accepts, returns and errors. Fix bugs discovered along the way. --- lib/swagger.js | 6 +- test/swagger.test.js | 131 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 3889166..32843b7 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -91,7 +91,11 @@ function Swagger(loopbackApplication, swaggerApp, opts) { classDoc.apis.forEach(function(api) { api.operations.forEach(function(routeDoc) { routeDoc.parameters.forEach(function(param) { - addTypeToModels(param.type); + var type = param.type; + if (type === 'array' && param.items) + type = param.items.type; + + addTypeToModels(type); }); addTypeToModels(routeDoc.type); diff --git a/test/swagger.test.js b/test/swagger.test.js index 77a4542..9a79082 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -196,12 +196,108 @@ describe('swagger definition', function() { responseModel: 'ValidationError' }] }); - mountExplorer(app); - getAPIDeclaration(app, 'products').end(function(err, res) { - expect(Object.keys(res.body.models)).to.include('ValidationError'); - done(); + expectProductDocIncludesModels(app, 'ValidationError', done); + }); + + it('includes nested model references in properties', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + app.models.Product.defineProperty('location', { type: 'Warehouse' }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested array model references in properties', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + app.models.Product.defineProperty('location', { type: ['Warehouse'] }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in modelTo relation', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + app.models.Product.belongsTo(app.models.Warehouse); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in modelTo relation', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + givenPrivateAppModel(app, 'ProductLocations'); + + app.models.Product.hasMany(app.models.Warehouse, + { through: app.models.ProductLocations }); + + expectProductDocIncludesModels( + app, + ['Address', 'Warehouse', 'ProductLocations'], + done); + }); + + it('includes nested model references in accept args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + accepts: { arg: 'w', type: 'Warehouse' } }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested array model references in accept args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + accepts: { arg: 'w', type: [ 'Warehouse' ] } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in return args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + returns: { arg: 'w', type: 'Warehouse', root: true } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested array model references in return args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + returns: { arg: 'w', type: ['Warehouse'], root: true } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in error responses', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + errors: { + code: '222', + message: 'Warehouse', + responseModel: 'Warehouse' + } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); }); }); @@ -234,8 +330,8 @@ describe('swagger definition', function() { return request(app) .get(urlJoin(restPath || '/explorer', '/resources', classPath || '')) .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200); + .expect(200) + .expect('Content-Type', /json/); } function getAPIDeclaration(app, className) { @@ -284,8 +380,27 @@ describe('swagger definition', function() { loopback.remoteMethod(model[name], metadata); } - function givenPrivateAppModel(app, name) { - var model = loopback.createModel(name); + function givenPrivateAppModel(app, name, properties) { + var model = loopback.createModel(name, properties); app.model(model, { dataSource: 'db', public: false} ); } + + function givenWarehouseWithAddressModels(app) { + givenPrivateAppModel(app, 'Address'); + givenPrivateAppModel(app, 'Warehouse', { + shippingAddress: { type: 'Address' } + }); + } + + function expectProductDocIncludesModels(app, modelNames, done) { + if (!Array.isArray(modelNames)) modelNames = [modelNames]; + + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + if (err) return done(err); + expect(Object.keys(res.body.models)).to.include.members(modelNames); + done(); + }); + } });