From a657e8aa4646cc8366bb3fa46a7c6583c789bab7 Mon Sep 17 00:00:00 2001 From: ebarault Date: Thu, 23 Mar 2017 10:18:28 +0100 Subject: [PATCH] starting unit tests --- lib/application.js | 139 +++++++++++++++++++++++++++------------------ test/app.test.js | 89 +++++++++++++++++++++++++---- test/user.test.js | 2 +- 3 files changed, 161 insertions(+), 69 deletions(-) diff --git a/lib/application.js b/lib/application.js index a2209ec8..ebccabd5 100644 --- a/lib/application.js +++ b/lib/application.js @@ -399,26 +399,25 @@ app.enableAuth = function(options) { } }; - this._verifyAuthModelRelations(); - - this.isAuthEnabled = true; + return this._verifyAuthModelRelations(app._warnOnBadAuthModelRelations) + .then(() => { + this.isAuthEnabled = true; + }); }; -app._verifyAuthModelRelations = function() { +app._verifyAuthModelRelations = function(warnFn) { let self = this; // Allow unit-tests (but also LoopBack users) to disable the warnings - const warnOnBadSetup = self.get('_disableAuthModelRelationsWarnings') !== true; + const warnOnBadSetup = this.get('_warnOnBadAuthModelsSetup') !== false; // Prevent the app from being killed although the configuration is inconsistent - const dismissRejection = self.get('_dismissBadUserConfigRejection') !== true; + const abortOnBadUserSetup = this.get('_abortOnBadUserSetup') !== false; - const AccessToken = self.registry.findModel('AccessToken'); - const User = self.registry.findModel('User'); - const models = self.models(); + const AccessToken = this.registry.findModel('AccessToken'); + const User = this.registry.findModel('User'); + const models = this.models(); - verifyUserModelsSetup().then(()=> { - verifyRelationsSetup(); - }); + return verifyUserModelsSetup().then(verifyRelationsSetup); function verifyUserModelsSetup() { let userModels = models.filter(model => { @@ -428,25 +427,22 @@ app._verifyAuthModelRelations = function() { // we use Promise.reflect() here to let all the tests go without throwing, // this way we can get all the logs for all models we are checking - return Promise.map(userModels, (model) => { - return scheduleVerification(model, hasMultipleUserModelsConfig).reflect(); - }) - .then((inspections) => { + return Promise.map(userModels, + model => scheduleVerification(model, hasMultipleUserModelsConfig).reflect() + ) + .then(inspections => { // proceed to next checkups if no error - let hasErrors = inspections.filter(inspection => { - return inspection.isRejected(); - }); + let hasErrors = inspections.filter(inspection => inspection.isRejected()); if (!hasErrors.length) { return; } // else log and eventually kill the app - console.warn('Application setup is inconsistent: some models are set to use ' + - 'the Multiple user models configuration while some other models are set to ' + - 'use the Single user model configuration. The model config is listed below.'); + warnFn('BAD_USER_MODELS_SETUP'); // detailed logs inspections.forEach(inspection => { + console.log(inspection); let modelSetup = (inspection.value() || inspection.reason()).modelSetup; console.warn( 'Model %j of type %j is set to use the %j user model config', @@ -456,11 +452,14 @@ app._verifyAuthModelRelations = function() { ); }); - delete self.hasMultipleUserModelsConfig; + delete this.hasMultipleUserModelsConfig; - if (!dismissRejection) { - throw new Error('Application setup is inconsistent, please see the log for more ' + - 'information'); + if (abortOnBadUserSetup) { + const msg = 'Application setup is inconsistent, please see the log ' + + 'for more information'; + const error = new Error(msg); + error.code = 'BAD_USER_MODELS_SETUP'; + throw error; } }); } @@ -501,7 +500,8 @@ app._verifyAuthModelRelations = function() { if (model === User || model.prototype instanceof User) { isInstanceOfUser = true; const hasManyTokens = model.relations && model.relations.accessTokens; - hasMultipleUserModelsConfig = !!hasManyTokens.polymorphic; + // TODO: handle when hasManyTokens is undefined? + hasMultipleUserModelsConfig = !!(hasManyTokens.polymorphic); } else if (model === AccessToken || model.prototype instanceof AccessToken) { const belongsToUser = model.relations && model.relations.user; // the test on belongsToUser is required as we allow AccessToken model not @@ -512,7 +512,7 @@ app._verifyAuthModelRelations = function() { let modelSetup = { model: model.modelName, - instanceOf: isInstanceOfUser ? 'User' : 'AccesToken', + instanceOf: isInstanceOfUser ? 'User' : 'AccessToken', hasMultipleUserModelsConfig, }; @@ -531,30 +531,18 @@ app._verifyAuthModelRelations = function() { function verifyAccessTokenRelations(Model) { if (!warnOnBadSetup) return; - const belongsToUser = Model.relations && Model.relations.user; if (belongsToUser) return; + const modelFrom = Model.modelName; const relationsConfig = Model.settings.relations || {}; - const userName = (relationsConfig.user || {}).model; - if (userName) { - console.warn( - 'The model %j configures "belongsTo User-like models" relation ' + - 'with target model %j. However, the model %j is not attached to ' + - 'the application and therefore cannot be used by this relation. ' + - 'This typically happens when the application has a custom ' + - 'custom User subclass, but does not fix AccessToken relations ' + - 'to use this new model.\n' + - 'Learn more at http://ibm.biz/setup-loopback-auth', - Model.modelName, userName, userName); + const modelTo = (relationsConfig.user || {}).model; + if (modelTo) { + warnFn('CUSTOM_USER_MODEL_NOT_AVAILABLE', {modelFrom, modelTo}); return; } - console.warn( - 'The model %j does not have "belongsTo User-like model" relation ' + - 'configured.\n' + - 'Learn more at http://ibm.biz/setup-loopback-auth', - Model.modelName); + warnFn('MISSING_RELATION_TO_USER', {modelFrom}); } function verifyUserRelations(Model) { @@ -563,10 +551,49 @@ app._verifyAuthModelRelations = function() { const hasManyTokens = Model.relations && Model.relations.accessTokens; if (hasManyTokens) return; + const modelFrom = Model.modelName; const relationsConfig = Model.settings.relations || {}; - const accessTokenName = (relationsConfig.accessTokens || {}).model; - if (accessTokenName) { - console.warn( + const modelTo = (relationsConfig.accessTokens || {}).model; + if (modelTo) { + warnFn('CUSTOM_ACCESS_TOKEN_MODEL_NOT_AVAILABLE', {modelFrom, modelTo}); + return; + } + + warnFn('MISSING_RELATION_TO_ACCESS_TOKEN', {modelFrom}); + } +}; + +// function warnOnBadAuthModelRelations(code, args) { +app._warnOnBadAuthModelRelations = function(code, args) { + switch (code) { + case 'BAD_USER_MODELS_SETUP': + g.warn('Application setup is inconsistent: some models are set to use ' + + 'the Multiple user models configuration while some other models are set to ' + + 'use the Single user model configuration. The model config is listed below.'); + break; + + case 'CUSTOM_USER_MODEL_NOT_AVAILABLE': + g.warn( + 'The model %j configures "belongsTo User-like models" relation ' + + 'with target model %j. However, the model %j is not attached to ' + + 'the application and therefore cannot be used by this relation. ' + + 'This typically happens when the application has a custom ' + + 'custom User subclass, but does not fix AccessToken relations ' + + 'to use this new model.\n' + + 'Learn more at http://ibm.biz/setup-loopback-auth', + args.modelFrom, args.modelTo, args.modelTo); + break; + + case 'MISSING_RELATION_TO_USER': + g.warn( + 'The model %j does not have "belongsTo User-like model" relation ' + + 'configured.\n' + + 'Learn more at http://ibm.biz/setup-loopback-auth', + args.modelFrom); + break; + + case 'CUSTOM_ACCESS_TOKEN_MODEL_NOT_AVAILABLE': + g.warn( 'The model %j configures "hasMany AccessToken-like models" relation ' + 'with target model %j. However, the model %j is not attached to ' + 'the application and therefore cannot be used by this relation. ' + @@ -574,15 +601,15 @@ app._verifyAuthModelRelations = function() { 'AccessToken subclass, but does not fix User relations to use this ' + 'new model.\n' + 'Learn more at http://ibm.biz/setup-loopback-auth', - Model.modelName, accessTokenName, accessTokenName); - return; - } + args.modelFrom, args.modelTo, args.modelTo); + break; - console.warn( - 'The model %j does not have "hasMany AccessToken-like models" relation ' + - 'configured.\n' + - 'Learn more at http://ibm.biz/setup-loopback-auth', - Model.modelName); + case 'MISSING_RELATION_TO_ACCESS_TOKEN': + g.warn( + 'The model %j does not have "hasMany AccessToken-like models" relation ' + + 'configured.\n' + + 'Learn more at http://ibm.biz/setup-loopback-auth', + args.modelFrom); } }; diff --git a/test/app.test.js b/test/app.test.js index ab56eff3..dabc0790 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -18,6 +18,8 @@ var expect = require('./helpers/expect'); var it = require('./util/it'); var request = require('supertest'); +var Promise = require('bluebird'); + describe('app', function() { var app; beforeEach(function() { @@ -859,10 +861,11 @@ describe('app', function() { }); describe.onServer('enableAuth', function() { - it('should set app.isAuthEnabled to true', function() { + it('sets app.isAuthEnabled to true', function() { expect(app.isAuthEnabled).to.not.equal(true); - app.enableAuth(); - expect(app.isAuthEnabled).to.equal(true); + return app.enableAuth().then(() => { + expect(app.isAuthEnabled).to.equal(true); + }); }); it('auto-configures required models to provided dataSource', function() { @@ -871,14 +874,14 @@ describe('app', function() { require('../lib/builtin-models')(app.registry); var db = app.dataSource('db', {connector: 'memory'}); - app.enableAuth({dataSource: 'db'}); + return app.enableAuth({dataSource: 'db'}).then(() => { + expect(Object.keys(app.models)).to.include.members(AUTH_MODELS); - expect(Object.keys(app.models)).to.include.members(AUTH_MODELS); - - AUTH_MODELS.forEach(function(m) { - var Model = app.models[m]; - expect(Model.dataSource, m + '.dataSource').to.equal(db); - expect(Model.shared, m + '.shared').to.equal(m === 'User'); + AUTH_MODELS.forEach(function(m) { + var Model = app.models[m]; + expect(Model.dataSource, m + '.dataSource').to.equal(db); + expect(Model.shared, m + '.shared').to.equal(m === 'User'); + }); }); }); @@ -892,9 +895,71 @@ describe('app', function() { const AccessToken = app.registry.getModel('AccessToken'); AccessToken.settings.relations.user.model = 'Customer'; - app.enableAuth({dataSource: 'db'}); + return app.enableAuth({dataSource: 'db'}).then(() => { + expect(Object.keys(app.models)).to.not.include('User'); + }); + }); - expect(Object.keys(app.models)).to.not.include('User'); + describe('auth models config health check', function() { + var app, warnings; + beforeEach(function() { + app = loopback({localRegistry: true, loadBuiltinModels: true}); + app.dataSource('db', {connector: 'memory'}); + + warnings = []; + function warnFn(code, args) { + warnings.push(Object.assign(args || {}, {code})); + } + app._warnOnBadAuthModelRelations = warnFn; + }); + + it('sets app.hasMultipleUserModelsConfig to false if the app ' + + 'has a single User model correctly configured', function() { + return app.enableAuth({dataSource: 'db'}).then(() => { + expect(app.hasMultipleUserModelsConfig).to.be.false(); + }); + }); + + it('sets app.hasMultipleUserModelsConfig to true if the app ' + + 'has multiple User models correctly configured', function() { + var Merchant = createModel(app, 'Merchant', {base: 'User'}); + var Customer = createModel(app, 'Customer', {base: 'User'}); + var Token = createModel(app, 'Token', {base: 'AccessToken'}); + + // Update AccessToken and Users to bind them through polymorphic relations + Token.belongsTo('user', {idName: 'id', polymorphic: {idType: 'string', + foreignKey: 'userId', discriminator: 'principalType'}}); + Merchant.hasMany('accessTokens', {model: Token, polymorphic: {foreignKey: 'userId', + discriminator: 'principalType'}}); + Customer.hasMany('accessTokens', {model: Token, polymorphic: {foreignKey: 'userId', + discriminator: 'principalType'}}); + return app.enableAuth({dataSource: 'db'}).then(() => { + expect(app.hasMultipleUserModelsConfig).to.be.true(); + }); + }); + + it('warns if a custom user model is referenced in the ' + + 'AccessToken "user" relation, but is not available', function() { + // Set AccessToken's belongsTo relation "user" to use a custom user model + // This model is deliberately not available + const AccessToken = app.registry.getModel('AccessToken'); + AccessToken.settings.relations.user.model = 'Customer'; + + return app.enableAuth({dataSource: 'db'}).then(() => { + expect(warnings).to.eql([{ + code: 'CUSTOM_USER_MODEL_NOT_AVAILABLE', + modelFrom: 'AccessToken', + modelTo: 'Customer', + }]); + }); + }); + + // helpers + function createModel(app, name, options) { + var model = app.registry.createModel(Object.assign({name: name}, options)); + app.model(model, {dataSource: 'db'}); + return model; + } }); }); diff --git a/test/user.test.js b/test/user.test.js index cac82d48..877a1486 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2442,7 +2442,7 @@ describe('User', function() { it('handles subclassed user with no accessToken relation', () => { // setup a new LoopBack app, we don't want to use shared models app = loopback({localRegistry: true, loadBuiltinModels: true}); - app.set('_disableAuthModelRelationsWarnings', true); + app.set('_warnOnBadAuthModelsSetup', false); app.set('remoting', {errorHandler: {debug: true, log: false}}); app.dataSource('db', {connector: 'memory'}); const User = app.registry.createModel({