From 0e08a6e6987631f7246cba7295fb3e77810385c7 Mon Sep 17 00:00:00 2001 From: Mike Stone Date: Mon, 19 Sep 2016 11:35:38 -0400 Subject: [PATCH] Reset password page submit (#184) * API client sends reset password requests * ResetPasswordPage actions and reducer * Reset password happy path --- frontend/kolide/endpoints.js | 1 + frontend/kolide/index.js | 7 ++ frontend/kolide/index.tests.js | 50 +++++++- .../ResetPasswordPage/ResetPasswordPage.jsx | 15 ++- .../ResetPasswordPage.tests.jsx | 12 +- .../ForgotPasswordPage/reducer.tests.js | 6 +- .../components/ResetPasswordPage/actions.js | 36 ++++++ .../components/ResetPasswordPage/reducer.js | 41 ++++++ .../ResetPasswordPage/reducer.tests.js | 117 ++++++++++++++++++ frontend/redux/nodes/components/reducer.js | 2 + frontend/test/mocks.js | 29 ++++- 11 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 frontend/redux/nodes/components/ResetPasswordPage/actions.js create mode 100644 frontend/redux/nodes/components/ResetPasswordPage/reducer.js create mode 100644 frontend/redux/nodes/components/ResetPasswordPage/reducer.tests.js diff --git a/frontend/kolide/endpoints.js b/frontend/kolide/endpoints.js index 31b0621d6f..c1a3b7ced7 100644 --- a/frontend/kolide/endpoints.js +++ b/frontend/kolide/endpoints.js @@ -1,4 +1,5 @@ export default { FORGOT_PASSWORD: '/v1/kolide/forgot_password', LOGIN: '/v1/kolide/login', + RESET_PASSWORD: '/v1/kolide/reset_password', }; diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index bd2b072b50..fad42eaec5 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -17,6 +17,13 @@ class Kolide extends Base { return this.post(forgotPasswordEndpoint, JSON.stringify({ email })); } + + resetPassword (formData) { + const { RESET_PASSWORD } = endpoints; + const resetPasswordEndpoint = this.baseURL + RESET_PASSWORD; + + return this.post(resetPasswordEndpoint, JSON.stringify(formData)); + } } export default new Kolide(); diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js index 5b41ef58af..15874015f2 100644 --- a/frontend/kolide/index.tests.js +++ b/frontend/kolide/index.tests.js @@ -3,9 +3,11 @@ import Kolide from './index'; import mocks from '../test/mocks'; const { - invalidPasswordResetRequest, + invalidForgotPasswordRequest, + invalidResetPasswordRequest, + validForgotPasswordRequest, validLoginRequest, - validPasswordResetRequest, + validResetPasswordRequest, validUser, } = mocks; @@ -35,7 +37,7 @@ describe('Kolide - API client', () => { describe('#forgotPassword', () => { it('calls the appropriate endpoint with the correct parameters when successful', (done) => { - const request = validPasswordResetRequest(); + const request = validForgotPasswordRequest(); const email = 'hi@thegnar.co'; Kolide.forgotPassword({ email }) @@ -48,7 +50,7 @@ describe('Kolide - API client', () => { it('return errors correctly for unsuccessful requests', (done) => { const error = 'Something went wrong'; - const request = invalidPasswordResetRequest(error); + const request = invalidForgotPasswordRequest(error); const email = 'hi@thegnar.co'; Kolide.forgotPassword({ email }) @@ -62,4 +64,44 @@ describe('Kolide - API client', () => { }); }); }); + + describe('#resetPassword', () => { + const newPassword = 'p@ssw0rd'; + + it('calls the appropriate endpoint with the correct parameters when successful', (done) => { + const passwordResetToken = 'password-reset-token'; + const request = validResetPasswordRequest(newPassword, passwordResetToken); + const formData = { + new_password: newPassword, + password_reset_token: passwordResetToken, + }; + + Kolide.resetPassword(formData) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + + it('return errors correctly for unsuccessful requests', (done) => { + const error = 'Resource not found'; + const passwordResetToken = 'invalid-password-reset-token'; + const request = invalidResetPasswordRequest(newPassword, passwordResetToken, error); + const formData = { + new_password: newPassword, + password_reset_token: passwordResetToken, + }; + + Kolide.resetPassword(formData) + .then(done) + .catch(errorResponse => { + const { response } = errorResponse; + + expect(response).toEqual({ error }); + expect(request.isDone()).toEqual(true); + done(); + }); + }); + }); }); diff --git a/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx b/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx index 4555f34ad6..b10d6fc7cf 100644 --- a/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx +++ b/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx @@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { noop } from 'lodash'; import { push } from 'react-router-redux'; +import { resetPassword } from '../../redux/nodes/components/ResetPasswordPage/actions'; import ResetPasswordForm from '../../components/forms/ResetPasswordForm'; import StackedWhiteBoxes from '../../components/StackedWhiteBoxes'; @@ -24,8 +25,16 @@ export class ResetPasswordPage extends Component { } onSubmit = (formData) => { - console.log('ResetPasswordForm data', formData); - return false; + const { dispatch, token } = this.props; + const resetPasswordData = { + ...formData, + password_reset_token: token, + }; + + return dispatch(resetPassword(resetPasswordData)) + .then(() => { + return dispatch(push('/login')); + }); } render () { @@ -45,8 +54,10 @@ export class ResetPasswordPage extends Component { const mapStateToProps = (state, ownProps) => { const { query = {} } = ownProps.location || {}; const { token } = query; + const { ResetPasswordPage: componentState } = state.components; return { + ...componentState, token, }; }; diff --git a/frontend/pages/ResetPasswordPage/ResetPasswordPage.tests.jsx b/frontend/pages/ResetPasswordPage/ResetPasswordPage.tests.jsx index 1fbe95eecd..921d5d8cd3 100644 --- a/frontend/pages/ResetPasswordPage/ResetPasswordPage.tests.jsx +++ b/frontend/pages/ResetPasswordPage/ResetPasswordPage.tests.jsx @@ -12,6 +12,7 @@ describe('ResetPasswordPage - component', () => { }); it('Redirects to the login page when there is no token', () => { + const { connectedComponent, reduxMockStore } = testHelpers; const redirectToLoginAction = { type: '@@router/CALL_HISTORY_METHOD', payload: { @@ -19,8 +20,15 @@ describe('ResetPasswordPage - component', () => { args: ['/login'], }, }; - const { reduxMockStore, connectedComponent } = testHelpers; - const mockStore = reduxMockStore(); + const store = { + components: { + ResetPasswordPage: { + loading: false, + error: null, + }, + }, + }; + const mockStore = reduxMockStore(store); mount(connectedComponent(ConnectedPage, { mockStore })); diff --git a/frontend/redux/nodes/components/ForgotPasswordPage/reducer.tests.js b/frontend/redux/nodes/components/ForgotPasswordPage/reducer.tests.js index 4bda184918..b90d3b34c3 100644 --- a/frontend/redux/nodes/components/ForgotPasswordPage/reducer.tests.js +++ b/frontend/redux/nodes/components/ForgotPasswordPage/reducer.tests.js @@ -8,7 +8,7 @@ import { forgotPasswordSuccessAction, forgotPasswordErrorAction, } from './actions'; -import { invalidPasswordResetRequest, validPasswordResetRequest } from '../../../../test/mocks'; +import { invalidForgotPasswordRequest, validForgotPasswordRequest } from '../../../../test/mocks'; import reducer, { initialState } from './reducer'; describe('ForgotPasswordPage - reducer', () => { @@ -71,7 +71,7 @@ describe('ForgotPasswordPage - reducer', () => { it('dispatches the appropriate actions when successful', (done) => { const formData = { email: 'hi@thegnar.co' }; - const request = validPasswordResetRequest(); + const request = validForgotPasswordRequest(); const store = mockStore({}); store.dispatch(forgotPasswordAction(formData)) @@ -89,7 +89,7 @@ describe('ForgotPasswordPage - reducer', () => { it('dispatches the appropriate actions when unsuccessful', (done) => { const formData = { email: 'hi@thegnar.co' }; const error = 'Something went wrong'; - const invalidRequest = invalidPasswordResetRequest(error); + const invalidRequest = invalidForgotPasswordRequest(error); const store = mockStore({}); store.dispatch(forgotPasswordAction(formData)) diff --git a/frontend/redux/nodes/components/ResetPasswordPage/actions.js b/frontend/redux/nodes/components/ResetPasswordPage/actions.js new file mode 100644 index 0000000000..1b731efa15 --- /dev/null +++ b/frontend/redux/nodes/components/ResetPasswordPage/actions.js @@ -0,0 +1,36 @@ +import Kolide from '../../../../kolide'; + +export const CLEAR_RESET_PASSWORD_ERRORS = 'CLEAR_RESET_PASSWORD_ERRORS'; +export const RESET_PASSWORD_ERROR = 'RESET_PASSWORD_ERROR'; +export const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST'; +export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS'; + +export const clearResetPasswordErrors = { type: CLEAR_RESET_PASSWORD_ERRORS }; +export const resetPasswordError = (error) => { + return { + type: RESET_PASSWORD_ERROR, + payload: { + error, + }, + }; +}; +export const resetPasswordRequest = { type: RESET_PASSWORD_REQUEST }; +export const resetPasswordSuccess = { type: RESET_PASSWORD_SUCCESS }; + +// formData should be { new_password: , password_reset_token: } +export const resetPassword = (formData) => { + return (dispatch) => { + dispatch(resetPasswordRequest); + + return Kolide.resetPassword(formData) + .then(() => { + return dispatch(resetPasswordSuccess); + }) + .catch(response => { + const { error } = response; + + dispatch(resetPasswordError(error)); + throw response; + }); + }; +}; diff --git a/frontend/redux/nodes/components/ResetPasswordPage/reducer.js b/frontend/redux/nodes/components/ResetPasswordPage/reducer.js new file mode 100644 index 0000000000..c186e708e3 --- /dev/null +++ b/frontend/redux/nodes/components/ResetPasswordPage/reducer.js @@ -0,0 +1,41 @@ +import { + CLEAR_RESET_PASSWORD_ERRORS, + RESET_PASSWORD_ERROR, + RESET_PASSWORD_REQUEST, + RESET_PASSWORD_SUCCESS, +} from './actions'; + +export const initialState = { + error: null, + loading: false, +}; + +export default (state = initialState, { type, payload }) => { + switch (type) { + case CLEAR_RESET_PASSWORD_ERRORS: + return { + ...state, + error: null, + }; + case RESET_PASSWORD_ERROR: + return { + ...state, + error: payload.error, + loading: false, + }; + case RESET_PASSWORD_REQUEST: + return { + ...state, + loading: true, + }; + case RESET_PASSWORD_SUCCESS: + return { + ...state, + error: null, + loading: false, + }; + default: + return state; + } +}; + diff --git a/frontend/redux/nodes/components/ResetPasswordPage/reducer.tests.js b/frontend/redux/nodes/components/ResetPasswordPage/reducer.tests.js new file mode 100644 index 0000000000..0785f1bb3a --- /dev/null +++ b/frontend/redux/nodes/components/ResetPasswordPage/reducer.tests.js @@ -0,0 +1,117 @@ +import expect from 'expect'; +import { + clearResetPasswordErrors, + resetPassword, + resetPasswordRequest, + resetPasswordSuccess, + resetPasswordError, +} from './actions'; +import { invalidResetPasswordRequest, validResetPasswordRequest } from '../../../../test/mocks'; +import reducer, { initialState } from './reducer'; +import { reduxMockStore } from '../../../../test/helpers'; + +describe('ResetPasswordPage - reducer', () => { + describe('initial state', () => { + it('sets the initial state', () => { + expect(reducer(undefined, { type: 'FAKE-ACTION' })).toEqual(initialState); + }); + }); + + describe('clearResetPasswordErrors', () => { + it('changes the loading state to true', () => { + const errorState = { + ...initialState, + error: 'Something went wrong', + }; + + expect(reducer(errorState, clearResetPasswordErrors)).toEqual({ + ...errorState, + error: null, + }); + }); + }); + + describe('resetPasswordRequest', () => { + it('changes the loading state to true', () => { + expect(reducer(initialState, resetPasswordRequest)).toEqual({ + ...initialState, + loading: true, + }); + }); + }); + + describe('resetPasswordSuccess', () => { + it('changes the loading state to false and errors to null', () => { + const loadingStateWithError = { + loading: true, + error: 'Something went wrong', + }; + + expect(reducer(loadingStateWithError, resetPasswordSuccess)).toEqual({ + loading: false, + error: null, + }); + }); + }); + + describe('resetPasswordError', () => { + it('changes the loading state to false and sets the error state', () => { + const error = 'There was an error with your request'; + + expect(reducer(initialState, resetPasswordError(error))).toEqual({ + ...initialState, + error, + loading: false, + }); + }); + }); + + describe('resetPassword', () => { + const newPassword = 'p@ssw0rd'; + + it('dispatches the appropriate actions when successful', (done) => { + const token = 'valid-password-reset-token'; + const formData = { + new_password: newPassword, + password_reset_token: token, + }; + const request = validResetPasswordRequest(newPassword, token); + const store = reduxMockStore(); + + store.dispatch(resetPassword(formData)) + .then(() => { + const actions = store.getActions(); + + expect(actions).toInclude(resetPasswordRequest); + expect(actions).toInclude(resetPasswordSuccess); + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + + it('dispatches the appropriate actions when unsuccessful', (done) => { + const token = 'invalid-password-reset-token'; + + const formData = { + new_password: newPassword, + password_reset_token: token, + }; + const error = 'Something went wrong'; + const invalidRequest = invalidResetPasswordRequest(newPassword, token, error); + const store = reduxMockStore(); + + store.dispatch(resetPassword(formData)) + .then(done) + .catch(errorResponse => { + const actions = store.getActions(); + const { response } = errorResponse; + + expect(response).toEqual({ error }); + expect(actions).toInclude(resetPasswordError(error)); + expect(invalidRequest.isDone()).toEqual(true); + done(); + }); + }); + }); +}); diff --git a/frontend/redux/nodes/components/reducer.js b/frontend/redux/nodes/components/reducer.js index 17ef20fe1e..d95da95038 100644 --- a/frontend/redux/nodes/components/reducer.js +++ b/frontend/redux/nodes/components/reducer.js @@ -1,7 +1,9 @@ import { combineReducers } from 'redux'; import ForgotPasswordPage from './ForgotPasswordPage/reducer'; +import ResetPasswordPage from './ResetPasswordPage/reducer'; export default combineReducers({ ForgotPasswordPage, + ResetPasswordPage, }); diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js index 3f69843aa1..dd5f6a1398 100644 --- a/frontend/test/mocks.js +++ b/frontend/test/mocks.js @@ -17,21 +17,42 @@ export const validLoginRequest = () => { .reply(200, validUser); }; -export const validPasswordResetRequest = () => { +export const validForgotPasswordRequest = () => { return nock('http://localhost:8080') .post('/api/v1/kolide/forgot_password') .reply(200, validUser); }; -export const invalidPasswordResetRequest = (error) => { +export const invalidForgotPasswordRequest = (error) => { return nock('http://localhost:8080') .post('/api/v1/kolide/forgot_password') .reply(422, { error }); }; +export const validResetPasswordRequest = (password, token) => { + return nock('http://localhost:8080') + .post('/api/v1/kolide/reset_password', JSON.stringify({ + new_password: password, + password_reset_token: token, + })) + .reply(200, validUser); +}; + +export const invalidResetPasswordRequest = (password, token, error) => { + return nock('http://localhost:8080') + .post('/api/v1/kolide/reset_password', JSON.stringify({ + new_password: password, + password_reset_token: token, + })) + .reply(422, { error }); +}; + + export default { - invalidPasswordResetRequest, + invalidForgotPasswordRequest, + invalidResetPasswordRequest, + validForgotPasswordRequest, validLoginRequest, - validPasswordResetRequest, + validResetPasswordRequest, validUser, };