// Copyright IBM Corp. 2014,2016. All Rights Reserved. // Node module: loopback-boot // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; var async = require('async'); var boot = require('../'); var path = require('path'); var loopback = require('loopback'); var assert = require('assert'); var chai = require('chai'); var dirtyChai = require('dirty-chai'); var expect = chai.expect; chai.use(dirtyChai); var fs = require('fs-extra'); var sandbox = require('./helpers/sandbox'); var appdir = require('./helpers/appdir'); var supertest = require('supertest'); var os = require('os'); var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); var ENV_APP = path.join(__dirname, 'fixtures', 'env-app'); var app; describe('executor', function() { beforeEach(sandbox.reset); beforeEach(appdir.init); beforeEach(function() { app = loopback(); // process.bootFlags is used by simple-app/boot/*.js scripts process.bootFlags = []; }); afterEach(function() { delete process.bootFlags; }); var dummyInstructions = someInstructions({ application: { port: 0, host: '127.0.0.1', restApiRoot: '/rest-api', foo: {bar: 'bat'}, baz: true, }, models: [ { name: 'User', config: { dataSource: 'the-db', }, }, ], dataSources: { 'the-db': { connector: 'memory', }, }, }); describe('when booting', function() { it('should set the `booting` flag during execution', function(done) { expect(app.booting).to.be.undefined(); simpleAppInstructions(function(err, context) { if (err) return done(err); boot.execute(app, context.instructions, function(err) { expect(err).to.not.exist(); expect(process.bootingFlagSet).to.be.true(); expect(app.booting).to.be.false(); done(); }); }); }); it('should emit the `booted` event in the next tick', function(done) { boot.execute(app, dummyInstructions, function(err) { expect(err).to.not.exist(); }); app.on('booted', function() { // This test fails with a timeout when the `booted` event has not been // emitted correctly done(); }); }); it('should work when called synchronously', function() { boot.execute(app, dummyInstructions); }); }); it('configures models', function(done) { boot.execute(app, dummyInstructions, function(err, context) { if (err) return done(err); assert(app.models); assert(app.models.User); assert.equal(app.models.User, app.registry.getModel('User'), 'Boot should not have extended built-in User model'); assertValidDataSource(app.models.User.dataSource); assert.isFunc(app.models.User, 'find'); assert.isFunc(app.models.User, 'create'); done(); }); }); it('defines and customizes models', function(done) { appdir.writeFileSync('models/Customer.js', 'module.exports = ' + function(Customer) { Customer.settings._customized = 'Customer'; Customer.base.settings._customized = 'Base'; }.toString()); boot.execute(app, someInstructions({ models: [ { name: 'Customer', config: {dataSource: 'db'}, definition: { name: 'Customer', base: 'User', }, sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'), }, ], }), function(err, context) { if (err) return done(err); expect(app.models.Customer).to.exist(); expect(app.models.Customer.settings._customized).to.be.equal('Customer'); var UserModel = app.registry.getModel('User'); expect(UserModel.settings._customized).to.equal('Base'); done(); }); }); it('defines model without attaching it', function(done) { boot.execute(app, someInstructions({ models: [ { name: 'Vehicle', config: undefined, definition: { name: 'Vehicle', }, sourceFile: undefined, }, { name: 'Car', config: {dataSource: 'db'}, definition: { name: 'Car', base: 'Vehicle', }, sourceFile: undefined, }, ], }), function(err, context) { if (err) return done(err); expect(Object.keys(app.models)).to.eql(['Car']); done(); }); }); it('attaches models to data sources', function(done) { boot.execute(app, dummyInstructions, function(err, context) { if (err) return done(err); assert.equal(app.models.User.dataSource, app.dataSources.theDb); done(); }); }); it('defines all models first before running the config phase', function(done) { appdir.writeFileSync('models/Customer.js', 'module.exports = ' + function(Customer/* , Base */) { Customer.on('attached', function() { Customer._modelsWhenAttached = Object.keys(Customer.modelBuilder.models); }); }.toString()); boot.execute(app, someInstructions({ models: [ { name: 'Customer', config: {dataSource: 'db'}, definition: {name: 'Customer'}, sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'), }, { name: 'UniqueName', config: {dataSource: 'db'}, definition: {name: 'UniqueName'}, sourceFile: undefined, }, ], }), function(err, context) { if (err) return done(err); expect(app.models.Customer._modelsWhenAttached). to.include('UniqueName'); done(); }); }); it('defines models in the local app registry', function(done) { app = loopback({localRegistry: true}); boot.execute(app, someInstructions({ models: [ { name: 'LocalCustomer', config: {dataSource: 'db'}, definition: {name: 'LocalCustomer'}, sourceFile: undefined, }, ], }), function(err, context) { if (err) return done(err); expect(Object.keys(loopback.registry.modelBuilder.models), 'global models') .to.not.contain('LocalCustomer'); expect(Object.keys(app.registry.modelBuilder.models), 'local models') .to.contain('LocalCustomer'); done(); }); }); it('throws on bad require() call inside boot script', function(done) { var file = appdir.writeFileSync('boot/badScript.js', 'require("doesnt-exist"); module.exports = {};'); boot.execute(app, someInstructions({bootScripts: [file]}), function(err) { expect(err && err.message) .to.match(/Cannot find module \'doesnt-exist\'/); done(); }); }); it('instantiates data sources', function(done) { boot.execute(app, dummyInstructions, function(err, context) { if (err) return done(err); assert(app.dataSources); assert(app.dataSources.theDb); assertValidDataSource(app.dataSources.theDb); assert(app.dataSources.TheDb); done(); }); }); it('does not call autoAttach', function(done) { boot.execute(app, dummyInstructions, function(err, context) { if (err) return done(err); // loopback-datasource-juggler quirk: // Model.dataSources has modelBuilder as the default value, // therefore it's not enough to assert a false-y value var actual = loopback.Email.dataSource instanceof loopback.DataSource ? 'attached' : 'not attached'; expect(actual).to.equal('not attached'); done(); }); }); it('skips definition of already defined LoopBack models', function(done) { var builtinModel = { name: 'User', definition: fs.readJsonSync( require.resolve('loopback/common/models/user.json') ), config: {dataSource: 'db'}, sourceFile: require.resolve('loopback/common/models/user.js'), }; builtinModel.definition.redefined = true; boot.execute(app, someInstructions({models: [builtinModel]}), function(err, context) { if (err) return done(err); expect(app.models.User.settings.redefined, 'redefined').to.not.equal(true); done(); }); }); describe('with boot and models files', function() { beforeEach(function(done) { simpleAppInstructions(function(err, context) { if (err) return done(err); boot.execute(app, context.instructions, done); }); }); afterEach(function() { delete process.bootFlags; }); it('should run `boot/*` files', function() { // scripts are loaded by the order of file names expect(process.bootFlags).to.eql([ 'barLoaded', 'barSyncLoaded', 'fooLoaded', 'promiseLoaded', 'thenableLoaded', 'barStarted', 'barFinished', 'barSyncExecuted', 'promiseStarted', 'promiseFinished', 'thenableStarted', 'thenableFinished', ]); }); }); describe('with boot with callback', function() { it('should run `boot/*` files asynchronously', function(done) { simpleAppInstructions(function(err, context) { if (err) return done(err); boot.execute(app, context.instructions, function() { expect(process.bootFlags).to.eql([ 'barLoaded', 'barSyncLoaded', 'fooLoaded', 'promiseLoaded', 'thenableLoaded', 'barStarted', 'barFinished', 'barSyncExecuted', 'promiseStarted', 'promiseFinished', 'thenableStarted', 'thenableFinished', ]); done(); }); }); }); }); describe('with boot script returning a rejected promise', function() { before(function() { // Tell simple-app/boot/reject.js to return a rejected promise process.rejectPromise = true; }); after(function() { delete process.rejectPromise; }); it('receives rejected promise as callback error', function(done) { simpleAppInstructions(function(err, context) { if (err) return done(err); boot.execute(app, context.instructions, function(err) { expect(err).to.exist.and.be.an.instanceOf(Error) .with.property('message', 'reject'); done(); }); }); }); }); describe('with boot script throwing an error', function() { before(function() { // Tell simple-app/boot/throw.js to throw an error process.throwError = true; }); after(function() { delete process.throwError; }); it('receives thrown error as callback errors', function(done) { simpleAppInstructions(function(err, context) { if (err) return done(err); boot.execute(app, context.instructions, function(err) { expect(err).to.exist.and.be.an.instanceOf(Error) .with.property('message', 'throw'); done(); }); }); }); }); describe('with boot script returning a promise and calling callback', function() { before(function() { process.promiseAndCallback = true; }); after(function() { delete process.promiseAndCallback; }); it('should only call the callback once', function(done) { simpleAppInstructions(function(err, context) { if (err) return done(err); // Note: Mocha will fail this test if done() is called twice boot.execute(app, context.instructions, done); }); }); } ); describe('for mixins', function() { var options; beforeEach(function() { appdir.writeFileSync('custom-mixins/example.js', 'module.exports = ' + 'function(Model, options) {}'); appdir.writeFileSync('custom-mixins/time-stamps.js', 'module.exports = ' + 'function(Model, options) {}'); appdir.writeConfigFileSync('custom-mixins/time-stamps.json', { name: 'Timestamping', }); options = { appRootDir: appdir.PATH, }; }); it('defines mixins from instructions - using `mixinDirs`', function(done) { options.mixinDirs = ['./custom-mixins']; boot(app, options, function(err) { if (err) return done(err); var modelBuilder = app.registry.modelBuilder; var registry = modelBuilder.mixins.mixins; expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); done(); }); }); it('defines mixins from instructions - using `mixinSources`', function(done) { options.mixinSources = ['./custom-mixins']; boot(app, options, function(err) { if (err) return done(err); var modelBuilder = app.registry.modelBuilder; var registry = modelBuilder.mixins.mixins; expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); done(); }); }); }); describe('with PaaS and npm env variables', function() { beforeEach(function cleanEnvironment() { // jscs:disable requireCamelCaseOrUpperCaseIdentifiers delete process.env.npm_config_host; delete process.env.OPENSHIFT_SLS_IP; delete process.env.OPENSHIFT_NODEJS_IP; delete process.env.VCAP_APP_HOST; delete process.env.HOST; delete process.env.npm_package_config_host; delete process.env.npm_config_port; delete process.env.OPENSHIFT_SLS_PORT; delete process.env.OPENSHIFT_NODEJS_PORT; delete process.env.VCAP_APP_PORT; delete process.env.PORT; delete process.env.npm_package_config_port; }); function bootWithDefaults(done) { app = loopback(); boot.execute(app, someInstructions({ application: { port: undefined, host: undefined, }, }), done); } it('should apply env passed in option object', function(done) { boot.execute(app, someInstructions({env: 'custom_env'}), function(err) { if (err) return done(err); expect(app.get('env')).to.equal('custom_env'); done(); }); }); it('should honor host and port', function(done) { function assertHonored(portKey, hostKey, cb) { process.env[hostKey] = randomPort(); process.env[portKey] = randomHost(); bootWithDefaults(function(err) { if (err) return cb(err); assert.equal(app.get('port'), process.env[portKey], portKey); assert.equal(app.get('host'), process.env[hostKey], hostKey); delete process.env[portKey]; delete process.env[hostKey]; cb(); }); } async.eachSeries([ {port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_NODEJS_IP'}, {port: 'npm_config_port', host: 'npm_config_host'}, {port: 'npm_package_config_port', host: 'npm_package_config_host'}, {port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_SLS_IP'}, {port: 'VCAP_APP_PORT', host: 'VCAP_APP_HOST'}, {port: 'PORT', host: 'HOST'}, ], function(config, cb) { assertHonored(config.port, config.host, cb); }, done); }); it('should prioritize host sources', function(done) { // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /* eslint-disable camelcase */ process.env.npm_config_host = randomHost(); process.env.OPENSHIFT_SLS_IP = randomHost(); process.env.OPENSHIFT_NODEJS_IP = randomHost(); process.env.VCAP_APP_HOST = randomHost(); process.env.HOST = randomHost(); process.env.npm_package_config_host = randomHost(); bootWithDefaults(function(err) { if (err) return done(err); assert.equal(app.get('host'), process.env.npm_config_host); /* eslint-enable camelcase */ done(); }); }); it('should prioritize port sources', function(done) { /* eslint-disable camelcase */ process.env.npm_config_port = randomPort(); process.env.OPENSHIFT_SLS_PORT = randomPort(); process.env.OPENSHIFT_NODEJS_PORT = randomPort(); process.env.VCAP_APP_PORT = randomPort(); process.env.PORT = randomPort(); process.env.npm_package_config_port = randomPort(); bootWithDefaults(function(err) { if (err) return done(err); assert.equal(app.get('port'), process.env.npm_config_port); /* eslint-enable camelcase */ done(); }); }); function randomHost() { return Math.random().toString().split('.')[1]; } function randomPort() { return Math.floor(Math.random() * 10000); } it('should honor 0 for free port', function(done) { boot.execute(app, someInstructions({application: {port: 0}}), function(err) { if (err) return done(err); assert.equal(app.get('port'), 0); done(); }); }); it('should default to port 3000', function(done) { boot.execute(app, someInstructions({application: {port: undefined}}), function(err) { if (err) return done(err); assert.equal(app.get('port'), 3000); done(); }); }); it('should respect named pipes port values in ENV', function(done) { var NAMED_PORT = '\\.\\pipe\\test'; process.env.PORT = NAMED_PORT; boot.execute(app, someInstructions({application: {port: 3000}}), function(err) { if (err) return done(err); assert.equal(app.get('port'), NAMED_PORT); done(); }); }); }); describe('with middleware.json', function(done) { beforeEach(function() { delete process.env.restApiRoot; }); it('should parse a simple config variable', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', {path: '${restApiRoot}'} ), function(err) { if (err) return done(err); supertest(app).get('/').end(function(err, res) { if (err) return done(err); expect(res.body.path).to.equal(app.get('restApiRoot')); done(); }); }); }); it('should parse simple config variable from env var', function(done) { process.env.restApiRoot = '/url-from-env-var'; boot.execute(app, simpleMiddlewareConfig('routes', {path: '${restApiRoot}'} ), function(err) { if (err) return done(err); supertest(app).get('/url-from-env-var').end(function(err, res) { if (err) return done(err); expect(res.body.path).to.equal('/url-from-env-var'); done(); }); }); }); it('dynamic variable from `env var` should have' + ' precedence over app.get()', function(done) { process.env.restApiRoot = '/url-from-env-var'; var bootInstructions; bootInstructions = simpleMiddlewareConfig('routes', {path: '${restApiRoot}'}); bootInstructions.application = {restApiRoot: '/url-from-config'}; boot.execute(app, someInstructions(bootInstructions), function(err) { if (err) return done(err); supertest(app).get('/url-from-env-var').end(function(err, res) { if (err) return done(err); expect(app.get('restApiRoot')).to.equal('/url-from-config'); expect(res.body.path).to.equal('/url-from-env-var'); done(); }); }); }); it('should parse multiple config variables', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', {path: '${restApiRoot}', env: '${env}'} ), function(err) { if (err) return done(err); supertest(app).get('/').end(function(err, res) { if (err) return done(err); expect(res.body.path).to.equal(app.get('restApiRoot')); expect(res.body.env).to.equal(app.get('env')); done(); }); }); }); it('should parse config variables in an array', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', {paths: ['${restApiRoot}']} ), function(err) { if (err) return done(err); supertest(app).get('/').end(function(err, res) { if (err) return done(err); expect(res.body.paths).to.eql( [app.get('restApiRoot')] ); done(); }); }); }); it('should parse config variables in an object', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', {info: {path: '${restApiRoot}'}} ), function(err) { if (err) return done(err); supertest(app).get('/').end(function(err, res) { if (err) return done(err); expect(res.body.info).to.eql({ path: app.get('restApiRoot'), }); done(); }); }); }); it('should parse config variables in a nested object', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', {nested: {info: {path: '${restApiRoot}'}}} ), function(err) { if (err) return done(err); supertest(app).get('/').end(function(err, res) { if (err) return done(err); expect(res.body.nested).to.eql({ info: {path: app.get('restApiRoot')}, }); done(); }); }); }); it('should parse config variables with null values', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', {nested: {info: {path: '${restApiRoot}', some: null}}} ), function(err) { if (err) return done(err); supertest(app).get('/').end(function(err, res) { if (err) return done(err); expect(res.body.nested).to.eql({ info: { path: app.get('restApiRoot'), some: null, }, }); done(); }); }); }); it('should not parse invalid config variables', function(done) { var invalidDataTypes = [undefined, function() { }]; async.each(invalidDataTypes, function(invalidDataType, cb) { var config = simpleMiddlewareConfig('routes', { path: invalidDataType, }); boot.execute(app, config, function(err) { if (err) return done(err); supertest(app) .get('/') .end(function(err, res) { expect(err).to.be.null(); expect(res.body.path).to.be.undefined(); cb(); }); }, cb); }, done); }); it('should parse valid config variables', function(done) { var config = simpleMiddlewareConfig('routes', { props: ['a', '${vVar}', 1, true, function() { }, {x: 1, y: '${y}'}], }); boot.execute(app, config, function(err) { if (err) return done(err); supertest(app) .get('/') .end(function(err, res) { expect(err).to.be.null(); done(); }); }); }); it('should preserve object prototypes', function(done) { var config = simpleMiddlewareConfig( 'routes', // IMPORTANT we need more than one item to trigger the original issue [/^\/foobar/, /^\/another/], {}); boot.execute(app, config, function(err) { if (err) return done(err); supertest(app).get('/foobar') .expect(200) .end(done); }); }); }); describe('with component-config.json', function() { beforeEach(function() { delete process.env.DYNAMIC_ENVVAR; delete process.env.DYNAMIC_VARIABLE; }); it('should parse a simple config variable', function(done) { boot.execute(app, simpleComponentConfig( {path: '${restApiRoot}'} ), function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.path).to.equal(app.get('restApiRoot')); done(); }); }); }); it('should parse config from `env-var` and `config`', function(done) { var bootInstructions = simpleComponentConfig( { path: '${restApiRoot}', fromConfig: '${DYNAMIC_CONFIG}', fromEnvVar: '${DYNAMIC_ENVVAR}', } ); // result should get value from config.json bootInstructions.application['DYNAMIC_CONFIG'] = 'FOOBAR-CONFIG'; // result should get value from env var process.env.DYNAMIC_ENVVAR = 'FOOBAR-ENVVAR'; boot.execute(app, bootInstructions, function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG'); expect(res.body.fromEnvVar).to.equal('FOOBAR-ENVVAR'); done(); }); }); }); it('`env-var` should have precedence over `config`', function(done) { var key = 'DYNAMIC_VARIABLE'; var bootInstructions = simpleComponentConfig({ path: '${restApiRoot}', isDynamic: '${' + key + '}', }); bootInstructions.application[key] = 'should be overwritten'; process.env[key] = 'successfully overwritten'; boot.execute(app, bootInstructions, function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.isDynamic).to.equal('successfully overwritten'); done(); }); }); }); it('should parse multiple config variables', function(done) { boot.execute(app, simpleComponentConfig( {path: '${restApiRoot}', env: '${env}'} ), function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.path).to.equal(app.get('restApiRoot')); expect(res.body.env).to.equal(app.get('env')); done(); }); }); }); it('should parse config variables in an array', function(done) { boot.execute(app, simpleComponentConfig( {paths: ['${restApiRoot}']} ), function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.paths).to.eql( [app.get('restApiRoot')] ); done(); }); }); }); it('should parse config variables in an object', function(done) { boot.execute(app, simpleComponentConfig( {info: {path: '${restApiRoot}'}} ), function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.info).to.eql({ path: app.get('restApiRoot'), }); done(); }); }); }); it('should parse config variables in a nested object', function(done) { boot.execute(app, simpleComponentConfig( {nested: {info: {path: '${restApiRoot}'}}} ), function(err) { if (err) return done(err); supertest(app).get('/component').end(function(err, res) { if (err) return done(err); expect(res.body.nested).to.eql({ info: {path: app.get('restApiRoot')}, }); done(); }); }); }); }); it('calls function exported by boot/init.js', function(done) { var file = appdir.writeFileSync('boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); delete app.fnCalled; boot.execute(app, someInstructions({bootScripts: [file]}), function(err) { if (err) return done(err); expect(app.fnCalled, 'exported fn was called').to.be.true(); done(); }); }); it('configures middleware', function(done) { var pushNamePath = require.resolve('./helpers/push-name-middleware'); boot.execute(app, someInstructions({ middleware: { phases: ['initial', 'custom'], middleware: [ { sourceFile: pushNamePath, config: { phase: 'initial', params: 'initial', }, }, { sourceFile: pushNamePath, config: { phase: 'custom', params: 'custom', }, }, { sourceFile: pushNamePath, config: { phase: 'routes', params: 'routes', }, }, { sourceFile: pushNamePath, config: { phase: 'routes', enabled: false, params: 'disabled', }, }, ], }, }), function(err) { if (err) return done(err); supertest(app) .get('/') .end(function(err, res) { if (err) return done(err); var names = (res.headers.names || '').split(','); expect(names).to.eql(['initial', 'custom', 'routes']); done(); }); }); }); it('configures middleware using shortform', function(done) { boot.execute(app, someInstructions({ middleware: { middleware: [ { sourceFile: require.resolve('loopback'), fragment: 'static', config: { phase: 'files', params: path.join(__dirname, './fixtures/simple-app/client/'), }, }, ], }, }), function(err) { if (err) return done(err); supertest(app) .get('/') .end(function(err, res) { if (err) return done(err); var EXPECTED_TEXT = '\n\n
\n' + ' \n