loopback/test/user.test.js

2255 lines
71 KiB
JavaScript
Raw Normal View History

2016-05-03 22:50:21 +00:00
// 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
require('./support');
var loopback = require('../');
var User, AccessToken;
2016-07-07 15:30:57 +00:00
var async = require('async');
2014-11-21 02:35:36 +00:00
describe('User', function() {
2016-07-29 18:41:43 +00:00
this.timeout(10000);
var validCredentialsEmail = 'foo@bar.com';
var validCredentials = { email: validCredentialsEmail, password: 'bar' };
var validCredentialsEmailVerified = {
email: 'foo1@bar.com', password: 'bar1', emailVerified: true };
var validCredentialsEmailVerifiedOverREST = {
email: 'foo2@bar.com', password: 'bar2', emailVerified: true };
var validCredentialsWithTTL = { email: 'foo@bar.com', password: 'bar', ttl: 3600 };
var validCredentialsWithTTLAndScope = {
email: 'foo@bar.com', password: 'bar', ttl: 3600, scope: 'all' };
var validMixedCaseEmailCredentials = { email: 'Foo@bar.com', password: 'bar' };
var invalidCredentials = { email: 'foo1@bar.com', password: 'invalid' };
var incompleteCredentials = { password: 'bar1' };
// 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;
beforeEach(function setupAppAndModels(done) {
// override the global app object provided by test/support.js
// and create a local one that does not share state with other tests
app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('remoting', { errorHandler: { debug: true, log: false }});
app.dataSource('db', { connector: 'memory' });
// setup Email model, it's needed by User tests
app.dataSource('email', {
connector: loopback.Mail,
transports: [{ type: 'STUB' }],
});
var Email = app.registry.getModel('Email');
app.model(Email, { dataSource: 'email' });
// attach User and related models
2016-08-30 19:09:11 +00:00
// forceId is set to false for the purpose of updating the same affected user within the
// `Email Update` test cases.
User = app.registry.createModel('TestUser', {}, {
base: 'User',
http: { path: 'test-users' },
2016-08-30 19:09:11 +00:00
forceId: false,
});
app.model(User, { dataSource: 'db' });
AccessToken = app.registry.getModel('AccessToken');
app.model(AccessToken, { dataSource: 'db' });
User.email = Email;
// Update the AccessToken relation to use the subclass of User
AccessToken.belongsTo(User, { as: 'user', foreignKey: 'userId' });
User.hasMany(AccessToken, { as: 'accessTokens', foreignKey: 'userId' });
// Speed up the password hashing algorithm
// for tests using the built-in User model
User.settings.saltWorkFactor = 4;
2014-01-23 22:39:15 +00:00
// allow many User.afterRemote's to be called
User.setMaxListeners(0);
app.enableAuth({ dataSource: 'db' });
app.use(loopback.token({ model: AccessToken }));
2013-07-16 17:49:25 +00:00
app.use(loopback.rest());
User.create(validCredentials, function(err, user) {
if (err) return done(err);
User.create(validCredentialsEmailVerified, done);
});
2013-07-02 23:51:38 +00:00
});
2014-11-21 02:35:36 +00:00
describe('User.create', function() {
2013-07-16 20:41:17 +00:00
it('Create a new user', function(done) {
User.create({ email: 'f@b.com', password: 'bar' }, function(err, user) {
assert(!err);
assert(user.id);
assert(user.email);
done();
});
});
it('Create a new user (email case-sensitivity off)', function(done) {
User.settings.caseSensitiveEmail = false;
User.create({ email: 'F@b.com', password: 'bar' }, function(err, user) {
if (err) return done(err);
assert(user.id);
assert.equal(user.email, user.email.toLowerCase());
done();
});
});
it('Create a new user (email case-sensitive)', function(done) {
User.create({ email: 'F@b.com', password: 'bar' }, function(err, user) {
if (err) return done(err);
assert(user.id);
assert(user.email);
assert.notEqual(user.email, user.email.toLowerCase());
done();
});
});
2014-11-21 02:35:36 +00:00
it('Email is required', function(done) {
User.create({ password: '123' }, function(err) {
2014-05-20 21:39:28 +00:00
assert(err);
assert.equal(err.name, 'ValidationError');
2014-05-20 21:39:28 +00:00
assert.equal(err.statusCode, 422);
assert.equal(err.details.context, User.modelName);
assert.deepEqual(err.details.codes.email, ['presence']);
done();
});
});
// will change in future versions where password will be optional by default
it('Password is required', function(done) {
var u = new User({ email: '123@456.com' });
User.create({ email: 'c@d.com' }, function(err) {
assert(err);
done();
});
});
2013-07-16 20:41:17 +00:00
it('Requires a valid email', function(done) {
User.create({ email: 'foo@', password: '123' }, function(err) {
assert(err);
assert.equal(err.name, 'ValidationError');
assert.equal(err.statusCode, 422);
assert.equal(err.details.context, User.modelName);
assert.deepEqual(err.details.codes.email, ['custom.email']);
done();
});
});
it('allows TLD domains in email', function() {
return User.create({
email: 'local@com',
password: '123',
});
});
2013-07-16 20:41:17 +00:00
it('Requires a unique email', function(done) {
User.create({ email: 'a@b.com', password: 'foobar' }, function() {
User.create({ email: 'a@b.com', password: 'batbaz' }, function(err) {
assert(err, 'should error because the email is not unique!');
done();
});
});
});
it('Requires a unique email (email case-sensitivity off)', function(done) {
User.settings.caseSensitiveEmail = false;
User.create({ email: 'A@b.com', password: 'foobar' }, function(err) {
if (err) return done(err);
User.create({ email: 'a@b.com', password: 'batbaz' }, function(err) {
assert(err, 'should error because the email is not unique!');
done();
});
});
});
it('Requires a unique email (email case-sensitive)', function(done) {
User.create({ email: 'A@b.com', password: 'foobar' }, function(err, user1) {
User.create({ email: 'a@b.com', password: 'batbaz' }, function(err, user2) {
if (err) return done(err);
assert.notEqual(user1.email, user2.email);
done();
});
});
});
it('Requires a unique username', function(done) {
User.create({ email: 'a@b.com', username: 'abc', password: 'foobar' }, function() {
User.create({ email: 'b@b.com', username: 'abc', password: 'batbaz' }, function(err) {
assert(err, 'should error because the username is not unique!');
done();
});
});
});
2013-07-16 20:41:17 +00:00
it('Requires a password to login with basic auth', function(done) {
User.create({ email: 'b@c.com' }, function(err) {
User.login({ email: 'b@c.com' }, function(err, accessToken) {
2013-11-13 19:49:08 +00:00
assert(!accessToken, 'should not create a accessToken without a valid password');
assert(err, 'should not login without a password');
assert.equal(err.code, 'LOGIN_FAILED');
done();
});
});
});
2013-07-16 20:41:17 +00:00
it('Hashes the given password', function() {
var u = new User({ username: 'foo', password: 'bar' });
2013-07-16 01:22:33 +00:00
assert(u.password !== 'bar');
});
it('does not hash the password if it\'s already hashed', function() {
var u1 = new User({ username: 'foo', password: 'bar' });
assert(u1.password !== 'bar');
var u2 = new User({ username: 'foo', password: u1.password });
assert(u2.password === u1.password);
});
2016-07-07 15:30:57 +00:00
it('invalidates the user\'s accessToken when the user is deleted By id', function(done) {
var usersId;
async.series([
function(next) {
User.create({ email: 'b@c.com', password: 'bar' }, function(err, user) {
usersId = user.id;
next(err);
});
},
function(next) {
User.login({ email: 'b@c.com', password: 'bar' }, function(err, accessToken) {
if (err) return next(err);
assert(accessToken.userId);
next();
});
},
function(next) {
User.deleteById(usersId, function(err) {
next(err);
});
},
function(next) {
User.findById(usersId, function(err, userFound) {
if (err) return next(err);
expect(userFound).to.equal(null);
AccessToken.find({ where: { userId: usersId }}, function(err, tokens) {
if (err) return next(err);
expect(tokens.length).to.equal(0);
next();
});
});
},
], function(err) {
if (err) return done(err);
done();
});
});
it('invalidates the user\'s accessToken when the user is deleted all', function(done) {
2016-07-25 21:22:51 +00:00
var userIds = [];
var accessTokenId;
2016-07-07 15:30:57 +00:00
async.series([
function(next) {
2016-07-25 21:22:51 +00:00
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) {
return u.id;
});
2016-07-07 15:30:57 +00:00
next(err);
});
},
function(next) {
User.login({ email: 'b@c.com', password: 'bar' }, function(err, accessToken) {
accessTokenId = accessToken.userId;
2016-07-25 21:22:51 +00:00
if (err) return next(err);
2016-07-07 15:30:57 +00:00
assert(accessTokenId);
next();
});
},
function(next) {
User.login({ email: 'd@c.com', password: 'bar' }, function(err, accessToken) {
accessTokenId = accessToken.userId;
2016-07-25 21:22:51 +00:00
if (err) return next(err);
2016-07-07 15:30:57 +00:00
assert(accessTokenId);
next();
});
},
function(next) {
User.deleteAll({ name: 'myname' }, function(err, user) {
next(err);
});
},
function(next) {
User.find({ where: { name: 'myname' }}, function(err, userFound) {
2016-07-25 21:22:51 +00:00
if (err) return next(err);
2016-07-07 15:30:57 +00:00
expect(userFound.length).to.equal(0);
2016-07-25 21:22:51 +00:00
AccessToken.find({ where: { userId: { inq: userIds }}}, function(err, tokens) {
2016-07-07 15:30:57 +00:00
if (err) return next(err);
expect(tokens.length).to.equal(0);
next();
});
});
},
], function(err) {
if (err) return done(err);
done();
});
});
describe('custom password hash', function() {
var defaultHashPassword, defaultValidatePassword;
beforeEach(function() {
defaultHashPassword = User.hashPassword;
defaultValidatePassword = User.validatePassword;
User.hashPassword = function(plain) {
return plain.toUpperCase();
};
User.validatePassword = function(plain) {
if (!plain || plain.length < 3) {
throw new Error('Password must have at least 3 chars');
}
return true;
};
});
afterEach(function() {
User.hashPassword = defaultHashPassword;
User.validatePassword = defaultValidatePassword;
});
it('Reports invalid password', function() {
try {
var u = new User({ username: 'foo', password: 'aa' });
assert(false, 'Error should have been thrown');
} catch (e) {
// Ignore
}
});
it('Hashes the given password', function() {
var u = new User({ username: 'foo', password: 'bar' });
assert(u.password === 'BAR');
});
});
it('Create a user over REST should remove emailVerified property', function(done) {
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send(validCredentialsEmailVerifiedOverREST)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
assert(!res.body.emailVerified);
done();
});
});
2013-07-03 05:37:31 +00:00
});
2016-08-03 23:01:33 +00:00
describe('Password length validation', function() {
var pass72Char = new Array(70).join('a') + '012';
var pass73Char = pass72Char + '3';
var passTooLong = pass72Char + 'WXYZ1234';
it('rejects passwords longer than 72 characters', function(done) {
try {
User.create({ email: 'b@c.com', password: pass73Char }, function(err) {
if (err) return done (err);
done(new Error('User.create() should have thrown an error.'));
});
} catch (e) {
expect(e).to.match(/Password too long/);
done();
}
});
it('rejects a new user with password longer than 72 characters', function(done) {
try {
var u = new User({ username: 'foo', password: pass73Char });
assert(false, 'Error should have been thrown');
} catch (e) {
expect(e).to.match(/Password too long/);
done();
}
});
it('accepts passwords that are exactly 72 characters long', function(done) {
User.create({ email: 'b@c.com', password: pass72Char }, function(err, user) {
if (err) return done(err);
User.findById(user.id, function(err, userFound) {
if (err) return done (err);
assert(userFound);
done();
});
});
});
it('allows login with password exactly 72 characters long', function(done) {
User.create({ email: 'b@c.com', password: pass72Char }, function(err) {
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);
done();
});
});
});
it('rejects password reset when password is more than 72 chars', function(done) {
User.create({ email: 'b@c.com', password: pass72Char }, function(err) {
if (err) return done (err);
User.resetPassword({ email: 'b@c.com', password: pass73Char }, function(err) {
assert(err);
expect(err).to.match(/Password too long/);
done();
});
});
});
});
describe('Access-hook for queries with email NOT case-sensitive', function() {
it('Should not throw an error if the query does not contain {where: }', function(done) {
User.find({}, function(err) {
if (err) done(err);
done();
});
});
it('Should be able to find lowercase email with mixed-case email query', function(done) {
User.settings.caseSensitiveEmail = false;
User.find({ where: { email: validMixedCaseEmailCredentials.email }}, function(err, result) {
if (err) done(err);
assert(result[0], 'The query did not find the user');
assert.equal(result[0].email, validCredentialsEmail);
done();
});
});
it('Should be able to use query filters (email case-sensitivity off)', function(done) {
User.settings.caseSensitiveEmail = false;
var insensitiveUser = { email: 'insensitive@example.com', password: 'abc' };
User.create(insensitiveUser, function(err, user) {
User.find({ where: { email:
{ inq: [insensitiveUser.email] },
}}, function(err, result) {
if (err) done(err);
assert(result[0], 'The query did not find the user');
assert.equal(result[0].email, insensitiveUser.email);
done();
});
});
});
it('Should be able to use query filters (email case-sensitivity on)', function(done) {
User.settings.caseSensitiveEmail = true;
var sensitiveUser = { email: 'senSiTive@example.com', password: 'abc' };
User.create(sensitiveUser, function(err, user) {
User.find({ where: { email:
{ inq: [sensitiveUser.email] },
}}, function(err, result) {
if (err) done(err);
assert(result[0], 'The query did not find the user');
assert.equal(result[0].email, sensitiveUser.email);
done();
});
});
});
});
2013-07-03 05:37:31 +00:00
describe('User.login', function() {
2013-07-16 20:41:17 +00:00
it('Login a user by providing credentials', function(done) {
2014-11-21 02:35:36 +00:00
User.login(validCredentials, function(err, accessToken) {
assert(accessToken.userId);
2013-11-13 19:49:08 +00:00
assert(accessToken.id);
2013-11-14 21:01:47 +00:00
assert.equal(accessToken.id.length, 64);
done();
});
});
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);
done();
});
});
it('Try to login with invalid email case', function(done) {
User.login(validMixedCaseEmailCredentials, function(err, accessToken) {
assert(err);
done();
});
});
it('Login a user by providing credentials with TTL', function(done) {
2014-11-21 02:35:36 +00:00
User.login(validCredentialsWithTTL, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl);
assert.equal(accessToken.id.length, 64);
done();
});
});
2015-07-01 11:43:25 +00:00
it('honors default `createAccessToken` implementation', function(done) {
User.login(validCredentialsWithTTL, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
User.findById(accessToken.userId, function(err, user) {
user.createAccessToken(120, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 120);
assert.equal(accessToken.id.length, 64);
2015-07-01 11:43:25 +00:00
done();
});
});
});
});
it('honors default `createAccessToken` implementation - promise variant', function(done) {
User.login(validCredentialsWithTTL, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
User.findById(accessToken.userId, function(err, user) {
user.createAccessToken(120)
.then(function(accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 120);
assert.equal(accessToken.id.length, 64);
2015-07-01 11:43:25 +00:00
done();
})
.catch(function(err) {
done(err);
});
});
});
});
it('Login a user using a custom createAccessToken', function(done) {
var createToken = User.prototype.createAccessToken; // Save the original method
// Override createAccessToken
User.prototype.createAccessToken = function(ttl, cb) {
// Reduce the ttl by half for testing purpose
this.accessTokens.create({ ttl: ttl / 2 }, cb);
};
2014-11-21 02:35:36 +00:00
User.login(validCredentialsWithTTL, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 1800);
assert.equal(accessToken.id.length, 64);
User.findById(accessToken.userId, function(err, user) {
2014-11-21 02:35:36 +00:00
user.createAccessToken(120, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 60);
assert.equal(accessToken.id.length, 64);
// Restore create access token
User.prototype.createAccessToken = createToken;
done();
});
});
});
});
it('Login a user using a custom createAccessToken with options',
function(done) {
var createToken = User.prototype.createAccessToken; // Save the original method
// Override createAccessToken
User.prototype.createAccessToken = function(ttl, options, cb) {
// Reduce the ttl by half for testing purpose
this.accessTokens.create({ ttl: ttl / 2, scopes: options.scope }, cb);
};
User.login(validCredentialsWithTTLAndScope, function(err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
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);
assert.equal(accessToken.ttl, 60);
assert.equal(accessToken.id.length, 64);
assert.equal(accessToken.scopes, 'default');
// Restore create access token
User.prototype.createAccessToken = createToken;
done();
});
});
});
});
it('Login should only allow correct credentials', function(done) {
User.login(invalidCredentials, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'LOGIN_FAILED');
assert(!accessToken);
done();
});
});
2015-07-01 11:43:25 +00:00
it('Login should only allow correct credentials - promise variant', function(done) {
User.login(invalidCredentials)
.then(function(accessToken) {
assert(!accessToken);
2015-07-01 11:43:25 +00:00
done();
})
.catch(function(err) {
assert(err);
assert.equal(err.code, 'LOGIN_FAILED');
2015-07-01 11:43:25 +00:00
done();
});
});
it('Login a user providing incomplete credentials', function(done) {
User.login(incompleteCredentials, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED');
done();
});
});
2015-07-01 11:43:25 +00:00
it('Login a user providing incomplete credentials - promise variant', function(done) {
User.login(incompleteCredentials)
.then(function(accessToken) {
assert(!accessToken);
2015-07-01 11:43:25 +00:00
done();
})
.catch(function(err) {
assert(err);
assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED');
2015-07-01 11:43:25 +00:00
done();
});
});
it('Login a user over REST by providing credentials', function(done) {
2013-07-02 23:51:38 +00:00
request(app)
.post('/test-users/login')
2013-07-02 23:51:38 +00:00
.expect('Content-Type', /json/)
.expect(200)
.send(validCredentials)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
2013-11-13 19:49:08 +00:00
var accessToken = res.body;
assert(accessToken.userId);
2013-11-13 19:49:08 +00:00
assert(accessToken.id);
2013-11-14 21:01:47 +00:00
assert.equal(accessToken.id.length, 64);
assert(accessToken.user === undefined);
2013-07-02 23:51:38 +00:00
done();
});
});
it('Login a user over REST by providing invalid credentials', function(done) {
request(app)
.post('/test-users/login')
.expect('Content-Type', /json/)
.expect(401)
.send(invalidCredentials)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert.equal(errorResponse.code, 'LOGIN_FAILED');
done();
});
});
it('Login a user over REST by providing incomplete credentials', function(done) {
request(app)
.post('/test-users/login')
.expect('Content-Type', /json/)
.expect(400)
.send(incompleteCredentials)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert.equal(errorResponse.code, 'USERNAME_EMAIL_REQUIRED');
done();
});
});
it('Login a user over REST with the wrong Content-Type', function(done) {
request(app)
.post('/test-users/login')
.set('Content-Type', null)
.expect('Content-Type', /json/)
.expect(400)
.send(JSON.stringify(validCredentials))
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert.equal(errorResponse.code, 'USERNAME_EMAIL_REQUIRED');
done();
});
});
it('Returns current user when `include` is `USER`', function(done) {
request(app)
.post('/test-users/login?include=USER')
.send(validCredentials)
.expect(200)
.expect('Content-Type', /json/)
.end(function(err, res) {
if (err) return done(err);
var token = res.body;
expect(token.user, 'body.user').to.not.equal(undefined);
expect(token.user, 'body.user')
.to.have.property('email', validCredentials.email);
done();
});
});
it('should handle multiple `include`', function(done) {
request(app)
.post('/test-users/login?include=USER&include=Post')
.send(validCredentials)
.expect(200)
.expect('Content-Type', /json/)
.end(function(err, res) {
if (err) return done(err);
var token = res.body;
expect(token.user, 'body.user').to.not.equal(undefined);
expect(token.user, 'body.user')
.to.have.property('email', validCredentials.email);
done();
});
});
2016-08-03 23:01:33 +00:00
it('allows login with password too long but created in old LB version',
function(done) {
var bcrypt = require('bcryptjs');
var longPassword = new Array(80).join('a');
var oldHash = bcrypt.hashSync(longPassword, bcrypt.genSaltSync(1));
User.create({ email: 'b@c.com', password: oldHash }, function(err) {
if (err) return done(err);
User.login({ email: 'b@c.com', password: longPassword }, function(err, accessToken) {
if (err) return done(err);
assert(accessToken.id);
// we are logged in, the test passed
done();
});
});
});
2013-07-02 23:51:38 +00:00
});
2014-10-23 18:10:39 +00:00
function assertGoodToken(accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.id.length, 64);
}
describe('User.login requiring email verification', function() {
beforeEach(function() {
User.settings.emailVerificationRequired = true;
});
afterEach(function() {
User.settings.emailVerificationRequired = false;
});
it('Require valid and complete credentials for email verification error', function(done) {
User.login({ email: validCredentialsEmail }, function(err, accessToken) {
// strongloop/loopback#931
// error message should be "login failed"
// and not "login failed as the email has not been verified"
assert(err && !/verified/.test(err.message),
'expecting "login failed" error message, received: "' + err.message + '"');
assert.equal(err.code, 'LOGIN_FAILED');
done();
});
});
it('Require valid and complete credentials for email verification error - promise variant',
function(done) {
2015-07-01 11:43:25 +00:00
User.login({ email: validCredentialsEmail })
.then(function(accessToken) {
done();
})
2015-07-01 11:43:25 +00:00
.catch(function(err) {
// strongloop/loopback#931
// error message should be "login failed" and not "login failed as the email has not been verified"
assert(err && !/verified/.test(err.message),
'expecting "login failed" error message, received: "' + err.message + '"');
2015-07-01 11:43:25 +00:00
assert.equal(err.code, 'LOGIN_FAILED');
2015-07-01 11:43:25 +00:00
done();
});
});
it('Login a user by without email verification', function(done) {
2014-11-21 02:35:36 +00:00
User.login(validCredentials, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'LOGIN_FAILED_EMAIL_NOT_VERIFIED');
done();
});
});
2015-07-01 11:43:25 +00:00
it('Login a user by without email verification - promise variant', function(done) {
User.login(validCredentials)
.then(function(err, accessToken) {
done();
})
.catch(function(err) {
assert(err);
assert.equal(err.code, 'LOGIN_FAILED_EMAIL_NOT_VERIFIED');
2015-07-01 11:43:25 +00:00
done();
});
});
it('Login a user by with email verification', function(done) {
2014-11-21 02:35:36 +00:00
User.login(validCredentialsEmailVerified, function(err, accessToken) {
2014-10-23 18:10:39 +00:00
assertGoodToken(accessToken);
done();
});
});
2015-07-01 11:43:25 +00:00
it('Login a user by with email verification - promise variant', function(done) {
User.login(validCredentialsEmailVerified)
.then(function(accessToken) {
assertGoodToken(accessToken);
2015-07-01 11:43:25 +00:00
done();
})
.catch(function(err) {
done(err);
});
});
it('Login a user over REST when email verification is required', function(done) {
request(app)
.post('/test-users/login')
.expect('Content-Type', /json/)
.expect(200)
.send(validCredentialsEmailVerified)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
var accessToken = res.body;
2014-10-23 18:10:39 +00:00
assertGoodToken(accessToken);
assert(accessToken.user === undefined);
done();
});
});
it('Login user over REST require complete and valid credentials ' +
'for email verification error message',
function(done) {
request(app)
.post('/test-users/login')
.expect('Content-Type', /json/)
.expect(401)
.send({ email: validCredentialsEmail })
.end(function(err, res) {
if (err) return done(err);
// strongloop/loopback#931
// error message should be "login failed"
// and not "login failed as the email has not been verified"
var errorResponse = res.body.error;
assert(errorResponse && !/verified/.test(errorResponse.message),
'expecting "login failed" error message, received: "' + errorResponse.message + '"');
assert.equal(errorResponse.code, 'LOGIN_FAILED');
done();
});
});
it('Login a user over REST without email verification when it is required', function(done) {
request(app)
.post('/test-users/login')
.expect('Content-Type', /json/)
.expect(401)
.send(validCredentials)
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert.equal(errorResponse.code, 'LOGIN_FAILED_EMAIL_NOT_VERIFIED');
done();
});
});
});
2014-10-23 18:10:39 +00:00
describe('User.login requiring realm', function() {
var User, AccessToken;
2014-10-23 18:10:39 +00:00
beforeEach(function() {
User = app.registry.createModel('RealmUser', {}, {
base: 'TestUser',
realmRequired: true,
realmDelimiter: ':',
});
AccessToken = app.registry.createModel('RealmAccessToken', {}, {
base: 'AccessToken',
});
2014-10-23 18:10:39 +00:00
app.model(AccessToken, { dataSource: 'db' });
app.model(User, { dataSource: 'db' });
2014-10-23 18:10:39 +00:00
// Update the AccessToken relation to use the subclass of User
AccessToken.belongsTo(User, { as: 'user', foreignKey: 'userId' });
User.hasMany(AccessToken, { as: 'accessTokens', foreignKey: 'userId' });
2014-10-23 18:10:39 +00:00
// allow many User.afterRemote's to be called
User.setMaxListeners(0);
});
var realm1User = {
realm: 'realm1',
username: 'foo100',
email: 'foo100@bar.com',
password: 'pass100',
2014-10-23 18:10:39 +00:00
};
var realm2User = {
realm: 'realm2',
username: 'foo100',
email: 'foo100@bar.com',
password: 'pass200',
2014-10-23 18:10:39 +00:00
};
var credentialWithoutRealm = {
username: 'foo100',
email: 'foo100@bar.com',
password: 'pass100',
2014-10-23 18:10:39 +00:00
};
var credentialWithBadPass = {
realm: 'realm1',
username: 'foo100',
email: 'foo100@bar.com',
password: 'pass001',
2014-10-23 18:10:39 +00:00
};
var credentialWithBadRealm = {
realm: 'realm3',
username: 'foo100',
email: 'foo100@bar.com',
password: 'pass100',
2014-10-23 18:10:39 +00:00
};
var credentialWithRealm = {
realm: 'realm1',
username: 'foo100',
password: 'pass100',
2014-10-23 18:10:39 +00:00
};
var credentialRealmInUsername = {
username: 'realm1:foo100',
password: 'pass100',
2014-10-23 18:10:39 +00:00
};
var credentialRealmInEmail = {
email: 'realm1:foo100@bar.com',
password: 'pass100',
2014-10-23 18:10:39 +00:00
};
var user1 = null;
2014-10-23 18:10:39 +00:00
beforeEach(function(done) {
User.create(realm1User, function(err, u) {
if (err) return done(err);
2014-10-23 18:10:39 +00:00
user1 = u;
User.create(realm2User, done);
});
});
2016-08-18 21:52:04 +00:00
it('honors unique email for realm', function(done) {
User.create(realm1User, function(err, u) {
assert(err);
assert(err.message.match(/User already exists/) &&
err.message.match(/Email already exists/));
done();
});
});
2014-10-23 18:10:39 +00:00
it('rejects a user by without realm', function(done) {
User.login(credentialWithoutRealm, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'REALM_REQUIRED');
2014-10-23 18:10:39 +00:00
done();
});
});
it('rejects a user by with bad realm', function(done) {
User.login(credentialWithBadRealm, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'LOGIN_FAILED');
2014-10-23 18:10:39 +00:00
done();
});
});
it('rejects a user by with bad pass', function(done) {
User.login(credentialWithBadPass, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'LOGIN_FAILED');
2014-10-23 18:10:39 +00:00
done();
});
});
it('logs in a user by with realm', function(done) {
User.login(credentialWithRealm, function(err, accessToken) {
assertGoodToken(accessToken);
assert.equal(accessToken.userId, user1.id);
2014-10-23 18:10:39 +00:00
done();
});
});
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.id);
2014-10-23 18:10:39 +00:00
done();
});
});
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.id);
2014-10-23 18:10:39 +00:00
done();
});
});
describe('User.login with realmRequired but no realmDelimiter', function() {
beforeEach(function() {
2014-10-23 18:10:39 +00:00
User.settings.realmDelimiter = undefined;
});
afterEach(function() {
2014-10-23 18:10:39 +00:00
User.settings.realmDelimiter = ':';
});
it('logs in a user by with realm', function(done) {
User.login(credentialWithRealm, function(err, accessToken) {
assertGoodToken(accessToken);
assert.equal(accessToken.userId, user1.id);
2014-10-23 18:10:39 +00:00
done();
});
});
it('rejects a user by with realm in email if realmDelimiter is not set',
function(done) {
User.login(credentialRealmInEmail, function(err, accessToken) {
assert(err);
assert.equal(err.code, 'REALM_REQUIRED');
2014-10-23 18:10:39 +00:00
done();
});
});
});
});
2013-07-03 05:37:31 +00:00
describe('User.logout', function() {
2013-11-13 19:49:08 +00:00
it('Logout a user by providing the current accessToken id (using node)', function(done) {
login(logout);
function login(fn) {
User.login({ email: 'foo@bar.com', password: 'bar' }, fn);
}
2013-11-13 19:49:08 +00:00
function logout(err, accessToken) {
User.logout(accessToken.id, verify(accessToken.id, done));
}
});
it('Logout a user by providing the current accessToken id (using node) - promise variant',
function(done) {
2015-07-01 11:43:25 +00:00
login(logout);
function login(fn) {
User.login({ email: 'foo@bar.com', password: 'bar' }, fn);
2015-07-01 11:43:25 +00:00
}
function logout(err, accessToken) {
User.logout(accessToken.id)
.then(function() {
verify(accessToken.id, done);
})
.catch(done(err));
}
});
2013-11-13 19:49:08 +00:00
it('Logout a user by providing the current accessToken id (over rest)', function(done) {
2013-07-03 05:37:31 +00:00
login(logout);
function login(fn) {
request(app)
.post('/test-users/login')
2013-07-03 05:37:31 +00:00
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'foo@bar.com', password: 'bar' })
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
2013-11-13 19:49:08 +00:00
var accessToken = res.body;
assert(accessToken.userId);
2013-11-13 19:49:08 +00:00
assert(accessToken.id);
2013-11-13 19:49:08 +00:00
fn(null, accessToken.id);
2013-07-03 05:37:31 +00:00
});
}
function logout(err, token) {
2013-07-03 05:37:31 +00:00
request(app)
.post('/test-users/logout')
.set('Authorization', token)
2013-07-25 00:21:15 +00:00
.expect(204)
.end(verify(token, done));
2013-07-03 05:37:31 +00:00
}
});
function verify(token, done) {
assert(token);
2014-11-21 02:35:36 +00:00
return function(err) {
if (err) return done(err);
2014-11-21 02:35:36 +00:00
AccessToken.findById(token, function(err, accessToken) {
2013-11-13 19:49:08 +00:00
assert(!accessToken, 'accessToken should not exist after logging out');
done(err);
});
};
}
2013-07-03 05:37:31 +00:00
});
2014-11-21 02:35:36 +00:00
describe('user.hasPassword(plain, fn)', function() {
2013-07-16 20:41:17 +00:00
it('Determine if the password matches the stored password', function(done) {
var u = new User({ username: 'foo', password: 'bar' });
2014-11-21 02:35:36 +00:00
u.hasPassword('bar', function(err, isMatch) {
2013-07-03 05:37:31 +00:00
assert(isMatch, 'password doesnt match');
2013-07-03 05:37:31 +00:00
done();
});
2013-07-15 21:07:17 +00:00
});
2015-07-01 11:43:25 +00:00
it('Determine if the password matches the stored password - promise variant', function(done) {
var u = new User({ username: 'foo', password: 'bar' });
2015-07-01 11:43:25 +00:00
u.hasPassword('bar')
.then(function(isMatch) {
assert(isMatch, 'password doesnt match');
2015-07-01 11:43:25 +00:00
done();
})
.catch(function(err) {
done(err);
});
});
2013-07-16 01:22:33 +00:00
it('should match a password when saved', function(done) {
var u = new User({ username: 'a', password: 'b', email: 'z@z.net' });
2014-11-21 02:35:36 +00:00
u.save(function(err, user) {
User.findById(user.id, function(err, uu) {
uu.hasPassword('b', function(err, isMatch) {
2013-07-16 01:22:33 +00:00
assert(isMatch);
2013-07-16 01:22:33 +00:00
done();
});
});
});
});
2013-07-15 21:07:17 +00:00
it('should match a password after it is changed', function(done) {
User.create({ email: 'foo@baz.net', username: 'bat', password: 'baz' }, function(err, user) {
2014-11-21 02:35:36 +00:00
User.findById(user.id, function(err, foundUser) {
assert(foundUser);
foundUser.hasPassword('baz', function(err, isMatch) {
assert(isMatch);
foundUser.password = 'baz2';
foundUser.save(function(err, updatedUser) {
updatedUser.hasPassword('baz2', function(err, isMatch) {
assert(isMatch);
User.findById(user.id, function(err, uu) {
uu.hasPassword('baz2', function(err, isMatch) {
assert(isMatch);
2014-11-21 02:35:36 +00:00
done();
});
});
});
});
});
});
});
2013-07-03 05:37:31 +00:00
});
});
2014-11-21 02:35:36 +00:00
describe('Verification', function() {
describe('user.verify(options, fn)', function() {
2013-07-03 05:37:31 +00:00
it('Verify a user\'s email address', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
2013-07-03 05:37:31 +00:00
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: ctx.req.protocol,
host: ctx.req.get('host'),
2013-07-03 05:37:31 +00:00
};
2014-11-21 02:35:36 +00:00
user.verify(options, function(err, result) {
2013-07-03 05:37:31 +00:00
assert(result.email);
2014-07-15 23:46:43 +00:00
assert(result.email.response);
2013-07-03 05:37:31 +00:00
assert(result.token);
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('/api/test-users/confirm'));
assert(~msg.indexOf('To: bar@bat.com'));
2013-07-03 05:37:31 +00:00
done();
});
});
2013-07-03 05:37:31 +00:00
request(app)
.post('/test-users')
2013-07-03 05:37:31 +00:00
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
2013-07-03 05:37:31 +00:00
});
});
2014-10-24 05:46:23 +00:00
2015-07-01 11:43:25 +00:00
it('Verify a user\'s email address - promise variant', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: ctx.req.protocol,
host: ctx.req.get('host'),
2015-07-01 11:43:25 +00:00
};
user.verify(options)
.then(function(result) {
assert(result.email);
assert(result.email.response);
assert(result.token);
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('/api/test-users/confirm'));
assert(~msg.indexOf('To: bar@bat.com'));
2015-07-01 11:43:25 +00:00
done();
})
.catch(function(err) {
done(err);
});
});
request(app)
.post('/test-users')
.send({ email: 'bar@bat.com', password: 'bar' })
2015-07-01 11:43:25 +00:00
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
2015-07-01 11:43:25 +00:00
});
});
2014-10-24 05:46:23 +00:00
it('Verify a user\'s email address with custom header', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
2014-10-24 05:46:23 +00:00
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: ctx.req.protocol,
host: ctx.req.get('host'),
headers: { 'message-id': 'custom-header-value' },
2014-10-24 05:46:23 +00:00
};
2014-11-21 02:35:36 +00:00
user.verify(options, function(err, result) {
2014-10-24 05:46:23 +00:00
assert(result.email);
assert.equal(result.email.messageId, 'custom-header-value');
2014-10-24 05:46:23 +00:00
done();
});
});
2014-10-24 05:46:23 +00:00
request(app)
.post('/test-users')
2014-10-24 05:46:23 +00:00
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
2014-10-24 05:46:23 +00:00
});
});
it('Verify a user\'s email address with custom token generator', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: ctx.req.protocol,
host: ctx.req.get('host'),
generateVerificationToken: function(user, cb) {
assert(user);
assert.equal(user.email, 'bar@bat.com');
assert(cb);
assert.equal(typeof cb, 'function');
// let's ensure async execution works on this one
process.nextTick(function() {
cb(null, 'token-123456');
});
},
};
user.verify(options, function(err, result) {
assert(result.email);
assert(result.email.response);
assert(result.token);
assert.equal(result.token, 'token-123456');
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('token-123456'));
done();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
.end(function(err, res) {
if (err) return done(err);
});
});
it('Fails if custom token generator returns error', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: ctx.req.protocol,
host: ctx.req.get('host'),
generateVerificationToken: function(user, cb) {
// let's ensure async execution works on this one
process.nextTick(function() {
cb(new Error('Fake error'));
});
},
};
user.verify(options, function(err, result) {
assert(err);
assert.equal(err.message, 'Fake error');
assert.equal(result, undefined);
done();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
.end(function(err, res) {
if (err) return done(err);
});
});
describe('Verification link port-squashing', function() {
it('Do not squash non-80 ports for HTTP links', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: 'http',
host: 'myapp.org',
port: 3000,
};
user.verify(options, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('http://myapp.org:3000/'));
done();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
.end(function(err, res) {
if (err) return done(err);
});
});
it('Squash port 80 for HTTP links', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: 'http',
host: 'myapp.org',
port: 80,
};
user.verify(options, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('http://myapp.org/'));
done();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
.end(function(err, res) {
if (err) return done(err);
});
});
it('Do not squash non-443 ports for HTTPS links', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: 'https',
host: 'myapp.org',
port: 3000,
};
user.verify(options, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('https://myapp.org:3000/'));
done();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
.end(function(err, res) {
if (err) return done(err);
});
});
it('Squash port 443 for HTTPS links', function(done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
protocol: 'https',
host: 'myapp.org',
port: 443,
};
user.verify(options, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('https://myapp.org/'));
done();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'bar@bat.com', password: 'bar' })
.end(function(err, res) {
if (err) return done(err);
});
});
});
it('should hide verification tokens from user JSON', function(done) {
var user = new User({
email: 'bar@bat.com',
password: 'bar',
verificationToken: 'a-token',
});
var data = user.toJSON();
assert(!('verificationToken' in data));
done();
});
it('should squash "//" when restApiRoot is "/"', function(done) {
var emailBody;
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: '/',
host: 'myapp.org',
port: 3000,
restApiRoot: '/',
};
user.verify(options, function(err, result) {
if (err) return next(err);
emailBody = result.email.response.toString('utf-8');
next();
});
});
request(app)
.post('/test-users')
.expect('Content-Type', /json/)
.expect(200)
.send({ email: 'user@example.com', password: 'pass' })
.end(function(err, res) {
if (err) return done(err);
expect(emailBody)
.to.contain('http://myapp.org:3000/test-users/confirm');
done();
});
});
2013-07-03 05:37:31 +00:00
});
2014-11-21 02:35:36 +00:00
describe('User.confirm(options, fn)', function() {
var options;
function testConfirm(testFunc, done) {
2014-11-21 02:35:36 +00:00
User.afterRemote('create', function(ctx, user, next) {
2013-07-03 05:37:31 +00:00
assert(user, 'afterRemote should include result');
options = {
2013-07-03 05:37:31 +00:00
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
redirect: 'http://foo.com/bar',
protocol: ctx.req.protocol,
host: ctx.req.get('host'),
2013-07-03 05:37:31 +00:00
};
2014-11-21 02:35:36 +00:00
user.verify(options, function(err, result) {
if (err) return done(err);
testFunc(result, done);
2013-07-03 05:37:31 +00:00
});
});
2013-07-03 05:37:31 +00:00
request(app)
.post('/test-users')
2013-07-03 05:37:31 +00:00
.expect('Content-Type', /json/)
.expect(302)
.send({ email: 'bar@bat.com', password: 'bar' })
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
2013-07-03 05:37:31 +00:00
});
}
2014-11-21 02:35:36 +00:00
it('Confirm a user verification', function(done) {
testConfirm(function(result, done) {
request(app)
.get('/test-users/confirm?uid=' + (result.uid) +
'&token=' + encodeURIComponent(result.token) +
'&redirect=' + encodeURIComponent(options.redirect))
.expect(302)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
done();
});
}, done);
});
2016-06-16 06:20:33 +00:00
it('sets verificationToken to null after confirmation', function(done) {
testConfirm(function(result, done) {
User.confirm(result.uid, result.token, false, function(err) {
if (err) return done(err);
// Verify by loading user data stored in the datasource
User.findById(result.uid, function(err, user) {
if (err) return done(err);
expect(user).to.have.property('verificationToken', null);
done();
});
});
}, done);
});
it('Should report 302 when redirect url is set', function(done) {
testConfirm(function(result, done) {
request(app)
.get('/test-users/confirm?uid=' + (result.uid) +
'&token=' + encodeURIComponent(result.token) +
'&redirect=http://foo.com/bar')
.expect(302)
.expect('Location', 'http://foo.com/bar')
.end(done);
}, done);
});
it('Should report 204 when redirect url is not set', function(done) {
testConfirm(function(result, done) {
request(app)
.get('/test-users/confirm?uid=' + (result.uid) +
'&token=' + encodeURIComponent(result.token))
.expect(204)
.end(done);
}, done);
});
2014-11-21 02:35:36 +00:00
it('Report error for invalid user id during verification', function(done) {
testConfirm(function(result, done) {
request(app)
.get('/test-users/confirm?uid=' + (result.uid + '_invalid') +
'&token=' + encodeURIComponent(result.token) +
'&redirect=' + encodeURIComponent(options.redirect))
.expect(404)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'USER_NOT_FOUND');
done();
});
}, done);
});
2014-11-21 02:35:36 +00:00
it('Report error for invalid token during verification', function(done) {
testConfirm(function(result, done) {
request(app)
.get('/test-users/confirm?uid=' + result.uid +
'&token=' + encodeURIComponent(result.token) + '_invalid' +
'&redirect=' + encodeURIComponent(options.redirect))
.expect(400)
2014-11-21 02:35:36 +00:00
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'INVALID_TOKEN');
done();
});
}, done);
2013-07-03 05:37:31 +00:00
});
});
});
2013-11-20 18:59:29 +00:00
2014-11-21 02:35:36 +00:00
describe('Password Reset', function() {
describe('User.resetPassword(options, cb)', function() {
var email = 'foo@bar.com';
it('Requires email address to reset password', function(done) {
User.resetPassword({ }, function(err) {
assert(err);
assert.equal(err.code, 'EMAIL_REQUIRED');
done();
});
});
2015-07-01 11:43:25 +00:00
it('Requires email address to reset password - promise variant', function(done) {
User.resetPassword({ })
.then(function() {
throw new Error('Error should NOT be thrown');
})
.catch(function(err) {
assert(err);
assert.equal(err.code, 'EMAIL_REQUIRED');
2015-07-01 11:43:25 +00:00
done();
});
});
it('Reports when email is not found', function(done) {
User.resetPassword({ email: 'unknown@email.com' }, function(err) {
assert(err);
assert.equal(err.code, 'EMAIL_NOT_FOUND');
assert.equal(err.statusCode, 404);
done();
});
});
2014-11-21 02:35:36 +00:00
it('Creates a temp accessToken to allow a user to change password', function(done) {
2013-11-20 18:59:29 +00:00
var calledBack = false;
User.resetPassword({
email: email,
2014-11-21 02:35:36 +00:00
}, function() {
2013-11-20 18:59:29 +00:00
calledBack = true;
});
2014-11-21 02:35:36 +00:00
User.once('resetPasswordRequest', function(info) {
2013-11-20 18:59:29 +00:00
assert(info.email);
assert(info.accessToken);
assert(info.accessToken.id);
assert.equal(info.accessToken.ttl / 60, 15);
assert(calledBack);
2014-11-21 02:35:36 +00:00
info.accessToken.user(function(err, user) {
2015-04-07 11:43:26 +00:00
if (err) return done(err);
2013-11-20 18:59:29 +00:00
assert.equal(user.email, email);
2013-11-20 18:59:29 +00:00
done();
});
});
});
it('Password reset over REST rejected without email address', function(done) {
request(app)
.post('/test-users/reset')
.expect('Content-Type', /json/)
.expect(400)
.send({ })
.end(function(err, res) {
if (err) return done(err);
var errorResponse = res.body.error;
assert(errorResponse);
assert.equal(errorResponse.code, 'EMAIL_REQUIRED');
done();
});
});
it('Password reset over REST requires email address', function(done) {
request(app)
.post('/test-users/reset')
.expect('Content-Type', /json/)
.expect(204)
.send({ email: email })
.end(function(err, res) {
if (err) return done(err);
assert.deepEqual(res.body, '');
done();
});
});
2013-11-20 18:59:29 +00:00
});
});
2014-10-10 09:21:15 +00:00
2016-08-30 19:09:11 +00:00
describe('Email Update', function() {
describe('User changing email property', function() {
var user, originalUserToken1, originalUserToken2, newUserCreated;
var currentEmailCredentials = { email: 'original@example.com', password: 'bar' };
var updatedEmailCredentials = { email: 'updated@example.com', password: 'bar' };
var newUserCred = { email: 'newuser@example.com', password: 'newpass' };
beforeEach('create user then login', function createAndLogin(done) {
async.series([
function createUserWithOriginalEmail(next) {
User.create(currentEmailCredentials, function(err, userCreated) {
if (err) return next(err);
user = userCreated;
next();
});
},
function firstLoginWithOriginalEmail(next) {
User.login(currentEmailCredentials, function(err, accessToken1) {
if (err) return next(err);
assert(accessToken1.userId);
originalUserToken1 = accessToken1.id;
next();
});
},
function secondLoginWithOriginalEmail(next) {
User.login(currentEmailCredentials, function(err, accessToken2) {
if (err) return next(err);
assert(accessToken2.userId);
originalUserToken2 = accessToken2.id;
next();
});
},
], done);
});
it('invalidates sessions when email is changed using `updateAttributes`', function(done) {
user.updateAttributes(
{ email: updatedEmailCredentials.email },
function(err, userInstance) {
if (err) return done(err);
assertNoAccessTokens(done);
});
});
it('invalidates sessions when email is changed using `replaceAttributes`', function(done) {
user.replaceAttributes(updatedEmailCredentials, function(err, userInstance) {
if (err) return done(err);
assertNoAccessTokens(done);
});
});
it('invalidates sessions when email is changed using `updateOrCreate`', function(done) {
User.updateOrCreate({
id: user.id,
email: updatedEmailCredentials.email,
password: updatedEmailCredentials.password,
}, function(err, userInstance) {
if (err) return done(err);
assertNoAccessTokens(done);
});
});
it('invalidates sessions when the email is changed using `replaceById`', function(done) {
User.replaceById(user.id, updatedEmailCredentials, function(err, userInstance) {
if (err) return done(err);
assertNoAccessTokens(done);
});
});
it('invalidates sessions when the email is changed using `replaceOrCreate`', function(done) {
User.replaceOrCreate({
id: user.id,
email: updatedEmailCredentials.email,
password: updatedEmailCredentials.password,
}, function(err, userInstance) {
if (err) return done(err);
assertNoAccessTokens(done);
});
});
it('keeps sessions AS IS if firstName is added using `updateAttributes`', function(done) {
user.updateAttributes({ 'firstName': 'Janny' }, function(err, userInstance) {
if (err) return done(err);
assertUntouchedTokens(done);
});
});
it('keeps sessions AS IS if firstName is added using `replaceAttributes`', function(done) {
user.replaceAttributes({
email: currentEmailCredentials.email,
password: currentEmailCredentials.password,
firstName: 'Candy',
}, function(err, userInstance) {
if (err) return done(err);
assertUntouchedTokens(done);
});
});
it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) {
User.updateOrCreate({
id: user.id,
firstName: 'Loay',
email: currentEmailCredentials.email,
password: currentEmailCredentials.password,
}, function(err, userInstance) {
if (err) return done(err);
assertUntouchedTokens(done);
});
});
it('keeps sessions AS IS if firstName is added using `replaceById`', function(done) {
User.replaceById(
user.id,
{
firstName: 'Miroslav',
email: currentEmailCredentials.email,
password: currentEmailCredentials.password,
}, function(err, userInstance) {
if (err) return done(err);
assertUntouchedTokens(done);
});
});
it('keeps sessions AS IS if a new user is created using `create`', function(done) {
async.series([
function(next) {
User.create(newUserCred, function(err, newUserInstance) {
if (err) return done(err);
newUserCreated = newUserInstance;
next();
});
},
function(next) {
User.login(newUserCred, function(err, newAccessToken) {
if (err) return done(err);
assert(newAccessToken.id);
assertPreservedToken(next);
});
},
], done);
});
it('keeps sessions AS IS if a new user is created using `updateOrCreate`', function(done) {
async.series([
function(next) {
User.create(newUserCred, function(err, newUserInstance2) {
if (err) return done(err);
newUserCreated = newUserInstance2;
next();
});
},
function(next) {
User.login(newUserCred, function(err, newAccessToken2) {
if (err) return done(err);
assert(newAccessToken2.id);
assertPreservedToken(next);
});
},
], done);
});
it('keeps sessions AS IS if non-email property is changed using updateAll', function(done) {
var userPartial;
async.series([
function createPartialUser(next) {
User.create(
{ email: 'partial@example.com', password: 'pass1', age: 25 },
function(err, partialInstance) {
if (err) return next (err);
userPartial = partialInstance;
next();
});
},
function loginPartiallUser(next) {
User.login({ email: 'partial@example.com', password: 'pass1' }, function(err, ats) {
if (err) return next (err);
next();
});
},
function updatePartialUser(next) {
User.updateAll(
{ id: userPartial.id },
{ age: userPartial.age + 1 },
function(err, info) {
if (err) return next (err);
next();
});
},
function verifyTokensOfPartialUser(next) {
AccessToken.find({ where: { userId: userPartial.id }}, function(err, tokens1) {
if (err) return next (err);
expect(tokens1.length).to.equal(1);
next();
});
},
], done);
});
2016-08-30 19:09:11 +00:00
function assertPreservedToken(done) {
AccessToken.find({ where: { userId: user.id }}, function(err, tokens) {
if (err) return done(err);
expect(tokens.length).to.equal(2);
expect([tokens[0].id, tokens[1].id]).to.have.members([originalUserToken1,
originalUserToken2]);
done();
});
};
function assertNoAccessTokens(done) {
AccessToken.find({ where: { userId: user.id }}, function(err, tokens) {
if (err) return done(err);
expect(tokens.length).to.equal(0);
done();
});
}
function assertUntouchedTokens(done) {
AccessToken.find({ where: { userId: user.id }}, function(err, tokens) {
if (err) return done(err);
expect(tokens.length).to.equal(2);
done();
});
}
});
describe('User not changing email property', function() {
var user1, user2, user3;
it('preserves other users\' sessions if their email is untouched', function(done) {
async.series([
function(next) {
User.create({ email: 'user1@example.com', password: 'u1pass' }, function(err, u1) {
if (err) return done(err);
User.create({ email: 'user2@example.com', password: 'u2pass' }, function(err, u2) {
if (err) return done(err);
User.create({ email: 'user3@example.com', password: 'u3pass' }, function(err, u3) {
if (err) return done(err);
user1 = u1;
user2 = u2;
user3 = u3;
next();
});
});
});
},
function(next) {
User.login(
{ email: 'user1@example.com', password: 'u1pass' },
function(err, accessToken1) {
if (err) return next(err);
User.login(
{ email: 'user2@example.com', password: 'u2pass' },
function(err, accessToken2) {
if (err) return next(err);
User.login({ email: 'user3@example.com', password: 'u3pass' },
function(err, accessToken3) {
if (err) return next(err);
next();
});
});
});
},
function(next) {
user2.updateAttribute('email', 'user2Update@b.com', function(err, userInstance) {
if (err) return next(err);
assert.equal(userInstance.email, 'user2Update@b.com');
next();
});
},
function(next) {
AccessToken.find({ where: { userId: user1.id }}, function(err, tokens1) {
if (err) return next(err);
AccessToken.find({ where: { userId: user2.id }}, function(err, tokens2) {
if (err) return next(err);
AccessToken.find({ where: { userId: user3.id }}, function(err, tokens3) {
if (err) return next(err);
expect(tokens1.length).to.equal(1);
expect(tokens2.length).to.equal(0);
expect(tokens3.length).to.equal(1);
next();
});
});
});
},
], done);
});
});
it('invalidates sessions after using updateAll', function(done) {
var userSpecial, userNormal;
async.series([
function createSpecialUser(next) {
User.create(
{ email: 'special@example.com', password: 'pass1', name: 'Special' },
function(err, specialInstance) {
if (err) return next (err);
userSpecial = specialInstance;
next();
});
},
function createNormaluser(next) {
User.create(
{ email: 'normal@example.com', password: 'pass2' },
function(err, normalInstance) {
if (err) return next (err);
userNormal = normalInstance;
next();
});
},
function loginSpecialUser(next) {
User.login({ email: 'special@example.com', password: 'pass1' }, function(err, ats) {
if (err) return next (err);
next();
});
},
function loginNormalUser(next) {
User.login({ email: 'normal@example.com', password: 'pass2' }, function(err, atn) {
if (err) return next (err);
next();
});
},
function updateSpecialUser(next) {
User.updateAll(
{ name: 'Special' },
{ email: 'superspecial@example.com' }, function(err, info) {
if (err) return next (err);
next();
});
},
function verifyTokensOfSpecialUser(next) {
AccessToken.find({ where: { userId: userSpecial.id }}, function(err, tokens1) {
if (err) return done (err);
expect(tokens1.length).to.equal(0);
next();
});
},
function verifyTokensOfNormalUser(next) {
AccessToken.find({ userId: userNormal.userId }, function(err, tokens2) {
if (err) return done (err);
expect(tokens2.length).to.equal(1);
next();
});
},
], done);
});
});
2016-08-24 20:30:58 +00:00
describe('password reset with/without email verification', function() {
it('allows resetPassword by email if email verification is required and done',
function(done) {
User.settings.emailVerificationRequired = true;
var email = validCredentialsEmailVerified.email;
User.resetPassword({ email: email }, function(err, info) {
if (err) return done (err);
done();
});
});
it('disallows resetPassword by email if email verification is required and not done',
function(done) {
User.settings.emailVerificationRequired = true;
var email = validCredentialsEmail;
User.resetPassword({ email: email }, function(err) {
assert(err);
assert.equal(err.code, 'RESET_FAILED_EMAIL_NOT_VERIFIED');
assert.equal(err.statusCode, 401);
done ();
});
});
it('allows resetPassword by email if email verification is not required',
function(done) {
User.settings.emailVerificationRequired = false;
var email = validCredentialsEmail;
User.resetPassword({ email: email }, function(err) {
if (err) return done (err);
done();
});
});
});
2014-10-10 09:21:15 +00:00
describe('ctor', function() {
it('exports default Email model', function() {
expect(User.email, 'User.email').to.be.a('function');
expect(User.email.modelName, 'modelName').to.eql('Email');
2014-10-10 09:21:15 +00:00
});
it('exports default AccessToken model', function() {
expect(User.accessToken, 'User.accessToken').to.be.a('function');
expect(User.accessToken.modelName, 'modelName').to.eql('AccessToken');
});
});
describe('ttl', function() {
var User2;
beforeEach(function() {
User2 = loopback.User.extend('User2', {}, { ttl: 10 });
});
it('should override ttl setting in based User model', function() {
expect(User2.settings.ttl).to.equal(10);
});
});
});