diff --git a/common/models/access-token.js b/common/models/access-token.js index 43acd2ef..76d37081 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -146,6 +146,16 @@ module.exports = function(AccessToken) { var userRelation = AccessToken.relations.user; // may not be set up var User = userRelation && userRelation.modelTo; + // redefine user model if accessToken's principalType is available + if (this.principalType) { + User = AccessToken.registry.findModel(this.principalType); + if (!User) { + process.nextTick(function() { + return cb(null, false); + }); + } + } + var now = Date.now(); var created = this.created.getTime(); var elapsedSeconds = (now - created) / 1000; @@ -157,14 +167,18 @@ module.exports = function(AccessToken) { elapsedSeconds < secondsToLive; if (isValid) { - cb(null, isValid); + process.nextTick(function() { + cb(null, isValid); + }); } else { this.destroy(function(err) { cb(err, isValid); }); } } catch (e) { - cb(e); + process.nextTick(function() { + cb(e); + }); } }; diff --git a/common/models/acl.js b/common/models/acl.js index 5e02e613..4b1e7c27 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -395,6 +395,7 @@ module.exports = function(ACL) { self.resolveRelatedModels(); var roleModel = self.roleModel; + context.registry = this.registry; if (!(context instanceof AccessContext)) { context = new AccessContext(context); } @@ -480,6 +481,7 @@ module.exports = function(ACL) { assert(token, 'Access token is required'); if (!callback) callback = utils.createPromiseCallback(); var context = new AccessContext({ + registry: this.registry, accessToken: token, model: model, property: method, @@ -514,6 +516,7 @@ module.exports = function(ACL) { cb = cb || utils.createPromiseCallback(); type = type || ACL.ROLE; this.resolveRelatedModels(); + switch (type) { case ACL.ROLE: this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb); @@ -527,11 +530,20 @@ module.exports = function(ACL) { {where: {or: [{name: id}, {email: id}, {id: id}]}}, cb); break; default: - process.nextTick(function() { - var err = new Error(g.f('Invalid principal type: %s', type)); - err.statusCode = 400; - cb(err); - }); + // try resolving a user model that matches principalType + var userModel = this.registry.findModel(type); + if (userModel) { + userModel.findOne( + {where: {or: [{username: id}, {email: id}, {id: id}]}}, + cb); + } else { + process.nextTick(function() { + var err = new Error(g.f('Invalid principal type: %s', type)); + err.statusCode = 400; + err.code = 'INVALID_PRINCIPAL_TYPE'; + cb(err); + }); + } } return cb.promise; }; diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js index 8dfa5808..b038ea8a 100644 --- a/common/models/role-mapping.js +++ b/common/models/role-mapping.js @@ -63,9 +63,17 @@ module.exports = function(RoleMapping) { RoleMapping.prototype.user = function(callback) { callback = callback || utils.createPromiseCallback(); this.constructor.resolveRelatedModels(); + var userModel; if (this.principalType === RoleMapping.USER) { - var userModel = this.constructor.userModel; + userModel = this.constructor.userModel; + userModel.findById(this.principalId, callback); + return callback.promise; + } + + // try resolving a user model that matches principalType + userModel = this.constructor.registry.findModel(this.principalType); + if (userModel) { userModel.findById(this.principalId, callback); } else { process.nextTick(function() { diff --git a/common/models/role-mapping.json b/common/models/role-mapping.json index 732e24af..23c9d8ab 100644 --- a/common/models/role-mapping.json +++ b/common/models/role-mapping.json @@ -9,7 +9,7 @@ }, "principalType": { "type": "string", - "description": "The principal type, such as user, application, or role" + "description": "The principal type, such as USER, APPLICATION, ROLE, or user model name in case of multiple user models" }, "principalId": { "type": "string", diff --git a/common/models/role.js b/common/models/role.js index 1415f765..8bdd4393 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -9,9 +9,9 @@ var debug = require('debug')('loopback:security:role'); var assert = require('assert'); var async = require('async'); var utils = require('../../lib/utils'); - -var AccessContext = require('../../lib/access-context').AccessContext; - +var ctx = require('../../lib/access-context'); +var AccessContext = ctx.AccessContext; +var Principal = ctx.Principal; var RoleMapping = loopback.RoleMapping; assert(RoleMapping, 'RoleMapping model must be defined before Role model'); @@ -70,7 +70,8 @@ module.exports = function(Role) { callback = utils.createPromiseCallback(); } } - if (!query) query = {}; + query = query || {}; + query.where = query.where || {}; roleModel.resolveRelatedModels(); var relsToModels = { @@ -86,8 +87,29 @@ module.exports = function(Role) { roles: ACL.ROLE, }; - var model = relsToModels[rel]; - listByPrincipalType(this, model, relsToTypes[rel], query, callback); + var principalModel = relsToModels[rel]; + var principalType = relsToTypes[rel]; + + // redefine user model and user type if user principalType is custom (available and not "USER") + var isCustomUserPrincipalType = rel === 'users' && + query.where.principalType && + query.where.principalType !== RoleMapping.USER; + + if (isCustomUserPrincipalType) { + var registry = this.constructor.registry; + principalModel = registry.findModel(query.where.principalType); + principalType = query.where.principalType; + } + // make sure we don't keep principalType in userModel query + delete query.where.principalType; + + if (principalModel) { + listByPrincipalType(this, principalModel, principalType, query, callback); + } else { + process.nextTick(function() { + callback(null, []); + }); + } return callback.promise; }; }); @@ -102,10 +124,11 @@ module.exports = function(Role) { * @param {Function} [callback] callback function called with `(err, models)` arguments. */ function listByPrincipalType(context, model, principalType, query, callback) { - if (callback === undefined) { + if (callback === undefined && typeof query === 'function') { callback = query; query = {}; } + query = query || {}; roleModel.roleMappingModel.find({ where: {roleId: context.id, principalType: principalType}, @@ -303,6 +326,7 @@ module.exports = function(Role) { * @promise */ Role.isInRole = function(role, context, callback) { + context.registry = this.registry; if (!(context instanceof AccessContext)) { context = new AccessContext(context); } @@ -421,9 +445,9 @@ module.exports = function(Role) { callback = utils.createPromiseCallback(); } } - if (!options) options = {}; + context.registry = this.registry; if (!(context instanceof AccessContext)) { context = new AccessContext(context); } diff --git a/common/models/user.js b/common/models/user.js index 434afcd0..e25058e7 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -683,13 +683,18 @@ module.exports = function(User) { return process.nextTick(cb); var AccessToken = accessTokenRelation.modelTo; - var query = {userId: {inq: userIds}}; var tokenPK = AccessToken.definition.idName() || 'id'; if (options.accessToken && tokenPK in options.accessToken) { query[tokenPK] = {neq: options.accessToken[tokenPK]}; } - + // add principalType in AccessToken.query if using polymorphic relations + // between AccessToken and User + var relatedUser = AccessToken.relations.user; + var isRelationPolymorphic = relatedUser.polymorphic && !relatedUser.modelTo; + if (isRelationPolymorphic) { + query.principalType = this.modelName; + } AccessToken.deleteAll(query, options, cb); }; diff --git a/lib/access-context.js b/lib/access-context.js index b59f7086..d3076a06 100644 --- a/lib/access-context.js +++ b/lib/access-context.js @@ -32,9 +32,12 @@ function AccessContext(context) { } context = context || {}; + assert(context.registry, + 'Application registry is mandatory in AccessContext but missing in provided context'); + this.registry = context.registry; this.principals = context.principals || []; var model = context.model; - model = ('string' === typeof model) ? loopback.getModel(model) : model; + model = ('string' === typeof model) ? this.registry.getModel(model) : model; this.model = model; this.modelName = model && model.modelName; @@ -62,6 +65,7 @@ function AccessContext(context) { var principalType = context.principalType || Principal.USER; var principalId = context.principalId || undefined; var principalName = context.principalName || undefined; + if (principalId) { this.addPrincipal(principalType, principalId, principalName); } @@ -124,11 +128,25 @@ AccessContext.prototype.addPrincipal = function(principalType, principalId, prin * @returns {*} */ AccessContext.prototype.getUserId = function() { + var BaseUser = this.registry.getModel('User'); for (var i = 0; i < this.principals.length; i++) { var p = this.principals[i]; + var isBuiltinPrincipal = p.type === Principal.APP || + p.type === Principal.ROLE || + p.type == Principal.SCOPE; + if (isBuiltinPrincipal) continue; + + // the principalType must either be 'USER' if (p.type === Principal.USER) { return p.id; } + + // or permit to resolve a valid user model + var userModel = this.registry.findModel(p.type); + if (!userModel) continue; + if (userModel.prototype instanceof BaseUser) { + return p.id; + } } return null; }; @@ -189,8 +207,9 @@ AccessContext.prototype.debug = function() { * This class represents the abstract notion of a principal, which can be used * to represent any entity, such as an individual, a corporation, and a login id * @param {String} type The principal type - * @param {*} id The princiapl id + * @param {*} id The principal id * @param {String} [name] The principal name + * @param {String} modelName The principal model name * @returns {Principal} * @class */ diff --git a/test/multiple-user-principal-types.test.js b/test/multiple-user-principal-types.test.js new file mode 100644 index 00000000..9530e22e --- /dev/null +++ b/test/multiple-user-principal-types.test.js @@ -0,0 +1,444 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var expect = require('./helpers/expect'); +var request = require('supertest'); +var loopback = require('../'); +var ctx = require('../lib/access-context'); +var AccessContext = ctx.AccessContext; +var Principal = ctx.Principal; +var Promise = require('bluebird'); + +describe('Multiple users with custom principalType', function() { + this.timeout(10000); + + var commonCredentials = {email: 'foo@bar.com', password: 'bar'}; + var app, OneUser, AnotherUser, AccessToken, Role, + userFromOneModel, userFromAnotherModel, userRole, userOneBaseContext; + + beforeEach(function setupAppAndModels() { + // create a local app object that does not share state with other tests + app = loopback({localRegistry: true, loadBuiltinModels: true}); + app.dataSource('db', {connector: 'memory'}); + + var userModelOptions = { + base: 'User', + // forceId is set to false for the purpose of updating the same affected user within the + // `Email Update` test cases. + forceId: false, + // Speed up the password hashing algorithm for tests + saltWorkFactor: 4, + }; + + // create and attach 2 User-based models + OneUser = createUserModel(app, 'OneUser', userModelOptions); + AnotherUser = createUserModel(app, 'AnotherUser', userModelOptions); + + AccessToken = app.registry.getModel('AccessToken'); + app.model(AccessToken, {dataSource: 'db'}); + + Role = app.registry.getModel('Role'); + app.model(Role, {dataSource: 'db'}); + + // Update AccessToken and Users to bind them through polymorphic relations + AccessToken.belongsTo('user', {idName: 'id', polymorphic: {idType: 'string', + foreignKey: 'userId', discriminator: 'principalType'}}); + OneUser.hasMany('accessTokens', {polymorphic: {foreignKey: 'userId', + discriminator: 'principalType'}}); + AnotherUser.hasMany('accessTokens', {polymorphic: {foreignKey: 'userId', + discriminator: 'principalType'}}); + + app.enableAuth({dataSource: 'db'}); + app.use(loopback.token({model: AccessToken})); + + // create one user per user model to use them throughout the tests + return Promise.all([ + OneUser.create(commonCredentials), + AnotherUser.create(commonCredentials), + Role.create({name: 'userRole'}), + ]) + .spread(function(u1, u2, r) { + userFromOneModel = u1; + userFromAnotherModel = u2; + userRole = r; + userOneBaseContext = { + principalType: OneUser.modelName, + principalId: userFromOneModel.id, + }; + }); + }); + + describe('User.login', function() { + it('works for one user model and valid credentials', function() { + return OneUser.login(commonCredentials) + .then(function(accessToken) { + assertGoodToken(accessToken, userFromOneModel); + }); + }); + + it('works for a second user model and valid credentials', function() { + return AnotherUser.login(commonCredentials) + .then(function(accessToken) { + assertGoodToken(accessToken, userFromAnotherModel); + }); + }); + + it('fails when credentials are not correct', function() { + return OneUser.login({email: 'foo@bar.com', password: 'invalid'}) + .then( + function onSuccess() { + throw new Error('OneUser.login() should have failed'); + }, + function onError(err) { + expect(err).to.have.property('code', 'LOGIN_FAILED'); + } + ); + }); + }); + + function assertGoodToken(accessToken, user) { + if (accessToken instanceof AccessToken) { + accessToken = accessToken.toJSON(); + } + expect(accessToken.id, 'token id').to.have.lengthOf(64); + expect(accessToken).to.have.property('userId', user.id); + expect(accessToken).to.have.property('principalType', user.constructor.definition.name); + } + + describe('User.logout', function() { + it('logs out a user from user model 1 without logging out user from model 2', + function() { + var tokenOfOneUser; + return Promise.all([ + OneUser.login(commonCredentials), + AnotherUser.login(commonCredentials), + ]) + .spread(function(t1, t2) { + tokenOfOneUser = t1; + return OneUser.logout(tokenOfOneUser.id); + }) + .then(function() { + return AccessToken.find({}); + }) + .then(function(allTokens) { + var data = allTokens.map(function(token) { + return {userId: token.userId, principalType: token.principalType}; + }); + expect(data).to.eql([ + // no token for userFromAnotherModel + {userId: userFromAnotherModel.id, principalType: 'AnotherUser'}, + ]); + }); + }); + }); + + describe('Password Reset', function() { + describe('User.resetPassword(options)', function() { + var options = { + email: 'foo@bar.com', + redirect: 'http://foobar.com/reset-password', + }; + + it('creates a temp accessToken to allow a user to change password', + function() { + return Promise.all([ + OneUser.resetPassword({email: options.email}), + waitForResetRequestAndVerify, + ]); + }); + + function waitForResetRequestAndVerify() { + return waitForEvent(OneUser, 'resetPasswordRequest') + .then(function(info) { + assertGoodToken(info.accessToken, userFromOneModel); + return info.accessToken.user.getAsync(); + }) + .then(function(user) { + expect(user).to.have.property('id', userFromOneModel.id); + expect(user).to.have.property('email', userFromOneModel.email); + }); + } + }); + }); + + describe('AccessToken (session) invalidation when changing email', function() { + var anotherUserFromOneModel; + + it('impact only the related user', function() { + return OneUser.create({email: 'original@example.com', password: 'bar'}) + .then(function(u) { + anotherUserFromOneModel = u; + return Promise.all([ + OneUser.login({email: 'original@example.com', password: 'bar'}), + OneUser.login(commonCredentials), + AnotherUser.login(commonCredentials), + ]); + }) + .then(function() { + return anotherUserFromOneModel.updateAttribute('email', 'updated@example.com'); + }) + .then(function() { + // we need to sort on principalType to ensure stability in results' order + return AccessToken.find({'order': 'principalType ASC'}); + }) + .then(function(allTokens) { + var data = allTokens.map(function(token) { + return {userId: token.userId, principalType: token.principalType}; + }); + expect(data).to.eql([ + // no token for anotherUserFromOneModel + {userId: userFromAnotherModel.id, principalType: 'AnotherUser'}, + {userId: userFromOneModel.id, principalType: 'OneUser'}, + ]); + }); + }); + }); + + describe('AccessContext', function() { + var ThirdUser, userFromThirdModel, accessContext; + + beforeEach(function() { + accessContext = new AccessContext({registry: OneUser.registry}); + }); + + describe('getUserId()', function() { + it('returns userId although principals contain non USER principals', + function() { + return Promise.try(function() { + addToAccessContext([ + {type: Principal.ROLE}, + {type: Principal.APP}, + {type: Principal.SCOPE}, + {type: OneUser.modelName, id: userFromOneModel.id}, + ]); + var userId = accessContext.getUserId(); + expect(userId).to.equal(userFromOneModel.id); + }); + }); + + it('returns userId although principals contain invalid principals', + function() { + return Promise.try(function() { + addToAccessContext([ + {type: 'AccessToken'}, + {type: 'invalidModelName'}, + {type: OneUser.modelName, id: userFromOneModel.id}, + ]); + var userId = accessContext.getUserId(); + expect(userId).to.equal(userFromOneModel.id); + }); + }); + + it('supports any level of built-in User model inheritance', + function() { + ThirdUser = createUserModel(app, 'ThirdUser', {base: 'OneUser'}); + return ThirdUser.create(commonCredentials) + .then(function(userFromThirdModel) { + accessContext.addPrincipal(ThirdUser.modelName, userFromThirdModel.id); + var userId = accessContext.getUserId(); + expect(userId).to.equal(userFromThirdModel.id); + }); + }); + }); + + // helper + function addToAccessContext(list) { + list.forEach(function(principal) { + expect(principal).to.exist(); + accessContext.addPrincipal(principal.type, principal.id); + }); + } + }); + + describe('role model', function() { + this.timeout(10000); + + var RoleMapping, ACL, user; + + beforeEach(function() { + ACL = app.registry.getModel('ACL'); + app.model(ACL, {dataSource: 'db'}); + + RoleMapping = app.registry.getModel('RoleMapping'); + app.model(RoleMapping, {dataSource: 'db'}); + }); + + describe('role.users()', function() { + it('returns users when using custom user principalType', function() { + return userRole.principals.create( + {principalType: OneUser.modelName, principalId: userFromOneModel.id}) + .then(function() { + return userRole.users({where: {principalType: OneUser.modelName}}); + }) + .then(getIds) + .then(function(userIds) { + expect(userIds).to.eql([userFromOneModel.id]); + }); + }); + + it('returns empty array when using invalid principalType', function() { + return userRole.principals.create( + {principalType: 'invalidModelName', principalId: userFromOneModel.id}) + .then(function() { + return userRole.users({where: {principalType: 'invalidModelName'}}); + }) + .then(function(users) { + expect(users).to.be.empty(); + }); + }); + }); + + describe('principal.user()', function() { + it('returns the correct user instance', function() { + return userRole.principals.create( + {principalType: OneUser.modelName, principalId: userFromOneModel.id}) + .then(function(principal) { + return principal.user(); + }) + .then(function(user) { + expect(user).to.have.property('id', userFromOneModel.id); + }); + }); + + it('returns null when created with invalid principalType', function() { + return userRole.principals.create( + {principalType: 'invalidModelName', principalId: userFromOneModel.id}) + .then(function(principal) { + return principal.user(); + }) + .then(function(user) { + expect(user).to.not.exist(); + }); + }); + }); + + describe('isInRole() & getRole()', function() { + beforeEach(function() { + return userRole.principals.create({principalType: OneUser.modelName, + principalId: userFromOneModel.id}); + }); + + it('supports isInRole()', function() { + return Role.isInRole('userRole', userOneBaseContext) + .then(function(isInRole) { + expect(isInRole).to.be.true(); + }); + }); + + it('supports getRoles()', function() { + return Role.getRoles( + userOneBaseContext) + .then(function(roles) { + expect(roles).to.eql([ + Role.AUTHENTICATED, + Role.EVERYONE, + userRole.id, + ]); + }); + }); + }); + + describe('built-in role resolver', function() { + it('supports AUTHENTICATED', function() { + return Role.isInRole(Role.AUTHENTICATED, userOneBaseContext) + .then(function(isInRole) { + expect(isInRole).to.be.true(); + }); + }); + + it('supports UNAUTHENTICATED', function() { + return Role.isInRole(Role.UNAUTHENTICATED, userOneBaseContext) + .then(function(isInRole) { + expect(isInRole).to.be.false(); + }); + }); + + it('supports OWNER', function() { + var Album = app.registry.createModel('Album', { + name: String, + userId: Number, + }, { + relations: { + user: { + type: 'belongsTo', + model: 'OneUser', + foreignKey: 'userId', + }, + }, + }); + app.model(Album, {dataSource: 'db'}); + + return Album.create({name: 'album', userId: userFromOneModel.id}) + .then(function(album) { + return Role.isInRole( + Role.OWNER, + { + principalType: OneUser.modelName, + principalId: userFromOneModel.id, + model: Album, + id: album.id, + }); + }) + .then(function(isInRole) { + expect(isInRole).to.be.true(); + }); + }); + }); + + describe('isMappedToRole()', function() { + beforeEach(function() { + return userRole.principals.create(userOneBaseContext); + }); + + it('resolves user by id using custom user principalType', function() { + return ACL.resolvePrincipal(OneUser.modelName, userFromOneModel.id) + .then(function(principal) { + expect(principal.id).to.eql(userFromOneModel.id); + }); + }); + + it('throws error with code \'INVALID_PRINCIPAL_TYPE\' when principalType is incorrect', + function() { + return ACL.resolvePrincipal('incorrectPrincipalType', userFromOneModel.id) + .then( + function onSuccess() { + throw new Error('ACL.resolvePrincipal() should have failed'); + }, + function onError(err) { + expect(err).to.have.property('statusCode', 400); + expect(err).to.have.property('code', 'INVALID_PRINCIPAL_TYPE'); + } + ); + }); + + it('reports isMappedToRole by user.username using custom user principalType', + function() { + return ACL.isMappedToRole(OneUser.modelName, userFromOneModel.username, 'userRole') + .then(function(isMappedToRole) { + expect(isMappedToRole).to.be.true(); + }); + }); + }); + }); + + // helpers + function createUserModel(app, name, options) { + var model = app.registry.createModel(Object.assign({name: name}, options)); + app.model(model, {dataSource: 'db'}); + model.setMaxListeners(0); // allow many User.afterRemote's to be called + return model; + } + + function waitForEvent(emitter, name) { + return new Promise(function(resolve, reject) { + emitter.once(name, resolve); + }); + }; + + function getIds(array) { + return array.map(function(it) { return it.id; }); + }; +}); diff --git a/test/user.test.js b/test/user.test.js index 4f5ba5e0..dec0459a 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -28,11 +28,12 @@ describe('User', function() { var validMixedCaseEmailCredentials = {email: 'Foo@bar.com', password: 'bar'}; var invalidCredentials = {email: 'foo1@bar.com', password: 'invalid'}; var incompleteCredentials = {password: 'bar1'}; + var validCredentialsUser, validCredentialsEmailVerifiedUser; // Create a local app variable to prevent clashes with the global // variable shared by all tests. While this should not be necessary if // the tests were written correctly, it turns out that's not the case :( - var app; + var app = null; beforeEach(function setupAppAndModels(done) { // override the global app object provided by test/support.js @@ -89,8 +90,12 @@ describe('User', function() { User.create(validCredentials, function(err, user) { if (err) return done(err); - - User.create(validCredentialsEmailVerified, done); + validCredentialsUser = user; + User.create(validCredentialsEmailVerified, function(err, user) { + if (err) return done(err); + validCredentialsEmailVerifiedUser = user; + done(); + }); }); }); @@ -279,14 +284,15 @@ describe('User', function() { it('invalidates the user\'s accessToken when the user is deleted all', function(done) { var userIds = []; - var accessTokenId; + var users; async.series([ function(next) { User.create([ {name: 'myname', email: 'b@c.com', password: 'bar'}, {name: 'myname', email: 'd@c.com', password: 'bar'}, - ], function(err, users) { - userIds = users.map(function(u) { + ], function(err, createdUsers) { + users = createdUsers; + userIds = createdUsers.map(function(u) { return u.pk; }); next(err); @@ -294,17 +300,15 @@ describe('User', function() { }, function(next) { User.login({email: 'b@c.com', password: 'bar'}, function(err, accessToken) { - accessTokenId = accessToken.userId; if (err) return next(err); - assert(accessTokenId); + assertGoodToken(accessToken, users[0]); next(); }); }, function(next) { User.login({email: 'd@c.com', password: 'bar'}, function(err, accessToken) { - accessTokenId = accessToken.userId; if (err) return next(err); - assert(accessTokenId); + assertGoodToken(accessToken, users[1]); next(); }); }, @@ -424,12 +428,11 @@ describe('User', function() { }); it('allows login with password exactly 72 characters long', function(done) { - User.create({email: 'b@c.com', password: pass72Char}, function(err) { + User.create({email: 'b@c.com', password: pass72Char}, function(err, user) { if (err) return done(err); User.login({email: 'b@c.com', password: pass72Char}, function(err, accessToken) { if (err) return done(err); - assertGoodToken(accessToken); - assert(accessToken.id); + assertGoodToken(accessToken, user); done(); }); }); @@ -502,9 +505,7 @@ describe('User', function() { describe('User.login', function() { it('Login a user by providing credentials', function(done) { User.login(validCredentials, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); - assert.equal(accessToken.id.length, 64); + assertGoodToken(accessToken, validCredentialsUser); done(); }); @@ -513,9 +514,7 @@ describe('User', function() { it('Login a user by providing email credentials (email case-sensitivity off)', function(done) { User.settings.caseSensitiveEmail = false; User.login(validMixedCaseEmailCredentials, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); - assert.equal(accessToken.id.length, 64); + assertGoodToken(accessToken, validCredentialsUser); done(); }); @@ -531,10 +530,8 @@ describe('User', function() { it('Login a user by providing credentials with TTL', function(done) { User.login(validCredentialsWithTTL, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl); - assert.equal(accessToken.id.length, 64); done(); }); @@ -547,10 +544,8 @@ describe('User', function() { User.findById(accessToken.userId, function(err, user) { user.createAccessToken(120, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, 120); - assert.equal(accessToken.id.length, 64); done(); }); @@ -566,10 +561,8 @@ describe('User', function() { User.findById(accessToken.userId, function(err, user) { user.createAccessToken(120) .then(function(accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, 120); - assert.equal(accessToken.id.length, 64); done(); }) @@ -588,17 +581,13 @@ describe('User', function() { this.accessTokens.create({ttl: ttl / 2}, cb); }; User.login(validCredentialsWithTTL, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, 1800); - assert.equal(accessToken.id.length, 64); User.findById(accessToken.userId, function(err, user) { user.createAccessToken(120, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, 60); - assert.equal(accessToken.id.length, 64); // Restore create access token User.prototype.createAccessToken = createToken; @@ -617,18 +606,14 @@ describe('User', function() { this.accessTokens.create({ttl: ttl / 2, scopes: options.scope}, cb); }; User.login(validCredentialsWithTTLAndScope, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, 1800); - assert.equal(accessToken.id.length, 64); assert.equal(accessToken.scopes, 'all'); User.findById(accessToken.userId, function(err, user) { user.createAccessToken(120, {scope: 'default'}, function(err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); assert.equal(accessToken.ttl, 60); - assert.equal(accessToken.id.length, 64); assert.equal(accessToken.scopes, 'default'); // Restore create access token User.prototype.createAccessToken = createToken; @@ -652,13 +637,13 @@ describe('User', function() { it('Login should only allow correct credentials - promise variant', function(done) { User.login(invalidCredentials) .then(function(accessToken) { - assert(!accessToken); + expect(accessToken, 'accessToken').to.not.exist(); done(); }) .catch(function(err) { - assert(err); - assert.equal(err.code, 'LOGIN_FAILED'); + expect(err, 'err').to.exist(); + expect(err).to.have.property('code', 'LOGIN_FAILED'); done(); }); @@ -666,8 +651,8 @@ describe('User', function() { it('Login a user providing incomplete credentials', function(done) { User.login(incompleteCredentials, function(err, accessToken) { - assert(err); - assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED'); + expect(err, 'err').to.exist(); + expect(err).to.have.property('code', 'USERNAME_EMAIL_REQUIRED'); done(); }); @@ -676,13 +661,13 @@ describe('User', function() { it('Login a user providing incomplete credentials - promise variant', function(done) { User.login(incompleteCredentials) .then(function(accessToken) { - assert(!accessToken); + expect(accessToken, 'accessToken').to.not.exist(); done(); }) .catch(function(err) { - assert(err); - assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED'); + expect(err, 'err').to.exist(); + expect(err).to.have.property('code', 'USERNAME_EMAIL_REQUIRED'); done(); }); @@ -699,9 +684,7 @@ describe('User', function() { var accessToken = res.body; - assert(accessToken.userId); - assert(accessToken.id); - assert.equal(accessToken.id.length, 64); + assertGoodToken(accessToken, validCredentialsUser); assert(accessToken.user === undefined); done(); @@ -811,8 +794,11 @@ describe('User', function() { }); }); - function assertGoodToken(accessToken) { - assert(accessToken.userId); + function assertGoodToken(accessToken, user) { + if (accessToken instanceof AccessToken) { + accessToken = accessToken.toJSON(); + } + expect(accessToken).to.have.property('userId', user.pk); assert(accessToken.id); assert.equal(accessToken.id.length, 64); } @@ -880,7 +866,7 @@ describe('User', function() { it('Login a user by with email verification', function(done) { User.login(validCredentialsEmailVerified, function(err, accessToken) { - assertGoodToken(accessToken); + assertGoodToken(accessToken, validCredentialsEmailVerifiedUser); done(); }); @@ -889,7 +875,7 @@ describe('User', function() { it('Login a user by with email verification - promise variant', function(done) { User.login(validCredentialsEmailVerified) .then(function(accessToken) { - assertGoodToken(accessToken); + assertGoodToken(accessToken, validCredentialsEmailVerifiedUser); done(); }) @@ -909,7 +895,7 @@ describe('User', function() { var accessToken = res.body; - assertGoodToken(accessToken); + assertGoodToken(accessToken, validCredentialsEmailVerifiedUser); assert(accessToken.user === undefined); done(); @@ -1079,8 +1065,7 @@ describe('User', function() { it('logs in a user by with realm', function(done) { User.login(credentialWithRealm, function(err, accessToken) { - assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.pk); + assertGoodToken(accessToken, user1); done(); }); @@ -1088,8 +1073,7 @@ describe('User', function() { it('logs in a user by with realm in username', function(done) { User.login(credentialRealmInUsername, function(err, accessToken) { - assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.pk); + assertGoodToken(accessToken, user1); done(); }); @@ -1097,8 +1081,7 @@ describe('User', function() { it('logs in a user by with realm in email', function(done) { User.login(credentialRealmInEmail, function(err, accessToken) { - assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.pk); + assertGoodToken(accessToken, user1); done(); }); @@ -1115,8 +1098,7 @@ describe('User', function() { it('logs in a user by with realm', function(done) { User.login(credentialWithRealm, function(err, accessToken) { - assertGoodToken(accessToken); - assert.equal(accessToken.userId, user1.pk); + assertGoodToken(accessToken, user1); done(); }); @@ -1139,7 +1121,7 @@ describe('User', function() { login(logout); function login(fn) { - User.login({email: 'foo@bar.com', password: 'bar'}, fn); + User.login(validCredentials, fn); } function logout(err, accessToken) { @@ -1152,7 +1134,7 @@ describe('User', function() { login(logout); function login(fn) { - User.login({email: 'foo@bar.com', password: 'bar'}, fn); + User.login(validCredentials, fn); } function logout(err, accessToken) { @@ -1171,14 +1153,12 @@ describe('User', function() { .post('/test-users/login') .expect('Content-Type', /json/) .expect(200) - .send({email: 'foo@bar.com', password: 'bar'}) + .send(validCredentials) .end(function(err, res) { if (err) return done(err); var accessToken = res.body; - - assert(accessToken.userId); - assert(accessToken.id); + assertGoodToken(accessToken, validCredentialsUser); fn(null, accessToken.id); });