feathers-service-verify-reset
Version:
Adds user email verification and password reset capabilities to local feathers-authentication
485 lines (393 loc) • 17.3 kB
JavaScript
/* global assert, describe, it */
/* eslint no-param-reassign: 0, no-shadow: 0, no-unused-vars: 0, no-var: 0, one-var: 0,
one-var-declaration-per-line: 0 */
const assert = require('chai').assert;
const feathersStubs = require('./../test/helpers/feathersStubs');
const verifyResetService = require('../lib/index').service;
const SpyOn = require('./../test/helpers/basicSpy');
const defaultVerifyDelay = 1000 * 60 * 60 * 24 * 5; // 5 days
// user DB
const now = Date.now();
const usersDb = [
{ _id: 'a', email: 'a', isVerified: false, verifyToken: '000', verifyShortToken: '00099', verifyExpires: now + 50000, username: 'Doe' },
{ _id: 'b', email: 'b', isVerified: true, verifyToken: null, verifyShortToken: null, verifyExpires: null },
{ _id: 'c', email: 'c', isVerified: true, verifyToken: '999', verifyShortToken: '99900', verifyExpires: null }, // impossible
];
// Tests
['_id', 'id'].forEach(idType => {
['paginated', 'non-paginated'].forEach(pagination => {
describe(`resendVerifySignup ${pagination} ${idType}`, () => {
const ifNonPaginated = pagination === 'non-paginated';
function basicTest1(desc, values) {
describe(desc, () => {
var db;
var app;
var users;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
verifyResetService().call(app); // define and attach verifyReset service
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('verifyReset::create exists', () => {
assert.isFunction(verifyReset.create);
});
it('updates unverified user', (done) => {
const i = 0;
verifyReset.create({ action: 'resend', value: values[0] }, {}, (err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 6, 'verify short token wrong length');
assert.match(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
done();
});
});
it('sanitizes user', (done) => {
verifyReset.create({ action: 'resend', value: values[1] }, {}, (err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'isVerified not false');
assert.strictEqual(user.verifyToken, undefined, 'verifyToken not undefined');
assert.strictEqual(user.verifyShortToken, undefined, 'verifyShortToken not undefined');
assert.strictEqual(user.verifyExpires, undefined, 'verifyExpires not undefined');
done();
});
});
it('error on verified user', (done) => {
verifyReset.create({ action: 'resend', value: values[2] }, {}, (err, user) => {
assert.isString(err.message);
assert.isNotFalse(err.message);
done();
});
});
it('error on email not found', (done) => {
verifyReset.create({ action: 'resend', value: values[3] }, {}, (err, user) => {
assert.isString(err.message);
assert.isNotFalse(err.message);
done();
});
});
it('works as Promise', (done) => {
const i = 0;
verifyReset.create({ action: 'resend', value: values[0] }, {})
.then(user => {
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 6, 'verify short token wrong length');
assert.match(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
done();
})
.catch(err => {
assert.fail(true, false, 'unexpected catch');
done();
});
});
});
}
basicTest1('emailOrToken is a string (email)', [
'a',
'a',
'b',
'x'
]);
basicTest1('emailOfToken is {email}', [
{ email: 'a' },
{ email: 'a' },
{ email: 'b' },
{ email: 'x' }
]);
basicTest1('emailOfToken is {verifyToken}', [
{ verifyToken: '000' },
{ verifyToken: '000' },
{ verifyToken: '999' },
{ verifyToken: 'xxx' },
]);
basicTest1('emailOfToken is {verifyShortToken}', [
{ verifyShortToken: '00099' },
{ verifyShortToken: '00099' },
{ verifyShortToken: '99900' },
{ verifyShortToken: 'xxx' },
]);
describe('emailOfToken is {verifyToken} can change len', () => {
var db;
var app;
var users;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
verifyResetService({
longTokenLen: 10,
}).call(app); // define and attach verifyReset service
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('can change', (done) => {
const verifyToken = '000';
const i = 0;
verifyReset.create({ action: 'resend', value: { verifyToken } }, {}, (err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 20, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 6, 'verify short token wrong length');
assert.match(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
done();
});
});
});
describe('short token (digit) can change length', () => {
var db;
var app;
var users;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
verifyResetService({
longTokenLen: 15, // need to reset this
shortTokenLen: 8,
}).call(app); // define and attach verifyReset service
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('can change', (done) => {
const verifyToken = '000';
const i = 0;
verifyReset.create({ action: 'resend', value: { verifyToken } }, {}, (err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 8, 'verify short token wrong length');
assert.match(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
done();
});
});
});
describe('short token (alpha) can change length', () => {
var db;
var app;
var users;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
verifyResetService({
longTokenLen: 15, // need to reset this
shortTokenLen: 9,
shortTokenDigits: false,
}).call(app); // define and attach verifyReset service
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('can change', (done) => {
const verifyToken = '000';
const i = 0;
verifyReset.create({ action: 'resend', value: { verifyToken } }, {}, (err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 9, 'verify short token wrong length');
assert.notMatch(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
done();
});
});
});
describe('use affirming properties', () => {
var db;
var app;
var users;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
verifyResetService({
longTokenLen: 15, // need to reset this
shortTokenLen: 6,
shortTokenDigits: false,
}).call(app); // define and attach verifyReset service
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('verifies when correct', (done) => {
const verifyToken = '000';
const i = 0;
verifyReset.create({ action: 'resend', value: {
verifyToken, email: 'a'
} }, {}, (err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 6, 'verify short token wrong length');
assert.notMatch(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
done();
});
});
it('fails when incorrect', (done) => {
const verifyToken = '000';
const i = 0;
verifyReset.create({ action: 'resend', value: {
verifyToken, email: 'a', username: 'Doexxxxxxx'
} }, {}, (err, user) => {
assert.isString(err.message);
assert.isNotFalse(err.message);
done();
});
});
it('fails when hacks attempted', (done) => {
const verifyToken = '000';
const i = 0;
verifyReset.create({ action: 'resend', value: {
username: 'Doe'
} }, {}, (err, user) => {
assert.isString(err.message);
assert.isNotFalse(err.message);
done();
});
});
});
describe('with notification', () => {
var db;
var app;
var users;
var spyEmailer;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
spyEmailer = new SpyOn(emailer);
verifyResetService({
longTokenLen: 15, // need to reset this
shortTokenLen: 6, // need to reset this
shortTokenDigits: true, // need to reset this
userNotifier: spyEmailer.callWithCb
}).call(app);
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('is called', (done) => {
const email = 'a';
const i = 0;
verifyReset.create({
action: 'resend',
value: email,
notifierOptions: { transport: 'email' }
},
{},
(err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 6, 'verify short token wrong length');
assert.match(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
assert.deepEqual(spyEmailer.result(), [
{ args: [
'resendVerifySignup',
sanitizeUserForEmail(db[i]),
{ transport: 'email' },
''
],
result: [null]
},
]);
done();
});
});
});
describe('with notification - backwards compatibility', () => {
var db;
var app;
var users;
var spyEmailer;
var verifyReset;
beforeEach(() => {
db = clone(usersDb);
app = feathersStubs.app();
users = feathersStubs.users(app, db, ifNonPaginated, idType);
spyEmailer = new SpyOn(emailer);
verifyResetService({
longTokenLen: 15, // need to reset this
shortTokenLen: 6, // need to reset this
shortTokenDigits: true, // need to reset this
emailer: spyEmailer.callWithCb
}).call(app);
verifyReset = app.service('verifyReset'); // get handle to verifyReset
});
it('is called', (done) => {
const email = 'a';
const i = 0;
verifyReset.create({
action: 'resend',
value: email,
notifierOptions: { transport: 'email' }
},
{},
(err, user) => {
assert.strictEqual(err, null, 'err code set');
assert.strictEqual(user.isVerified, false, 'user.isVerified not false');
assert.strictEqual(db[i].isVerified, false, 'isVerified not false');
assert.isString(db[i].verifyToken, 'verifyToken not String');
assert.equal(db[i].verifyToken.length, 30, 'verify token wrong length');
assert.equal(db[i].verifyShortToken.length, 6, 'verify short token wrong length');
assert.match(db[i].verifyShortToken, /^[0-9]+$/);
aboutEqualDateTime(db[i].verifyExpires, makeDateTime());
assert.deepEqual(spyEmailer.result(), [
{ args: [
'resendVerifySignup',
sanitizeUserForEmail(db[i]),
{ transport: 'email' },
''
],
result: [null]
},
]);
done();
});
});
});
});
});
});
// Helpers
function emailer(action, user, notifierOptions, newEmail, cb) {
cb(null);
}
function makeDateTime(options1) {
options1 = options1 || {};
return Date.now() + (options1.delay || defaultVerifyDelay);
}
function aboutEqualDateTime(time1, time2, msg, delta) {
delta = delta || 500;
const diff = Math.abs(time1 - time2);
assert.isAtMost(diff, delta, msg || `times differ by ${diff}ms`);
}
function sanitizeUserForEmail(user) {
const user1 = clone(user);
delete user1.password;
return user1;
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}