node-local-auth
Version:
Framework agnostic library for secure username/email/password authentication including registration and password management
372 lines (283 loc) • 14.2 kB
JavaScript
'use strict';
const chai = require('chai');
chai.use(require('chai-shallow-deep-equal'));
const assert = chai.assert;
const UserStoreFake = require('./fakes/userStoreFake');
const TokenStoreFake = require('./fakes/tokenStoreFake');
const EmailServiceFake = require('./fakes/emailServiceFake');
const hashAlgoFake = require('./fakes/hashAlgoFake');
const Registration = require('../lib/registration');
const ValidationError = require('../lib/errors/validationError');
const DuplicateRegistrationError = require('../lib/errors/duplicateRegistrationError');
const _ = require('lodash');
const testUtils = require('./testUtils');
describe('Registration', () => {
const ValidEmail = 'foo@example.com';
const ValidUsername = 'username';
const ValidPassword = 'password';
let userStoreFake;
let tokenStoreFake;
let emailServiceFake;
let sut;
beforeEach(() => {
userStoreFake = new UserStoreFake();
tokenStoreFake = new TokenStoreFake();
emailServiceFake = new EmailServiceFake();
sut = createSut();
});
function createSut(options, services) {
const opts = _.merge({
userStore: userStoreFake,
tokenStore: tokenStoreFake,
hashAlgo: hashAlgoFake,
emailService: emailServiceFake
}, services);
return new Registration(
opts.userStore,
opts.tokenStore,
opts.hashAlgo,
opts.emailService,
options
);
}
describe('User Registration', () => {
it('should require email address', function *() {
let expectedErr;
try {
const email = '';
yield sut.register(email, ValidPassword, ValidUsername);
} catch (e) {
expectedErr = e;
}
assertError(expectedErr, ValidationError, 'Valid email address required');
});
it('should require valid email address', function *() {
let expectedErr;
try {
const email = 'foo';
yield sut.register(email, ValidPassword, ValidUsername);
} catch (e) {
expectedErr = e;
}
assertError(expectedErr, ValidationError, 'Valid email address required');
});
it('should require password', function *() {
let expectedErr;
try {
const password = '';
yield sut.register(ValidEmail, password, ValidUsername);
} catch (e) {
expectedErr = e;
}
assertError(expectedErr, ValidationError, 'Password required');
});
it('should allow registration with username, email and password', function *() {
yield sut.register('foo@example.com', 'the-password', 'the-username');
assert.lengthOf(userStoreFake.users, 1, 'User registered');
assert.deepEqual(userStoreFake.users[0], {
username: 'the-username',
email: 'foo@example.com',
id: "User#1",
hashedPassword: 'hashed:the-password'
});
});
it('should allow registration with just email and password', function *() {
assert.lengthOf(userStoreFake.users, 0, 'No user registered yet');
yield sut.register('foo@example.com', 'the-password');
assert.lengthOf(userStoreFake.users, 1, 'User registered');
assert.deepEqual(userStoreFake.users[0], {
email: 'foo@example.com',
id: "User#1",
hashedPassword: 'hashed:the-password'
});
});
it('should normalize the case of the email address when registering in order to avoid confusion', function *() {
assert.lengthOf(userStoreFake.users, 0, 'No user registered yet');
yield sut.register('FOO@EXAMPLE.COM', 'the-password');
assert.lengthOf(userStoreFake.users, 1, 'User registered');
assert.deepEqual(userStoreFake.users[0], {
email: 'foo@example.com',
hashedPassword: 'hashed:the-password',
id: "User#1"
});
});
it('should prevent same user registering more than once', function *() {
yield sut.register('foo@example.com', 'the-password');
let regErr;
try {
yield sut.register('foo@example.com', 'the-password');
} catch (e) {
regErr = e;
}
assertError(regErr, DuplicateRegistrationError, 'A user with that email address already exists');
});
it('should use email service to send registration email', function *() {
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 0, 'no calls yet');
yield sut.register('foo@example.com', 'the-password', 'the-username');
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 1, 'reg email sent');
const callArgs = emailServiceFake.calls.sendRegistrationEmail[0];
const userDetails = callArgs[0];
assert.deepEqual(userDetails, {
email: 'foo@example.com',
username: 'the-username'
});
});
function assertError(err, ErrorType, expectedMsg) {
assert.ok(err, 'Expect error');
assert.instanceOf(err, ErrorType, 'error type');
assert.equal(err.message, expectedMsg);
}
});
describe('User Registration With Email Verification', () => {
beforeEach(function() {
sut = createSut({
verifyEmail: true
});
});
it('provides email address verification token when sending registration email', function *() {
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 0, 'no calls yet');
yield sut.register('foo@example.com', 'the-password', 'the-username');
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 1, 'reg email sent');
const verifyParams = emailServiceFake.calls.sendRegistrationEmail[0][1];
assert.equal(verifyParams.email, 'foo@example.com');
assert.ok(verifyParams.token, 'has a token');
assert.equal(verifyParams.queryString, '?email=foo@example.com&token=' + verifyParams.token);
});
it('registers user with emailVerified property set to false initially', function *() {
assert.lengthOf(userStoreFake.users, 0, 'No user registered yet');
yield sut.register('foo@example.com', 'the-password');
assert.lengthOf(userStoreFake.users, 1, 'User registered');
const user = userStoreFake.users[0];
assert.isFalse(user.emailVerified, 'emailVerified set to false');
});
describe('Verifying email', () => {
it('requires email address when verifying email', function *() {
const email = '';
const err = yield testUtils.assertThrows(function *() {
yield sut.verifyEmail(email, 'the-token');
});
assert.equal(err.message, 'Valid email address required');
});
it('requires token address when verifying email', function *() {
const token = '';
const err = yield testUtils.assertThrows(function *() {
yield sut.verifyEmail('foo@example.com', token);
});
assert.equal(err.message, 'Verify email token required');
});
it('rejects attempt to verify email with invalid token', function *() {
const invalidToken = 'unknown-token';
const verifyParams = yield registerUserAndGetVerifyParams();
const err = yield testUtils.assertThrows(function *() {
yield sut.verifyEmail(verifyParams.email, invalidToken);
});
assert.equal(err.message, 'Unknown or invalid token');
});
it('rejects attempt to verify email with token for unknown user', function *() {
const verifyParams = yield registerUserAndGetVerifyParams();
// Some time later, user deletes their account
// Clear all users:
userStoreFake.users.length = 0;
const err = yield testUtils.assertThrows(function *() {
yield sut.verifyEmail(verifyParams.email, verifyParams.token);
});
assert.equal(err.message, 'User not found');
});
it('marks user email address verified given valid token', function *() {
assert.lengthOf(userStoreFake.users, 0, 'No user registered yet');
const verifyParams = yield registerUserAndGetVerifyParams();
yield sut.verifyEmail(verifyParams.email, verifyParams.token);
assert.lengthOf(userStoreFake.users, 1, 'User registered');
const user = userStoreFake.users[0];
assert.isTrue(user.emailVerified, 'emailVerified set to true');
});
it('ignores case of email address when verifying email address', function *() {
assert.lengthOf(userStoreFake.users, 0, 'No user registered yet');
const verifyParams = yield registerUserAndGetVerifyParams({
email: 'foo@example.com'
});
yield sut.verifyEmail('FoO@EXAMPLE.com', verifyParams.token);
assert.lengthOf(userStoreFake.users, 1, 'User registered');
const user = userStoreFake.users[0];
assert.isTrue(user.emailVerified, 'emailVerified set to true');
});
it('removes email verification token after use', function *() {
const verifyParams = yield registerUserAndGetVerifyParams();
assert.lengthOf(tokenStoreFake.tokens, 1, 'token stored');
yield sut.verifyEmail(verifyParams.email, verifyParams.token);
assert.lengthOf(tokenStoreFake.tokens, 0, 'token removed');
});
function *registerUserAndGetVerifyParams(opts) {
opts = opts || {};
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 0, 'no calls yet');
yield sut.register(opts.email || 'foo@example.com', opts.password || 'the-password');
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 1, 'reg email sent');
return emailServiceFake.calls.sendRegistrationEmail[0][1];
}
});
});
describe('User Unregistration', () => {
it('attempting to unregister when not logged in will throw an AuthenticationError', function*() {
const loggedInUserEmail = null;
const err = yield testUtils.assertThrows(function *() {
yield sut.unregister(loggedInUserEmail);
});
assert.equal(err.message, 'Unauthenticated')
});
it('should remove existing user from userStore when unregistering', function *() {
assert.lengthOf(userStoreFake.users, 0, 'No user registered yet');
const user = yield sut.register('foo@example.com', 'the-password');
assert.lengthOf(userStoreFake.users, 1, 'User registered');
yield sut.unregister(user.email);
assert.lengthOf(userStoreFake.users, 0, 'User removed');
});
});
describe('Multi Tenant Registration', function() {
beforeEach(function() {
sut = createSut({
verifyEmail: true
});
});
it('can add a tenantId to user during registration', function *() {
assert.lengthOf(userStoreFake.users, 0);
yield sut.register('foo@example.com', 'the-password', 'the-username', 'tenant-1');
assert.lengthOf(userStoreFake.users, 1);
assert.deepEqual(userStoreFake.users[0], {
tenantId: 'tenant-1',
username: 'the-username',
email: 'foo@example.com',
emailVerified: false,
id: "User#1",
hashedPassword: 'hashed:the-password'
});
});
it('can add a tenantId to verify email token during registration', function *() {
assert.lengthOf(tokenStoreFake.tokens, 0);
const username = null;
yield sut.register('foo@example.com', 'the-password', username, 'tenant-1');
assert.lengthOf(tokenStoreFake.tokens, 1);
const storedTokenObj = tokenStoreFake.tokens[0];
assert.shallowDeepEqual(storedTokenObj, {
tenantId: 'tenant-1',
email: 'foo@example.com'
});
assert.ok(storedTokenObj.hashedToken);
});
it('provides tenantId when sending registration email', function *() {
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 0, 'no calls yet');
yield sut.register('foo@example.com', 'the-password', 'the-username', 'tenant-1');
assert.equal(emailServiceFake.calls.sendRegistrationEmail.length, 1, 'reg email sent');
const verifyParams = emailServiceFake.calls.sendRegistrationEmail[0][1];
assert.equal(verifyParams.tenantId, 'tenant-1');
assert.equal(verifyParams.email, 'foo@example.com');
assert.ok(verifyParams.token, 'has a token');
assert.equal(verifyParams.queryString, '?email=foo@example.com&tenant=tenant-1&token=' + verifyParams.token);
});
describe('Verifying Email', function() {
// TODO: make sure optionalTenantId passed to all services
});
describe('Unregistering', function() {
// TODO: make sure optionalTenantId passed to all services
});
});
});