Merge pull request #3344 from strongloop/maintenance/passing-context-options-in-user.verify

forward context options in user.verify
This commit is contained in:
Miroslav Bajtoš 2017-04-12 09:13:20 +02:00 committed by GitHub
commit faa4975b78
2 changed files with 288 additions and 266 deletions

View File

@ -437,18 +437,18 @@ module.exports = function(User) {
* Verify a user's identity by sending them a confirmation email.
*
* ```js
* var options = {
* var verifyOptions = {
* type: 'email',
* to: user.email,
* from: noreply@example.com,
* template: 'verify.ejs',
* redirect: '/',
* tokenGenerator: function (user, cb) { cb("random-token"); }
* };
*
* user.verify(options, next);
* user.verify(verifyOptions, options, next);
* ```
*
* @options {Object} options
* @options {Object} verifyOptions
* @property {String} type Must be 'email'.
* @property {String} to Email address to which verification email is sent.
* @property {String} from Sender email addresss, for example
@ -468,130 +468,157 @@ module.exports = function(User) {
* callback function. This function should NOT add the token to the user
* object, instead simply execute the callback with the token! User saving
* and email sending will be handled in the `verify()` method.
* @callback {Function} fn Callback function.
* @param {Object} options remote context options.
* @callback {Function} cb Callback function.
* @param {Error} err Error object.
* @param {Object} object Contains email, token, uid.
* @promise
*/
User.prototype.verify = function(options, fn) {
fn = fn || utils.createPromiseCallback();
User.prototype.verify = function(verifyOptions, options, cb) {
if (cb === undefined && typeof options === 'function') {
cb = options;
options = undefined;
}
cb = cb || utils.createPromiseCallback();
var user = this;
var userModel = this.constructor;
var registry = userModel.registry;
var pkName = userModel.definition.idName() || 'id';
assert(typeof options === 'object', 'options required when calling user.verify()');
assert(options.type, 'You must supply a verification type (options.type)');
assert(options.type === 'email', 'Unsupported verification type');
assert(options.to || this.email,
'Must include options.to when calling user.verify() ' +
'or the user must have an email property');
assert(options.from, 'Must include options.from when calling user.verify()');
options.redirect = options.redirect || '/';
// final assertion is performed once all options are assigned
assert(typeof verifyOptions === 'object',
'verifyOptions object param required when calling user.verify()');
// Set a default template generation function if none provided
verifyOptions.templateFn = verifyOptions.templateFn || createVerificationEmailBody;
// Set a default token generation function if none provided
verifyOptions.generateVerificationToken = verifyOptions.generateVerificationToken ||
User.generateVerificationToken;
// Set a default mailer function if none provided
verifyOptions.mailer = verifyOptions.mailer || userModel.email ||
registry.getModelByType(loopback.Email);
var pkName = userModel.definition.idName() || 'id';
verifyOptions.redirect = verifyOptions.redirect || '/';
var defaultTemplate = path.join(__dirname, '..', '..', 'templates', 'verify.ejs');
options.template = path.resolve(options.template || defaultTemplate);
options.user = this;
options.protocol = options.protocol || 'http';
verifyOptions.template = path.resolve(verifyOptions.template || defaultTemplate);
verifyOptions.user = user;
verifyOptions.protocol = verifyOptions.protocol || 'http';
var app = userModel.app;
options.host = options.host || (app && app.get('host')) || 'localhost';
options.port = options.port || (app && app.get('port')) || 3000;
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
verifyOptions.host = verifyOptions.host || (app && app.get('host')) || 'localhost';
verifyOptions.port = verifyOptions.port || (app && app.get('port')) || 3000;
verifyOptions.restApiRoot = verifyOptions.restApiRoot || (app && app.get('restApiRoot')) || '/api';
var displayPort = (
(options.protocol === 'http' && options.port == '80') ||
(options.protocol === 'https' && options.port == '443')
) ? '' : ':' + options.port;
(verifyOptions.protocol === 'http' && verifyOptions.port == '80') ||
(verifyOptions.protocol === 'https' && verifyOptions.port == '443')
) ? '' : ':' + verifyOptions.port;
var urlPath = joinUrlPath(
options.restApiRoot,
verifyOptions.restApiRoot,
userModel.http.path,
userModel.sharedClass.findMethodByName('confirm').http.path
);
options.verifyHref = options.verifyHref ||
options.protocol +
verifyOptions.verifyHref = verifyOptions.verifyHref ||
verifyOptions.protocol +
'://' +
options.host +
verifyOptions.host +
displayPort +
urlPath +
'?' + qs.stringify({
uid: '' + options.user[pkName],
redirect: options.redirect,
uid: '' + verifyOptions.user[pkName],
redirect: verifyOptions.redirect,
});
options.templateFn = options.templateFn || createVerificationEmailBody;
verifyOptions.to = verifyOptions.to || user.email;
verifyOptions.subject = verifyOptions.subject || g.f('Thanks for Registering');
verifyOptions.headers = verifyOptions.headers || {};
// Email model
var Email =
options.mailer || this.constructor.email || registry.getModelByType(loopback.Email);
// assert the verifyOptions params that might have been badly defined
assertVerifyOptions(verifyOptions);
// Set a default token generation function if one is not provided
var tokenGenerator = options.generateVerificationToken || User.generateVerificationToken;
assert(typeof tokenGenerator === 'function', 'generateVerificationToken must be a function');
tokenGenerator(user, function(err, token) {
if (err) { return fn(err); }
user.verificationToken = token;
user.save(function(err) {
if (err) {
fn(err);
// argument "options" is passed depending on verifyOptions.generateVerificationToken function requirements
var tokenGenerator = verifyOptions.generateVerificationToken;
if (tokenGenerator.length == 3) {
tokenGenerator(user, options, addTokenToUserAndSave);
} else {
sendEmail(user);
tokenGenerator(user, addTokenToUserAndSave);
}
function addTokenToUserAndSave(err, token) {
if (err) return cb(err);
user.verificationToken = token;
user.save(options, function(err) {
if (err) return cb(err);
sendEmail(user);
});
});
}
// TODO - support more verification types
function sendEmail(user) {
options.verifyHref += '&token=' + user.verificationToken;
verifyOptions.verifyHref += '&token=' + user.verificationToken;
verifyOptions.verificationToken = user.verificationToken;
options.verificationToken = user.verificationToken;
verifyOptions.text = verifyOptions.text || g.f('Please verify your email by opening ' +
'this link in a web browser:\n\t%s', verifyOptions.verifyHref);
options.text = options.text || g.f('Please verify your email by opening ' +
'this link in a web browser:\n\t%s', options.verifyHref);
verifyOptions.text = verifyOptions.text.replace(/\{href\}/g, verifyOptions.verifyHref);
options.text = options.text.replace(/\{href\}/g, options.verifyHref);
options.to = options.to || user.email;
options.subject = options.subject || g.f('Thanks for Registering');
options.headers = options.headers || {};
options.templateFn(options, function(err, html) {
if (err) {
fn(err);
// argument "options" is passed depending on templateFn function requirements
var templateFn = verifyOptions.templateFn;
if (templateFn.length == 3) {
templateFn(verifyOptions, options, setHtmlContentAndSend);
} else {
setHtmlContentAndSend(html);
templateFn(verifyOptions, setHtmlContentAndSend);
}
});
function setHtmlContentAndSend(html) {
options.html = html;
function setHtmlContentAndSend(err, html) {
if (err) return cb(err);
// Remove options.template to prevent rejection by certain
verifyOptions.html = html;
// Remove verifyOptions.template to prevent rejection by certain
// nodemailer transport plugins.
delete options.template;
delete verifyOptions.template;
Email.send(options, function(err, email) {
if (err) {
fn(err);
// argument "options" is passed depending on Email.send function requirements
var Email = verifyOptions.mailer;
if (Email.send.length == 3) {
Email.send(verifyOptions, options, handleAfterSend);
} else {
fn(null, {email: email, token: user.verificationToken, uid: user[pkName]});
Email.send(verifyOptions, handleAfterSend);
}
});
function handleAfterSend(err, email) {
if (err) return cb(err);
cb(null, {email: email, token: user.verificationToken, uid: user[pkName]});
}
}
return fn.promise;
}
return cb.promise;
};
function createVerificationEmailBody(options, cb) {
var template = loopback.template(options.template);
var body = template(options);
function assertVerifyOptions(verifyOptions) {
assert(verifyOptions.type, 'You must supply a verification type (verifyOptions.type)');
assert(verifyOptions.type === 'email', 'Unsupported verification type');
assert(verifyOptions.to, 'Must include verifyOptions.to when calling user.verify() ' +
'or the user must have an email property');
assert(verifyOptions.from, 'Must include verifyOptions.from when calling user.verify()');
assert(typeof verifyOptions.templateFn === 'function',
'templateFn must be a function');
assert(typeof verifyOptions.generateVerificationToken === 'function',
'generateVerificationToken must be a function');
assert(verifyOptions.mailer, 'A mailer function must be provided');
assert(typeof verifyOptions.mailer.send === 'function', 'mailer.send must be a function ');
}
function createVerificationEmailBody(verifyOptions, options, cb) {
var template = loopback.template(verifyOptions.template);
var body = template(verifyOptions);
cb(null, body);
}
@ -603,9 +630,10 @@ module.exports = function(User) {
* function will be called with the `user` object as it's context (`this`).
*
* @param {object} user The User this token is being generated for.
* @param {Function} cb The generator must pass back the new token with this function call
* @param {object} options remote context options.
* @param {Function} cb The generator must pass back the new token with this function call.
*/
User.generateVerificationToken = function(user, cb) {
User.generateVerificationToken = function(user, options, cb) {
crypto.randomBytes(64, function(err, buf) {
cb(err, buf && buf.toString('hex'));
});

View File

@ -8,10 +8,12 @@ var assert = require('assert');
var expect = require('./helpers/expect');
var request = require('supertest');
var loopback = require('../');
var User, AccessToken;
var async = require('async');
var url = require('url');
var extend = require('util')._extend;
var Promise = require('bluebird');
var User, AccessToken;
describe('User', function() {
this.timeout(10000);
@ -30,14 +32,14 @@ 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;
var validCredentialsUser, validCredentialsEmailVerifiedUser, user;
// 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 = null;
beforeEach(function setupAppAndModels(done) {
beforeEach(function setupAppAndModels() {
// 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});
@ -90,14 +92,13 @@ describe('User', function() {
app.use(loopback.token({model: AccessToken}));
app.use(loopback.rest());
User.create(validCredentials, function(err, user) {
if (err) return done(err);
validCredentialsUser = user;
User.create(validCredentialsEmailVerified, function(err, user) {
if (err) return done(err);
validCredentialsEmailVerifiedUser = user;
done();
});
// create 2 users: with and without verified email
return Promise.map(
[validCredentials, validCredentialsEmailVerified],
credentials => User.create(credentials)
).then(users => {
validCredentialsUser = user = users[0];
validCredentialsEmailVerifiedUser = users[1];
});
});
@ -1405,21 +1406,23 @@ describe('User', function() {
});
describe('Verification', function() {
describe('user.verify(options, fn)', function() {
describe('user.verify(verifyOptions, options, cb)', function() {
const ctxOptions = {testFlag: true};
let verifyOptions;
beforeEach(function() {
// reset verifyOptions before each test
verifyOptions = {
type: 'email',
from: 'noreply@example.org',
};
});
it('Verify a user\'s email address', 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'),
};
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
assert(result.email);
assert(result.email.response);
assert(result.token);
@ -1445,16 +1448,7 @@ describe('User', function() {
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'),
};
user.verify(options)
user.verify(verifyOptions)
.then(function(result) {
assert(result.email);
assert(result.email.response);
@ -1484,17 +1478,9 @@ describe('User', function() {
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'),
headers: {'message-id': 'custom-header-value'},
};
verifyOptions.headers = {'message-id': 'custom-header-value'};
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
assert(result.email);
assert.equal(result.email.messageId, 'custom-header-value');
@ -1516,19 +1502,11 @@ describe('User', function() {
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'),
templateFn: function(options, cb) {
cb(null, 'custom template - verify url: ' + options.verifyHref);
},
verifyOptions.templateFn = function(verifyOptions, cb) {
cb(null, 'custom template - verify url: ' + verifyOptions.verifyHref);
};
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
assert(result.email);
assert(result.email.response);
assert(result.token);
@ -1558,17 +1536,9 @@ describe('User', function() {
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'),
templateFn: function(options, cb) {
actualVerifyHref = options.verifyHref;
verifyOptions.templateFn = function(verifyOptions, cb) {
actualVerifyHref = verifyOptions.verifyHref;
cb(null, 'dummy body');
},
};
// replace the string id with an object
@ -1579,7 +1549,7 @@ describe('User', function() {
});
user.pk = {toString: function() { return idString; }};
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
expect(result.uid).to.exist().and.be.an('object');
expect(result.uid.toString()).to.equal(idString);
const parsed = url.parse(actualVerifyHref, true);
@ -1602,14 +1572,7 @@ describe('User', function() {
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) {
verifyOptions.generateVerificationToken = function(user, cb) {
assert(user);
assert.equal(user.email, 'bar@bat.com');
assert(cb);
@ -1618,10 +1581,9 @@ describe('User', function() {
process.nextTick(function() {
cb(null, 'token-123456');
});
},
};
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
assert(result.email);
assert(result.email.response);
assert(result.token);
@ -1647,22 +1609,14 @@ describe('User', function() {
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) {
verifyOptions.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) {
user.verify(verifyOptions, function(err, result) {
assert(err);
assert.equal(err.message, 'Fake error');
assert.equal(result, undefined);
@ -1686,17 +1640,12 @@ describe('User', function() {
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',
Object.assign(verifyOptions, {
host: 'myapp.org',
port: 3000,
};
});
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('http://myapp.org:3000/'));
@ -1718,17 +1667,12 @@ describe('User', function() {
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',
Object.assign(verifyOptions, {
host: 'myapp.org',
port: 80,
};
});
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('http://myapp.org/'));
@ -1750,17 +1694,13 @@ describe('User', function() {
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',
Object.assign(verifyOptions, {
host: 'myapp.org',
port: 3000,
};
protocol: 'https',
});
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('https://myapp.org:3000/'));
@ -1782,17 +1722,13 @@ describe('User', function() {
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',
Object.assign(verifyOptions, {
host: 'myapp.org',
protocol: 'https',
port: 443,
};
});
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
var msg = result.email.response.toString('utf-8');
assert(~msg.indexOf('https://myapp.org/'));
@ -1828,17 +1764,13 @@ describe('User', function() {
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: '/',
Object.assign(verifyOptions, {
host: 'myapp.org',
port: 3000,
restApiRoot: '/',
};
});
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
if (err) return next(err);
emailBody = result.email.response.toString('utf-8');
next();
@ -1858,40 +1790,31 @@ describe('User', function() {
});
});
it('removes "options.template" from Email payload', function() {
it('removes "verifyOptions.template" from Email payload', function() {
var MailerMock = {
send: function(options, cb) { cb(null, options); },
send: function(verifyOptions, cb) { cb(null, verifyOptions); },
};
verifyOptions.mailer = MailerMock;
return User.create({email: 'user@example.com', password: 'pass'})
.then(function(user) {
return user.verify({
type: 'email',
from: 'noreply@example.com',
mailer: MailerMock,
});
})
return user.verify(verifyOptions)
.then(function(result) {
expect(result.email).to.not.have.property('template');
});
});
it('allows hash fragment in redirectUrl', function() {
return User.create({email: 'test@example.com', password: 'pass'})
.then(user => {
let actualVerifyHref;
return user.verify({
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
Object.assign(verifyOptions, {
redirect: '#/some-path?a=1&b=2',
templateFn: (options, cb) => {
actualVerifyHref = options.verifyHref;
templateFn: (verifyOptions, cb) => {
actualVerifyHref = verifyOptions.verifyHref;
cb(null, 'dummy body');
},
})
.then(() => actualVerifyHref);
})
});
return user.verify(verifyOptions)
.then(() => actualVerifyHref)
.then(verifyHref => {
var parsed = url.parse(verifyHref, true);
expect(parsed.query.redirect, 'redirect query')
@ -1899,36 +1822,107 @@ describe('User', function() {
});
});
it('verify that options.templateFn receives options.verificationToken', function() {
return User.create({email: 'test1@example.com', password: 'pass'})
.then(user => {
it('verifies that verifyOptions.templateFn receives verifyOptions.verificationToken',
function() {
let actualVerificationToken;
return user.verify({
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
Object.assign(verifyOptions, {
redirect: '#/some-path?a=1&b=2',
templateFn: (options, cb) => {
actualVerificationToken = options.verificationToken;
templateFn: (verifyOptions, cb) => {
actualVerificationToken = verifyOptions.verificationToken;
cb(null, 'dummy body');
},
})
.then(() => actualVerificationToken);
})
});
return user.verify(verifyOptions)
.then(() => actualVerificationToken)
.then(token => {
expect(token).to.exist();
});
});
it('forwards the "options" argument to user.save() ' +
'when adding verification token', function() {
let onBeforeSaveCtx = {};
// before save operation hook to capture remote ctx when saving
// verification token in user instance
User.observe('before save', function(ctx, next) {
onBeforeSaveCtx = ctx || {};
next();
});
return user.verify(verifyOptions, ctxOptions)
.then(() => {
// not checking equality since other properties are added by user.save()
expect(onBeforeSaveCtx.options).to.contain({testFlag: true});
});
});
it('forwards the "options" argument to a custom templateFn function', function() {
let templateFnOptions;
// custom templateFn function accepting the options argument
verifyOptions.templateFn = (verifyOptions, options, cb) => {
templateFnOptions = options;
cb(null, 'dummy body');
};
return user.verify(verifyOptions, ctxOptions)
.then(() => {
// not checking equality since other properties are added by user.save()
expect(templateFnOptions).to.contain({testFlag: true});
});
});
it('forwards the "options" argment to a custom token generator function', function() {
let generateTokenOptions;
// custom generateVerificationToken function accepting the options argument
verifyOptions.generateVerificationToken = (user, options, cb) => {
generateTokenOptions = options;
cb(null, 'dummy token');
};
return user.verify(verifyOptions, ctxOptions)
.then(() => {
// not checking equality since other properties are added by user.save()
expect(generateTokenOptions).to.contain({testFlag: true});
});
});
it('forwards the "options" argument to a custom mailer function', function() {
let mailerOptions;
// custom mailer function accepting the options argument
const mailer = function() {};
mailer.send = function(verifyOptions, options, cb) {
mailerOptions = options;
cb(null, 'dummy result');
};
verifyOptions.mailer = mailer;
return user.verify(verifyOptions, ctxOptions)
.then(() => {
// not checking equality since other properties are added by user.save()
expect(mailerOptions).to.contain({testFlag: true});
});
});
function givenUser() {
return User.create({email: 'test@example.com', password: 'pass'})
.then(u => user = u);
}
});
describe('User.confirm(options, fn)', function() {
var options;
var verifyOptions;
function testConfirm(testFunc, done) {
User.afterRemote('create', function(ctx, user, next) {
assert(user, 'afterRemote should include result');
options = {
verifyOptions = {
type: 'email',
to: user.email,
from: 'noreply@myapp.org',
@ -1937,7 +1931,7 @@ describe('User', function() {
host: ctx.req.get('host'),
};
user.verify(options, function(err, result) {
user.verify(verifyOptions, function(err, result) {
if (err) return done(err);
testFunc(result, done);
@ -1959,7 +1953,7 @@ describe('User', function() {
request(app)
.get('/test-users/confirm?uid=' + (result.uid) +
'&token=' + encodeURIComponent(result.token) +
'&redirect=' + encodeURIComponent(options.redirect))
'&redirect=' + encodeURIComponent(verifyOptions.redirect))
.expect(302)
.end(function(err, res) {
if (err) return done(err);
@ -2011,7 +2005,7 @@ describe('User', function() {
request(app)
.get('/test-users/confirm?uid=' + (result.uid + '_invalid') +
'&token=' + encodeURIComponent(result.token) +
'&redirect=' + encodeURIComponent(options.redirect))
'&redirect=' + encodeURIComponent(verifyOptions.redirect))
.expect(404)
.end(function(err, res) {
if (err) return done(err);
@ -2030,7 +2024,7 @@ describe('User', function() {
request(app)
.get('/test-users/confirm?uid=' + result.uid +
'&token=' + encodeURIComponent(result.token) + '_invalid' +
'&redirect=' + encodeURIComponent(options.redirect))
'&redirect=' + encodeURIComponent(verifyOptions.redirect))
.expect(400)
.end(function(err, res) {
if (err) return done(err);