Reset password page submit (#184)

* API client sends reset password requests

* ResetPasswordPage actions and reducer

* Reset password happy path
This commit is contained in:
Mike Stone 2016-09-19 11:35:38 -04:00 committed by GitHub
parent 482d025d05
commit 0e08a6e698
11 changed files with 301 additions and 15 deletions

View file

@ -1,4 +1,5 @@
export default {
FORGOT_PASSWORD: '/v1/kolide/forgot_password',
LOGIN: '/v1/kolide/login',
RESET_PASSWORD: '/v1/kolide/reset_password',
};

View file

@ -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();

View file

@ -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();
});
});
});
});

View file

@ -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,
};
};

View file

@ -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 }));

View file

@ -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))

View file

@ -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: <string>, password_reset_token: <string> }
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;
});
};
};

View file

@ -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;
}
};

View file

@ -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();
});
});
});
});

View file

@ -1,7 +1,9 @@
import { combineReducers } from 'redux';
import ForgotPasswordPage from './ForgotPasswordPage/reducer';
import ResetPasswordPage from './ResetPasswordPage/reducer';
export default combineReducers({
ForgotPasswordPage,
ResetPasswordPage,
});

View file

@ -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,
};