Merge pull request #2971 from ebarault/enable-multiple-user-models

Enable multiple user models inheriting from base class User
This commit is contained in:
Miroslav Bajtoš 2017-02-02 10:03:01 +01:00 committed by GitHub
commit 304ecc4784
9 changed files with 597 additions and 91 deletions

View File

@ -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);
});
}
};

View File

@ -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;
};

View File

@ -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() {

View File

@ -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",

View File

@ -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);
}

View File

@ -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);
};

View File

@ -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
*/

View File

@ -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; });
};
});

View File

@ -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);
});