// Copyright IBM Corp. 2013,2016. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT var loopback = require('loopback'); var explorer = require('../'); var request = require('supertest'); var assert = require('assert'); var path = require('path'); var expect = require('chai').expect; var urlJoin = require('../lib/url-join'); var os = require('os'); describe('explorer', function() { describe('with default config', function() { beforeEach(givenLoopBackAppWithExplorer()); it('should register "loopback-component-explorer" to the app', function() { expect(this.app.get('loopback-component-explorer')) .to.have.property('mountPath', '/explorer'); }); it('should redirect to /explorer/', function(done) { request(this.app) .get('/explorer') .expect(303) .end(done); }); it('should serve the explorer at /explorer/', function(done) { request(this.app) .get('/explorer/') .expect('Content-Type', /html/) .expect(200) .end(function(err, res) { if (err) throw err; assert(!!~res.text.indexOf('StrongLoop API Explorer'), 'text does not contain expected string'); done(); }); }); it('should serve correct swagger-ui config', function(done) { request(this.app) .get('/explorer/config.json') .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { if (err) return done(err); expect(res.body).to .have.property('url', '/explorer/swagger.json'); done(); }); }); }); describe('with custom explorer base', function() { beforeEach(givenLoopBackAppWithExplorer('/swagger')); it('should register "loopback-component-explorer" to the app', function() { expect(this.app.get('loopback-component-explorer')) .to.have.property('mountPath', '/swagger'); }); it('should serve correct swagger-ui config', function(done) { request(this.app) .get('/swagger/config.json') .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { if (err) return done(err); expect(res.body).to .have.property('url', '/swagger/swagger.json'); done(); }); }); }); describe('with custom app.restApiRoot', function() { it('should serve correct swagger-ui config', function(done) { var app = loopback(); app.set('restApiRoot', '/rest-api-root'); configureRestApiAndExplorer(app); request(app) .get('/explorer/config.json') .expect(200) .end(function(err, res) { if (err) return done(err); expect(res.body).to .have.property('url', '/explorer/swagger.json'); done(); }); }); it('removes trailing slash from baseUrl', function(done) { // SwaggerUI builds resource URL by concatenating basePath + resourcePath // Since the resource paths are always startign with a slash, // if the basePath ends with a slash too, an incorrect URL is produced var app = loopback(); app.set('restApiRoot', '/apis/'); configureRestApiAndExplorer(app); request(app) .get('/explorer/swagger.json') .expect(200) .end(function(err, res) { if (err) return done(err); var baseUrl = res.body.basePath; var apiPath = Object.keys(res.body.paths)[0]; expect(baseUrl + apiPath).to.equal('/apis/products'); done(); }); }); }); describe('with custom front-end files', function() { var app; beforeEach(function setupExplorerWithUiDirs() { app = loopback(); explorer(app, { uiDirs: [path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui')], }); }); it('overrides swagger-ui files', function(done) { request(app).get('/explorer/swagger-ui.js') .expect(200) .end(function(err, res) { if (err) return done(err); // expect the content of `dummy-swagger-ui/swagger-ui.js` expect(res.text).to.contain('/* custom swagger-ui file */' + os.EOL); done(); }); }); it('overrides strongloop overrides', function(done) { request(app).get('/explorer/') .expect(200) // expect the content of `dummy-swagger-ui/index.html` .expect('custom index.html' + os.EOL) .end(done); }); }); describe('with swaggerUI option', function() { var app; beforeEach(function setupExplorerWithoutUI() { app = loopback(); explorer(app, { swaggerUI: false, }); }); it('overrides swagger-ui files', function(done) { request(app).get('/explorer/swagger-ui.js') .expect(404, done); }); it('should serve config.json', function(done) { request(app) .get('/explorer/config.json') .expect(200) .end(function(err, res) { if (err) return done(err); expect(res.body).to .have.property('url', '/explorer/swagger.json'); done(); }); }); it('should serve swagger.json', function(done) { request(app) .get('/explorer/swagger.json') .expect(200, done); }); }); describe('explorer.routes API', function() { var app; beforeEach(function() { app = loopback(); var Product = loopback.PersistedModel.extend('product'); Product.attachTo(loopback.memory()); app.model(Product); }); it('creates explorer routes', function(done) { app.use('/explorer', explorer.routes(app)); app.use(app.get('restApiRoot') || '/', loopback.rest()); request(app) .get('/explorer/config.json') .expect('Content-Type', /json/) .expect(200) .end(done); }); }); describe('when specifying custom static file root directories', function() { var app; beforeEach(function() { app = loopback(); }); it('should allow `uiDirs` to be defined as an Array', function(done) { explorer(app, { uiDirs: [path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui')], }); request(app).get('/explorer/') .expect(200) // expect the content of `dummy-swagger-ui/index.html` .expect('custom index.html' + os.EOL) .end(done); }); it('should allow `uiDirs` to be defined as an String', function(done) { explorer(app, { uiDirs: path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui'), }); request(app).get('/explorer/') .expect(200) // expect the content of `dummy-swagger-ui/index.html` .expect('custom index.html' + os.EOL) .end(done); }); }); describe('Cross-origin resource sharing', function() { it('allows cross-origin requests by default', function(done) { var app = loopback(); configureRestApiAndExplorer(app, '/explorer'); request(app) .options('/explorer/swagger.json') .set('Origin', 'http://example.com/') .expect('Access-Control-Allow-Origin', /^http:\/\/example.com\/|\*/) .expect('Access-Control-Allow-Methods', /\bGET\b/) .end(done); }); it('can be disabled by configuration', function(done) { var app = loopback(); app.set('remoting', { cors: { origin: false }}); configureRestApiAndExplorer(app, '/explorer'); request(app) .options('/explorer/swagger.json') .end(function(err, res) { if (err) return done(err); var allowOrigin = res.get('Access-Control-Allow-Origin'); expect(allowOrigin, 'Access-Control-Allow-Origin') .to.equal(undefined); done(); }); }); }); it('updates swagger object when a new model is added', function(done) { var app = loopback(); configureRestApiAndExplorer(app, '/explorer'); // Ensure the swagger object was built request(app) .get('/explorer/swagger.json') .expect(200) .end(function(err) { if (err) return done(err); // Create a new model var Model = loopback.PersistedModel.extend('Customer'); Model.attachTo(loopback.memory()); app.model(Model); // Request swagger.json again request(app) .get('/explorer/swagger.json') .expect(200) .end(function(err, res) { if (err) return done(err); var modelNames = Object.keys(res.body.definitions); expect(modelNames).to.contain('Customer'); var paths = Object.keys(res.body.paths); expect(paths).to.have.contain('/Customers'); done(); }); }); }); it('updates swagger object when a remote method is disabled', function(done) { var app = loopback(); configureRestApiAndExplorer(app, '/explorer'); // Ensure the swagger object was built request(app) .get('/explorer/swagger.json') .expect(200) .end(function(err, res) { if (err) return done(err); // Check the method that will be disabled var paths = Object.keys(res.body.paths); expect(paths).to.contain('/products/findOne'); var Product = app.models.Product; Product.disableRemoteMethod('findOne', true); // Request swagger.json again request(app) .get('/explorer/swagger.json') .expect(200) .end(function(err, res) { if (err) return done(err); var paths = Object.keys(res.body.paths); expect(paths).to.not.contain('/products/findOne'); done(); }); }); }); function givenLoopBackAppWithExplorer(explorerBase) { return function(done) { var app = this.app = loopback(); configureRestApiAndExplorer(app, explorerBase); done(); }; } function configureRestApiAndExplorer(app, explorerBase) { var Product = loopback.PersistedModel.extend('product'); Product.attachTo(loopback.memory()); app.model(Product); explorer(app, { mountPath: explorerBase }); app.set('legacyExplorer', false); app.use(app.get('restApiRoot') || '/', loopback.rest()); } });