mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Reset password page submit (#184)
* API client sends reset password requests * ResetPasswordPage actions and reducer * Reset password happy path
This commit is contained in:
parent
482d025d05
commit
0e08a6e698
11 changed files with 301 additions and 15 deletions
|
|
@ -1,4 +1,5 @@
|
|||
export default {
|
||||
FORGOT_PASSWORD: '/v1/kolide/forgot_password',
|
||||
LOGIN: '/v1/kolide/login',
|
||||
RESET_PASSWORD: '/v1/kolide/reset_password',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
36
frontend/redux/nodes/components/ResetPasswordPage/actions.js
Normal file
36
frontend/redux/nodes/components/ResetPasswordPage/actions.js
Normal 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;
|
||||
});
|
||||
};
|
||||
};
|
||||
41
frontend/redux/nodes/components/ResetPasswordPage/reducer.js
Normal file
41
frontend/redux/nodes/components/ResetPasswordPage/reducer.js
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import ForgotPasswordPage from './ForgotPasswordPage/reducer';
|
||||
import ResetPasswordPage from './ResetPasswordPage/reducer';
|
||||
|
||||
export default combineReducers({
|
||||
ForgotPasswordPage,
|
||||
ResetPasswordPage,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue