From 956da204f2b199f04ab9764a3408a0a9878f15c4 Mon Sep 17 00:00:00 2001 From: Nazar Gargol Date: Tue, 30 Jul 2019 22:48:59 +0200 Subject: [PATCH] Expanded authentication test suite with cases for password reset flow - Added missing endpoint coverage - Minor fixes with formatting and validations uncovered by the test - Added same test to v0.1 coverage --- core/server/api/v2/authentication.js | 6 +- .../serializers/output/authentication.js | 16 +++ .../utils/validators/input/passwordreset.js | 13 +++ .../api/v0.1/authentication_spec.js | 26 +++++ .../api/v2/admin/authentication_spec.js | 104 +++++++++++++++++- 5 files changed, 161 insertions(+), 4 deletions(-) diff --git a/core/server/api/v2/authentication.js b/core/server/api/v2/authentication.js index ac428341200c..242b70cff34c 100644 --- a/core/server/api/v2/authentication.js +++ b/core/server/api/v2/authentication.js @@ -91,6 +91,9 @@ module.exports = { }, generateResetToken: { + validation: { + docName: 'passwordreset' + }, permissions: true, options: [ 'email' @@ -101,7 +104,7 @@ module.exports = { return auth.setup.assertSetupCompleted(true)(); }) .then(() => { - return auth.passwordreset.generateToken(frame.data.email, api.settings); + return auth.passwordreset.generateToken(frame.data.passwordreset[0].email, api.settings); }) .then((token) => { return auth.passwordreset.sendResetNotification(token, api.mail); @@ -113,7 +116,6 @@ module.exports = { validation: { docName: 'passwordreset', data: { - token: {required: true}, newPassword: {required: true}, ne2Password: {required: true} } diff --git a/core/server/api/v2/utils/serializers/output/authentication.js b/core/server/api/v2/utils/serializers/output/authentication.js index ac4de515ae4e..6c713b9fafab 100644 --- a/core/server/api/v2/utils/serializers/output/authentication.js +++ b/core/server/api/v2/utils/serializers/output/authentication.js @@ -25,6 +25,22 @@ module.exports = { }; }, + generateResetToken(data, apiConfig, frame) { + frame.response = { + passwordreset: [{ + message: common.i18n.t('common.api.authentication.mail.checkEmailForInstructions') + }] + }; + }, + + resetPassword(data, apiConfig, frame) { + frame.response = { + passwordreset: [{ + message: common.i18n.t('common.api.authentication.mail.passwordChanged') + }] + }; + }, + acceptInvitation(data, apiConfig, frame) { debug('acceptInvitation'); diff --git a/core/server/api/v2/utils/validators/input/passwordreset.js b/core/server/api/v2/utils/validators/input/passwordreset.js index 52114714cf96..175baafe1a53 100644 --- a/core/server/api/v2/utils/validators/input/passwordreset.js +++ b/core/server/api/v2/utils/validators/input/passwordreset.js @@ -1,4 +1,5 @@ const Promise = require('bluebird'); +const validator = require('validator'); const debug = require('ghost-ignition').debug('api:v2:utils:validators:input:passwordreset'); const common = require('../../../../../lib/common'); @@ -13,5 +14,17 @@ module.exports = { message: common.i18n.t('errors.models.user.newPasswordsDoNotMatch') })); } + }, + + generateResetToken(apiConfig, frame) { + debug('generateResetToken'); + + const email = frame.data.passwordreset[0].email; + + if (typeof email !== 'string' || !validator.isEmail(email)) { + throw new common.errors.BadRequestError({ + message: common.i18n.t('errors.api.authentication.invalidEmailReceived') + }); + } } }; diff --git a/core/test/regression/api/v0.1/authentication_spec.js b/core/test/regression/api/v0.1/authentication_spec.js index 5ffed710c7b1..503cb047e9a4 100644 --- a/core/test/regression/api/v0.1/authentication_spec.js +++ b/core/test/regression/api/v0.1/authentication_spec.js @@ -32,7 +32,12 @@ describe('Authentication API', function () { }); }); + beforeEach(function () { + sinon.stub(mailService.GhostMailer.prototype, 'send').resolves('Mail is disabled'); + }); + afterEach(function () { + sinon.restore(); return testUtils.clearBruteData(); }); @@ -271,6 +276,27 @@ describe('Authentication API', function () { .expect(401); }); + it('reset password: send reset password', function () { + return request + .post(localUtils.API.getApiQuery('authentication/passwordreset/')) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .send({ + passwordreset: [{ + email: user.email + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + var jsonResponse = res.body; + should.exist(jsonResponse.passwordreset[0].message); + jsonResponse.passwordreset[0].message.should.equal('Check your email for further instructions.'); + mailService.GhostMailer.prototype.send.args[0][0].to.should.equal(user.email); + }); + }); + it('revoke token', function () { return request .post(localUtils.API.getApiQuery('authentication/revoke')) diff --git a/core/test/regression/api/v2/admin/authentication_spec.js b/core/test/regression/api/v2/admin/authentication_spec.js index 938e2daad1da..68b200054a37 100644 --- a/core/test/regression/api/v2/admin/authentication_spec.js +++ b/core/test/regression/api/v2/admin/authentication_spec.js @@ -1,15 +1,18 @@ const should = require('should'); const sinon = require('sinon'); const supertest = require('supertest'); -const testUtils = require('../../../../utils/index'); const localUtils = require('./utils'); +const testUtils = require('../../../../utils/index'); +const models = require('../../../../../server/models/index'); +const security = require('../../../../../server/lib/security/index'); +const settingsCache = require('../../../../../server/services/settings/cache'); const config = require('../../../../../server/config/index'); const mailService = require('../../../../../server/services/mail/index'); let ghost = testUtils.startGhost; let request; -describe.only('Authentication API v2', function () { +describe('Authentication API v2', function () { let ghostServer; describe('Blog setup', function () { @@ -209,4 +212,101 @@ describe.only('Authentication API v2', function () { }); }); }); + + describe('Password reset', function () { + const user = testUtils.DataGenerator.forModel.users[0]; + + before(function () { + return ghost({forceStart: true}) + .then(() => { + request = supertest.agent(config.get('url')); + }) + .then(() => { + return localUtils.doAuth(request); + }); + }); + + beforeEach(function () { + sinon.stub(mailService.GhostMailer.prototype, 'send').resolves('Mail is disabled'); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('reset password', function (done) { + models.User.getOwnerUser(testUtils.context.internal) + .then(function (ownerUser) { + var token = security.tokens.resetToken.generateHash({ + expires: Date.now() + (1000 * 60), + email: user.email, + dbHash: settingsCache.get('db_hash'), + password: ownerUser.get('password') + }); + + request.put(localUtils.API.getApiQuery('authentication/passwordreset')) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .send({ + passwordreset: [{ + token: token, + newPassword: 'thisissupersafe', + ne2Password: 'thisissupersafe' + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + const jsonResponse = res.body; + should.exist(jsonResponse.passwordreset[0].message); + jsonResponse.passwordreset[0].message.should.equal('Password changed successfully.'); + done(); + }); + }) + .catch(done); + }); + + it('reset password: invalid token', function () { + return request + .put(localUtils.API.getApiQuery('authentication/passwordreset')) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .send({ + passwordreset: [{ + token: 'invalid', + newPassword: 'thisissupersafe', + ne2Password: 'thisissupersafe' + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(401); + }); + + it('reset password: generate reset token', function () { + return request + .post(localUtils.API.getApiQuery('authentication/passwordreset')) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .send({ + passwordreset: [{ + email: user.email + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + const jsonResponse = res.body; + should.exist(jsonResponse.passwordreset[0].message); + jsonResponse.passwordreset[0].message.should.equal('Check your email for further instructions.'); + mailService.GhostMailer.prototype.send.args[0][0].to.should.equal(user.email); + }); + }); + }); });