diff --git a/config/express.js b/config/express.js index 20da68f7..a096ef77 100644 --- a/config/express.js +++ b/config/express.js @@ -34,6 +34,23 @@ module.exports = function(app, historicSync, peerSync) { app.use(express.methodOverride()); app.use(express.compress()); + if (config.enableEmailstore) { + var allowCopayCrossDomain = function(req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); + res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization'); + + + if ('OPTIONS' == req.method) { + res.send(200); + res.end(); + return; + } + next(); + } + app.use(allowCopayCrossDomain); + } + if (config.publicPath) { var staticPath = path.normalize(config.rootPath + '/../' + config.publicPath); //IMPORTANT: for html5mode, this line must to be before app.router diff --git a/config/routes.js b/config/routes.js index c99ed8e2..cd449407 100644 --- a/config/routes.js +++ b/config/routes.js @@ -57,7 +57,9 @@ module.exports = function(app) { app.post(apiPrefix + '/email/register', emailPlugin.post); app.post(apiPrefix + '/email/validate', emailPlugin.validate); app.get(apiPrefix + '/email/retrieve/:email', emailPlugin.get); + app.get(apiPrefix + '/email/retrieve', emailPlugin.retrieve); app.get(apiPrefix + '/email/validate', emailPlugin.validate); + app.post(apiPrefix + '/email/change_passphrase', emailPlugin.changePassphrase); } // Address routes diff --git a/plugins/emailstore.js b/plugins/emailstore.js index 52a177e5..de470b3f 100644 --- a/plugins/emailstore.js +++ b/plugins/emailstore.js @@ -39,15 +39,17 @@ 'use strict'; -var logger = require('../lib/logger').logger; -var levelup = require('levelup'); -var async = require('async'); -var crypto = require('crypto'); -var querystring = require('querystring'); -var nodemailer = require('nodemailer'); -var globalConfig = require('../config/config'); var _ = require('lodash'); +var async = require('async'); +var bitcore = require('bitcore'); +var crypto = require('crypto'); var fs = require('fs'); +var levelup = require('levelup'); +var nodemailer = require('nodemailer'); +var querystring = require('querystring'); + +var logger = require('../lib/logger').logger; +var globalConfig = require('../config/config'); var emailPlugin = {}; @@ -81,16 +83,29 @@ emailPlugin.errors = { } }; -var NAMESPACE = 'credentials-store-'; +var EMAIL_TO_PASSPHRASE = 'email-to-passphrase-'; +var STORED_VALUE = 'emailstore-'; +var PENDING = 'pending-'; +var VALIDATED = 'validated-'; + var SEPARATOR = '#'; -var VALIDATION_NAMESPACE = 'validation-code-'; -var MAP_EMAIL_TO_SECRET = 'map-email-'; -var EMAIL_NAMESPACE = 'validated-email-'; var MAX_ALLOWED_STORAGE = 1024 * 100 /* no more than 100 kb */; -var makeKey = function(email, key) { - return NAMESPACE + email + SEPARATOR + key; -} +var valueKey = function(email, key) { + return STORED_VALUE + bitcore.util.twoSha256(email + SEPARATOR + key).toString('hex'); +}; + +var pendingKey = function(email) { + return PENDING + email; +}; + +var validatedKey = function(email) { + return VALIDATED + bitcore.util.twoSha256(email).toString('hex'); +}; + +var emailToPassphrase = function(email) { + return EMAIL_TO_PASSPHRASE + bitcore.util.twoSha256(email).toString('hex'); +}; /** * Initializes the plugin @@ -108,6 +123,8 @@ emailPlugin.init = function (config) { emailPlugin.textTemplate = config.textTemplate || 'copay.plain'; emailPlugin.htmlTemplate = config.htmlTemplate || 'copay.html'; + emailPlugin.crypto = config.crypto || crypto; + emailPlugin.confirmUrl = ( process.env.INSIGHT_EMAIL_CONFIRM_HOST || config.confirmUrl @@ -207,11 +224,11 @@ emailPlugin.makeEmailHTMLBody = applyTemplate('htmlTemplate'); * @param {Function(err, boolean)} callback */ emailPlugin.exists = function(email, callback) { - emailPlugin.db.get(MAP_EMAIL_TO_SECRET + email, function(err, value) { + emailPlugin.db.get(emailToPassphrase(email), function(err, value) { if (err && err.notFound) { return callback(null, false); } else if (err) { - return callback(err); + return callback(emailPlugin.errors.INTERNAL_ERROR); } return callback(null, true); }); @@ -223,11 +240,12 @@ emailPlugin.exists = function(email, callback) { * @param {Function(err, boolean)} callback */ emailPlugin.checkPassphrase = function(email, passphrase, callback) { - emailPlugin.db.get(MAP_EMAIL_TO_SECRET + email, function(err, retrievedPassphrase) { + emailPlugin.db.get(emailToPassphrase(email), function(err, retrievedPassphrase) { if (err) { if (err.notFound) { return callback(emailPlugin.errors.INVALID_CODE); } + logger.error('error checking passphrase', email, err); return callback(emailPlugin.errors.INTERNAL_ERROR); } return callback(err, passphrase === retrievedPassphrase); @@ -240,7 +258,13 @@ emailPlugin.checkPassphrase = function(email, passphrase, callback) { * @param {Function(err)} callback */ emailPlugin.savePassphrase = function(email, passphrase, callback) { - emailPlugin.db.put(MAP_EMAIL_TO_SECRET + email, passphrase, callback); + emailPlugin.db.put(emailToPassphrase(email), passphrase, function(err) { + if (err) { + logger.error('error saving passphrase', err); + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + return callback(null); + }); }; /** @@ -250,13 +274,20 @@ emailPlugin.savePassphrase = function(email, passphrase, callback) { * @param {Function(err)} callback */ emailPlugin.saveEncryptedData = function(email, key, record, callback) { - emailPlugin.db.put(makeKey(email, key), record, callback); + emailPlugin.db.put(valueKey(email, key), record, function(err) { + if (err) { + logger.error('error saving encrypted data', email, key, record, err); + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + return callback(); + }); }; emailPlugin.createVerificationSecretAndSendEmail = function (email, callback) { emailPlugin.createVerificationSecret(email, function(err, secret) { if (err) { - return callback(err); + logger.error('error saving verification secret', email, secret, err); + return callback(emailPlugin.errors.INTERNAL_ERROR); } if (secret) { emailPlugin.sendVerificationEmail(email, secret); @@ -265,6 +296,58 @@ emailPlugin.createVerificationSecretAndSendEmail = function (email, callback) { }); }; +/** + * Creates and stores a verification secret in the database. + * + * @param {string} email - the user's email + * @param {Function} callback - will be called with params (err, secret) + */ +emailPlugin.createVerificationSecret = function (email, callback) { + emailPlugin.db.get(pendingKey(email), function(err, value) { + if (err && err.notFound) { + var secret = emailPlugin.crypto.randomBytes(16).toString('hex'); + emailPlugin.db.put(pendingKey(email), secret, function (err) { + if (err) { + logger.error('error saving pending data:', email, secret); + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + return callback(null, secret); + }); + } else { + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + }); +}; + +/** + * @param {string} email + * @param {Function(err)} callback + */ +emailPlugin.retrieveByEmailAndKey = function(email, key, callback) { + emailPlugin.db.get(valueKey(email, key), function(error, value) { + if (error) { + if (error.notFound) { + return callback(emailPlugin.errors.NOT_FOUND); + } + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + return callback(null, value); + }); +}; + +emailPlugin.retrieveDataByEmailAndPassphrase = function(email, key, passphrase, callback) { + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err) { + return callback(err); + } + if (matches) { + return emailPlugin.retrieveByEmailAndKey(email, key, callback); + } else { + return callback(emailPlugin.errors.INVALID_CODE); + } + }); +}; + /** * Store a record in the database. The underlying database is merely a levelup instance (a key * value store) that uses the email concatenated with the secret as a key to store the record. @@ -279,6 +362,12 @@ emailPlugin.createVerificationSecretAndSendEmail = function (email, callback) { emailPlugin.post = function (request, response) { var queryData = ''; + var credentials = emailPlugin.getCredentialsFromRequest(request); + if (credentials.code) { + return emailPlugin.returnError(credentials, response); + } + var email = credentials.email; + var passphrase = credentials.passphrase; request.on('data', function (data) { queryData += data; @@ -289,19 +378,17 @@ emailPlugin.post = function (request, response) { } }).on('end', function () { var params = querystring.parse(queryData); - var email = params.email; var key = params.key; - var secret = params.secret; var record = params.record; - if (!email || !secret || !record || !key) { + if (!email || !passphrase || !record || !key) { return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); } - emailPlugin.processPost(request, response, email, key, secret, record); + emailPlugin.processPost(request, response, email, key, passphrase, record); }); }; -emailPlugin.processPost = function(request, response, email, key, secret, record) { +emailPlugin.processPost = function(request, response, email, key, passphrase, record) { async.series([ /** * Try to fetch this user's email. If it exists, check the secret is the same. @@ -311,7 +398,7 @@ emailPlugin.processPost = function(request, response, email, key, secret, record if (err) { return callback(err); } else if (exists) { - emailPlugin.checkPassphrase(email, secret, function(err, match) { + emailPlugin.checkPassphrase(email, passphrase, function(err, match) { if (err) { return callback(err); } @@ -322,9 +409,9 @@ emailPlugin.processPost = function(request, response, email, key, secret, record } }); } else { - emailPlugin.savePassphrase(email, secret, function(err) { + emailPlugin.savePassphrase(email, passphrase, function(err) { if (err) { - return callback({code: 500, message: err}); + return callback(err); } return callback(); }); @@ -365,62 +452,14 @@ emailPlugin.processPost = function(request, response, email, key, secret, record }; /** - * Creates and stores a verification secret in the database. - * - * @param {string} email - the user's email - * @param {Function} callback - will be called with params (err, secret) - */ -emailPlugin.createVerificationSecret = function (email, callback) { - emailPlugin.db.get(VALIDATION_NAMESPACE + email, function(err, value) { - if (err && err.notFound) { - var secret = crypto.randomBytes(16).toString('hex'); - emailPlugin.db.put(VALIDATION_NAMESPACE + email, secret, function (err, value) { - if (err) { - return callback(err); - } - callback(err, secret); - }); - } else { - callback(err, null); - } - }); -}; - -/** - * @param {string} email - * @param {Function(err)} callback - */ -emailPlugin.retrieveByEmailAndKey = function(email, key, callback) { - emailPlugin.db.get(makeKey(email, key), function(error, value) { - if (error) { - if (error.notFound) { - return callback(emailPlugin.errors.NOT_FOUND); - } - return callback(emailPlugin.errors.INTERNAL_ERROR); - } - return callback(null, value); - }); -}; - -emailPlugin.retrieveDataByEmailAndPassphrase = function(email, key, passphrase, callback) { - emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { - if (err) { - return callback(err); - } - if (matches) { - return emailPlugin.retrieveByEmailAndKey(email, key, callback); - } else { - return callback(emailPlugin.errors.INVALID_CODE); - } - }); -}; - -/** - * Retrieve a record from the database. + * Retrieve a record from the database (deprecated) * * The request is expected to contain the parameters: + * * email * * secret + * * key * + * @deprecated * @param {Express.Request} request * @param {Express.Response} response */ @@ -440,6 +479,45 @@ emailPlugin.get = function (request, response) { }); }; +emailPlugin.getCredentialsFromRequest = function(request) { + if (!request.header('authorization')) { + return emailPlugin.errors.INVALID_REQUEST; + } + var authHeader = new Buffer(request.header('authorization'), 'base64').toString('utf8'); + var splitIndex = authHeader.indexOf(':'); + if (splitIndex === -1) { + return emailPlugin.errors.INVALID_REQUEST; + } + var email = authHeader.substr(0, splitIndex); + var passphrase = authHeader.substr(splitIndex + 1); + + return {email: email, passphrase: passphrase}; +}; + +/** + * Retrieve a record from the database + */ +emailPlugin.retrieve = function (request, response) { + var credentialsResult = emailPlugin.getCredentialsFromRequest(request); + if (_.contains(emailPlugin.errors, credentialsResult)) { + return emailPlugin.returnError(credentialsResult); + } + var email = credentialsResult.email; + var passphrase = credentialsResult.passphrase; + + var key = request.param('key'); + if (!passphrase || !email || !key) { + return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); + } + + emailPlugin.retrieveDataByEmailAndPassphrase(email, key, passphrase, function (err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } + response.send(value).end(); + }); +}; + /** * Marks an email as validated * @@ -457,7 +535,7 @@ emailPlugin.validate = function (request, response) { return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); } - emailPlugin.db.get(VALIDATION_NAMESPACE + email, function (err, value) { + emailPlugin.db.get(pendingKey(email), function (err, value) { if (err) { if (err.notFound) { return emailPlugin.returnError(emailPlugin.errors.NOT_FOUND, response); @@ -466,17 +544,65 @@ emailPlugin.validate = function (request, response) { } else if (value !== secret) { return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response); } else { - emailPlugin.db.put(EMAIL_NAMESPACE + email, true, function (err, value) { + emailPlugin.db.put(validatedKey(email), true, function (err, value) { if (err) { return emailPlugin.returnError({code: 500, message: err}, response); } else { - response.redirect(emailPlugin.redirectUrl); + emailPlugin.db.remove(validatedKey(email), function (err, value) { + if (err) { + return emailPlugin.returnError({code: 500, message: err}, response); + } else { + response.redirect(emailPlugin.redirectUrl); + } + }); } }); } }); }; +/** + * Changes an user's passphrase + * + * @param {Express.Request} request + * @param {Express.Response} response + */ +emailPlugin.changePassphrase = function (request, response) { + var credentialsResult = emailPlugin.getCredentialsFromRequest(request); + if (_.contains(emailPlugin.errors, credentialsResult)) { + return emailPlugin.returnError(credentialsResult); + } + var email = credentialsResult.email; + var passphrase = credentialsResult.passphrase; + + var queryData = ''; + request.on('data', function (data) { + queryData += data; + if (queryData.length > MAX_ALLOWED_STORAGE) { + queryData = ''; + response.writeHead(413, {'Content-Type': 'text/plain'}).end(); + request.connection.destroy(); + } + }).on('end', function () { + var params = querystring.parse(queryData); + var newPassphrase = params.passphrase; + if (!email || !passphrase || !newPassphrase) { + return emailPlugin.returnError(emailPlugin.errors.INVALID_REQUEST, response); + } + emailPlugin.checkPassphrase(email, passphrase, function (error) { + if (error) { + return emailPlugin.returnError(error, response); + } + emailPlugin.savePassphrase(email, newPassphrase, function (error) { + if (error) { + return emailPlugin.returnError(error, response); + } + return response.json({success: true}).end(); + }); + }); + }); +}; + module.exports = emailPlugin; })(); diff --git a/test/test.EmailStore.js b/test/test.EmailStore.js index ceac4bc6..608e6493 100644 --- a/test/test.EmailStore.js +++ b/test/test.EmailStore.js @@ -1,13 +1,15 @@ 'use strict'; -var chai = require('chai'), - assert = require('assert'), - sinon = require('sinon'), - logger = require('../lib/logger').logger, - should = chai.should, - expect = chai.expect; +var chai = require('chai'); +var assert = require('assert'); +var sinon = require('sinon'); +var crypto = require('crypto'); +var bitcore = require('bitcore'); +var logger = require('../lib/logger').logger; +var should = chai.should; +var expect = chai.expect; -logger.transports.console.level = 'warn'; +logger.transports.console.level = 'non'; describe('emailstore test', function() { @@ -17,9 +19,14 @@ describe('emailstore test', function() { var leveldb_stub = sinon.stub(); leveldb_stub.put = sinon.stub(); leveldb_stub.get = sinon.stub(); + leveldb_stub.remove = sinon.stub(); var email_stub = sinon.stub(); email_stub.sendMail = sinon.stub(); + var cryptoMock = { + randomBytes: sinon.stub() + }; + var plugin = require('../plugins/emailstore'); var express_mock = null; var request = null; @@ -27,12 +34,11 @@ describe('emailstore test', function() { beforeEach(function() { - // Mock request and response objects (but don't configure behavior) - express_mock = sinon.stub(); - express_mock.post = sinon.stub(); - express_mock.get = sinon.stub(); - - plugin.init({db: leveldb_stub, emailTransport: email_stub}); + plugin.init({ + db: leveldb_stub, + emailTransport: email_stub, + crypto: cryptoMock + }); request = sinon.stub(); request.on = sinon.stub(); @@ -49,6 +55,198 @@ describe('emailstore test', function() { assert(plugin.db === leveldb_stub); }); + describe('database queries', function() { + + describe('exists', function() { + var fakeEmail = 'fake@email.com'; + var fakeEmailKey = 'email-to-passphrase-' + bitcore.util.twoSha256(fakeEmail).toString('hex'); + + beforeEach(function() { + leveldb_stub.get.reset(); + }); + + it('validates that an email is already registered', function(done) { + leveldb_stub.get.onFirstCall().callsArg(1); + + plugin.exists(fakeEmail, function(err, exists) { + leveldb_stub.get.firstCall.args[0].should.equal(fakeEmailKey); + exists.should.equal(true); + done(); + }); + }); + + it('returns false when an email doesn\'t exist', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, {notFound: true}); + + plugin.exists(fakeEmail, function(err, exists) { + leveldb_stub.get.firstCall.args[0].should.equal(fakeEmailKey); + exists.should.equal(false); + done(); + }); + }); + + it('returns an internal error if database query couldn\'t be made', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, 'error'); + plugin.exists(fakeEmail, function(err, exists) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + }); + + describe('passphrase', function() { + var fakeEmail = 'fake@email.com'; + var fakePassphrase = 'secretPassphrase123'; + + beforeEach(function() { + leveldb_stub.get.reset(); + leveldb_stub.put.reset(); + }); + + it('returns true if passphrase matches', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, null, fakePassphrase); + + plugin.checkPassphrase(fakeEmail, fakePassphrase, function(err, result) { + result.should.equal(true); + done(); + }); + }); + + it('returns false if passphrsase doesn\'t match', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, null, 'invalid passphrase'); + + plugin.checkPassphrase(fakeEmail, fakePassphrase, function(err, result) { + result.should.equal(false); + done(); + }); + }); + + it('returns an internal error if database query couldn\'t be made', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, 'error'); + + plugin.checkPassphrase(fakeEmail, fakePassphrase, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + + it('stores passphrase correctly', function(done) { + leveldb_stub.put.onFirstCall().callsArg(2); + + plugin.savePassphrase(fakeEmail, fakePassphrase, function(err) { + expect(err).to.equal(null); + done(); + }); + }); + + it('doesn\'t store the email in the key', function(done) { + leveldb_stub.put.onFirstCall().callsArg(2); + + plugin.savePassphrase(fakeEmail, fakePassphrase, function(err) { + leveldb_stub.put.firstCall.args[0].should.not.contain(fakeEmail); + done(); + }); + }); + + it('returns internal error on database error', function(done) { + leveldb_stub.put.onFirstCall().callsArgWith(2, 'error'); + + plugin.savePassphrase(fakeEmail, fakePassphrase, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + }); + + describe('saving encrypted data', function() { + var fakeEmail = 'fake@email.com'; + var fakeKey = 'nameForData'; + var fakeRecord = 'fakeRecord'; + var expectedKey = 'emailstore-' + + bitcore.util.twoSha256(fakeEmail + '#' + fakeKey).toString('hex'); + + beforeEach(function() { + leveldb_stub.get.reset(); + leveldb_stub.put.reset(); + }); + + it('saves data under the expected key', function(done) { + leveldb_stub.put.onFirstCall().callsArgWith(2); + + plugin.saveEncryptedData(fakeEmail, fakeKey, fakeRecord, function(err) { + leveldb_stub.put.firstCall.args[0].should.equal(expectedKey); + done(); + }); + }); + + it('fails with INTERNAL_ERROR on database error', function(done) { + leveldb_stub.put.onFirstCall().callsArgWith(2, 'error'); + + plugin.saveEncryptedData(fakeEmail, fakeKey, fakeRecord, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + }); + + describe('creating verification secret', function() { + var sendVerificationEmail = sinon.stub(plugin, 'sendVerificationEmail'); + var fakeEmail = 'fake@email.com'; + var fakeRandom = 'fakerandom'; + var randomBytes = {toString: function() { return fakeRandom; }}; + + beforeEach(function() { + leveldb_stub.get.reset(); + leveldb_stub.put.reset(); + + sendVerificationEmail.reset(); + cryptoMock.randomBytes = sinon.stub(); + cryptoMock.randomBytes.onFirstCall().returns(randomBytes); + }); + + var setupLevelDb = function() { + leveldb_stub.get.onFirstCall().callsArgWith(1, {notFound: true}); + leveldb_stub.put.onFirstCall().callsArg(2); + }; + + it('saves data under the expected key', function(done) { + setupLevelDb(); + + plugin.createVerificationSecretAndSendEmail(fakeEmail, function(err) { + leveldb_stub.put.firstCall.args[1].should.equal(fakeRandom); + done(); + }); + }); + it('calls the function to verify the email', function(done) { + setupLevelDb(); + + plugin.createVerificationSecretAndSendEmail(fakeEmail, function(err) { + sendVerificationEmail.calledOnce; + done(); + }); + }); + it('returns internal error on put database error', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, {notFound: true}); + leveldb_stub.put.onFirstCall().callsArgWith(2, 'error'); + plugin.createVerificationSecretAndSendEmail(fakeEmail, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + it('returns internal error on get database error', function(done) { + leveldb_stub.get.onFirstCall().callsArgWith(1, 'error'); + plugin.createVerificationSecretAndSendEmail(fakeEmail, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + + after(function() { + plugin.sendVerificationEmail.restore(); + }); + }); + }); + describe('on registration', function() { var emailParam = 'email'; @@ -65,6 +263,11 @@ describe('emailstore test', function() { }); it('should allow new registrations', function() { + plugin.getCredentialsFromRequest = sinon.mock(); + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: emailParam, + passphrase: secretParam + }); plugin.exists = sinon.stub(); plugin.exists.onFirstCall().callsArgWith(1, null, false); plugin.savePassphrase = sinon.stub(); @@ -87,6 +290,11 @@ describe('emailstore test', function() { }); it('should allow to overwrite data', function() { + plugin.getCredentialsFromRequest = sinon.mock(); + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: emailParam, + passphrase: secretParam + }); plugin.exists = sinon.stub(); plugin.exists.onFirstCall().callsArgWith(1, null, true); plugin.checkPassphrase = sinon.stub(); @@ -119,12 +327,15 @@ describe('emailstore test', function() { request.param.onSecondCall().returns(secret); leveldb_stub.put = sinon.stub(); leveldb_stub.get = sinon.stub(); + leveldb_stub.remove = sinon.stub(); leveldb_stub.put.onFirstCall().callsArg(2); + leveldb_stub.remove.onFirstCall().callsArg(1); response.json.returnsThis(); }); it('should validate correctly an email if the secret matches', function() { leveldb_stub.get.onFirstCall().callsArgWith(1, null, secret); + response.redirect = sinon.stub(); plugin.validate(request, response); @@ -167,5 +378,51 @@ describe('emailstore test', function() { assert(response.end.calledOnce); }); }); + + describe('changing the user password', function() { + + var originalCredentials = plugin.getCredentialsFromRequest; + + beforeEach(function() { + plugin.getCredentialsFromRequest = sinon.mock(); + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + passphrase: 'passphrase' + }); + request.on = sinon.stub(); + request.on.onFirstCall().callsArgWith(1, 'newPassphrase=newPassphrase'); + request.on.onFirstCall().returns(request); + request.on.onSecondCall().callsArg(1); + response.status.onFirstCall().returnsThis(); + plugin.checkPassphrase = sinon.stub(); + plugin.savePassphrase = sinon.stub(); + }); + + it('should validate the previous passphrase', function() { + response.status.onFirstCall().returnsThis(); + response.json.onFirstCall().returnsThis(); + plugin.checkPassphrase.onFirstCall().callsArgWith(2, 'error'); + + plugin.changePassphrase(request, response); + + assert(response.status.calledOnce); + assert(response.json.calledOnce); + assert(response.end.calledOnce); + }); + + it('should change the passphrase', function() { + response.json.onFirstCall().returnsThis(); + plugin.checkPassphrase.onFirstCall().callsArgWith(2, null); + plugin.savePassphrase.onFirstCall().callsArgWith(2, null); + + plugin.changePassphrase(request, response); + assert(response.json.calledOnce); + assert(response.end.calledOnce); + }); + + after(function() { + plugin.getCredentialsFromRequest = originalCredentials; + }); + }); });