mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
Forgot password submit (#168)
* Authentication middleware * API client refactor * Configure API client to make forgot_password requests * Successfully submit forgot password form * Display server errors for unknown email address
This commit is contained in:
parent
a069ec9acf
commit
5ea9115a95
19 changed files with 577 additions and 75 deletions
|
|
@ -7,6 +7,8 @@ import validEmail from '../validators/valid_email';
|
|||
|
||||
class ForgotPasswordForm extends Component {
|
||||
static propTypes = {
|
||||
clearErrors: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ class ForgotPasswordForm extends Component {
|
|||
}
|
||||
|
||||
onInputFieldChange = (evt) => {
|
||||
const { clearErrors, error: serverError } = this.props;
|
||||
const { value } = evt.target;
|
||||
|
||||
this.setState({
|
||||
|
|
@ -30,6 +33,8 @@ class ForgotPasswordForm extends Component {
|
|||
},
|
||||
});
|
||||
|
||||
if (serverError) clearErrors();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +66,8 @@ class ForgotPasswordForm extends Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { error, formData: { email } } = this.state;
|
||||
const { error: serverError } = this.props;
|
||||
const { error: clientError, formData: { email } } = this.state;
|
||||
const { formStyles, inputStyles, submitButtonStyles } = componentStyles;
|
||||
const { onFormSubmit, onInputFieldChange } = this;
|
||||
const disabled = !email;
|
||||
|
|
@ -69,7 +75,7 @@ class ForgotPasswordForm extends Component {
|
|||
return (
|
||||
<form onSubmit={onFormSubmit} style={formStyles}>
|
||||
<InputFieldWithIcon
|
||||
error={error}
|
||||
error={clientError || serverError}
|
||||
iconName="envelope"
|
||||
name="email"
|
||||
onChange={onInputFieldChange}
|
||||
|
|
|
|||
62
frontend/kolide/base.js
Normal file
62
frontend/kolide/base.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import config from '../config';
|
||||
|
||||
class Base {
|
||||
constructor () {
|
||||
this.baseURL = this.setBaseURL();
|
||||
}
|
||||
|
||||
setBaseURL () {
|
||||
const {
|
||||
settings: { env },
|
||||
environments: { development },
|
||||
} = config;
|
||||
|
||||
if (env === development) {
|
||||
return 'http://localhost:8080/api';
|
||||
}
|
||||
|
||||
throw new Error(`API base URL is not configured for environment: ${env}`);
|
||||
}
|
||||
|
||||
setBearerToken (bearerToken) {
|
||||
this.bearerToken = bearerToken;
|
||||
}
|
||||
|
||||
post(endpoint, body = {}, overrideHeaders = {}) {
|
||||
return this._request('POST', endpoint, body, overrideHeaders);
|
||||
}
|
||||
|
||||
_request (method, endpoint, body, overrideHeaders) {
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
return fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
...overrideHeaders
|
||||
},
|
||||
body,
|
||||
})
|
||||
.then(response => {
|
||||
return response.json()
|
||||
.then(jsonResponse => {
|
||||
if (response.ok) {
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
const error = new Error(response.statusText);
|
||||
error.response = jsonResponse;
|
||||
error.message = jsonResponse;
|
||||
error.error = jsonResponse.error;
|
||||
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Base;
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export default {
|
||||
FORGOT_PASSWORD: '/v1/kolide/forgot_password',
|
||||
LOGIN: '/v1/kolide/login',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,61 +1,21 @@
|
|||
import fetch from 'isomorphic-fetch';
|
||||
import config from '../config';
|
||||
import Base from './base';
|
||||
import endpoints from './endpoints';
|
||||
import local from '../utilities/local';
|
||||
|
||||
class Kolide {
|
||||
constructor () {
|
||||
this.baseURL = this.setBaseURL();
|
||||
}
|
||||
|
||||
setBaseURL () {
|
||||
const {
|
||||
settings: { env },
|
||||
environments: { development },
|
||||
} = config;
|
||||
|
||||
if (env === development) {
|
||||
return 'http://localhost:8080/api';
|
||||
}
|
||||
|
||||
throw new Error(`API base URL is not configured for environment: ${env}`);
|
||||
}
|
||||
|
||||
setBearerToken (bearerToken) {
|
||||
this.bearerToken = bearerToken;
|
||||
}
|
||||
|
||||
class Kolide extends Base {
|
||||
loginUser ({ username, password }) {
|
||||
const { LOGIN } = endpoints;
|
||||
const endpoint = this.baseURL + LOGIN;
|
||||
const loginEndpoint = this.baseURL + LOGIN;
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
.then(response => {
|
||||
return response.json()
|
||||
.then(user => {
|
||||
if (response.ok) {
|
||||
const { token } = user;
|
||||
return this.post(loginEndpoint, JSON.stringify({ username, password }));
|
||||
}
|
||||
|
||||
local.setItem('auth_token', token);
|
||||
this.setBearerToken(token);
|
||||
forgotPassword ({ email }) {
|
||||
const { FORGOT_PASSWORD } = endpoints;
|
||||
const forgotPasswordEndpoint = this.baseURL + FORGOT_PASSWORD;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
const error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
error.message = user.error;
|
||||
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
return this.post(forgotPasswordEndpoint, JSON.stringify({ email }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import expect from 'expect';
|
||||
import Kolide from './index';
|
||||
import { validLoginRequest } from '../test/mocks';
|
||||
import mocks from '../test/mocks';
|
||||
|
||||
const {
|
||||
invalidPasswordResetRequest,
|
||||
validLoginRequest,
|
||||
validPasswordResetRequest,
|
||||
validUser,
|
||||
} = mocks;
|
||||
|
||||
describe('Kolide - API client', () => {
|
||||
describe('defaults', () => {
|
||||
|
|
@ -10,19 +17,49 @@ describe('Kolide - API client', () => {
|
|||
});
|
||||
|
||||
describe('#loginUser', () => {
|
||||
it('sets the bearer token', (done) => {
|
||||
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||
const request = validLoginRequest();
|
||||
|
||||
Kolide.loginUser({
|
||||
username: 'admin',
|
||||
password: 'secret',
|
||||
})
|
||||
.then(() => {
|
||||
.then((user) => {
|
||||
expect(user).toEqual(validUser);
|
||||
expect(request.isDone()).toEqual(true);
|
||||
expect(Kolide.bearerToken).toEqual('auth_token');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#forgotPassword', () => {
|
||||
it('calls the appropriate endpoint with the correct parameters when successful', (done) => {
|
||||
const request = validPasswordResetRequest();
|
||||
const email = 'hi@thegnar.co';
|
||||
|
||||
Kolide.forgotPassword({ email })
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('return errors correctly for unsuccessful requests', (done) => {
|
||||
const error = 'Something went wrong';
|
||||
const request = invalidPasswordResetRequest(error);
|
||||
const email = 'hi@thegnar.co';
|
||||
|
||||
Kolide.forgotPassword({ email })
|
||||
.then(done)
|
||||
.catch(errorResponse => {
|
||||
const { response } = errorResponse;
|
||||
|
||||
expect(response).toEqual({ error });
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,73 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { noop } from 'lodash';
|
||||
import componentStyles from './styles';
|
||||
import {
|
||||
clearForgotPasswordErrors,
|
||||
forgotPasswordAction,
|
||||
} from '../../redux/nodes/components/ForgotPasswordPage/actions';
|
||||
import ForgotPasswordForm from '../../components/forms/ForgotPasswordForm';
|
||||
import Icon from '../../components/icons/Icon';
|
||||
|
||||
export class ForgotPasswordPage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
email: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dispatch: noop,
|
||||
};
|
||||
|
||||
class ForgotPasswordPage extends Component {
|
||||
onSubmit = (formData) => {
|
||||
console.log('ForgotPasswordPage formData', formData);
|
||||
const { dispatch } = this.props;
|
||||
|
||||
return dispatch(forgotPasswordAction(formData));
|
||||
}
|
||||
|
||||
clearErrors = () => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
return dispatch(clearForgotPasswordErrors);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const { clearErrors } = this;
|
||||
const { email, error } = this.props;
|
||||
const {
|
||||
emailSentButtonWrapperStyles,
|
||||
emailSentIconStyles,
|
||||
emailSentTextStyles,
|
||||
emailSentTextWrapperStyles,
|
||||
emailTextStyles,
|
||||
} = componentStyles;
|
||||
|
||||
if (email) {
|
||||
return (
|
||||
<div>
|
||||
<div style={emailSentTextWrapperStyles}>
|
||||
<p style={emailSentTextStyles}>
|
||||
An email was sent to
|
||||
<span style={emailTextStyles}> {email}</span>.
|
||||
Click the link on the email to proceed with the password reset process.
|
||||
</p>
|
||||
</div>
|
||||
<div style={emailSentButtonWrapperStyles}>
|
||||
<Icon name="check" style={emailSentIconStyles} />
|
||||
EMAIL SENT
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ForgotPasswordForm
|
||||
clearErrors={clearErrors}
|
||||
error={error}
|
||||
onSubmit={this.onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
@ -24,11 +87,15 @@ class ForgotPasswordPage extends Component {
|
|||
<div style={forgotPasswordStyles}>
|
||||
<p style={headerStyles}>Forgot Password</p>
|
||||
<p style={textStyles}>If you’ve forgotten your password enter your email below and we will email you a link so that you can reset your password.</p>
|
||||
<ForgotPasswordForm onSubmit={this.onSubmit} />
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ForgotPasswordPage;
|
||||
const mapStateToProps = (state) => {
|
||||
return state.components.ForgotPasswordPage;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ForgotPasswordPage);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { ForgotPasswordPage } from './ForgotPasswordPage';
|
||||
|
||||
describe('ForgotPasswordPage - component', () => {
|
||||
it('renders the ForgotPasswordForm when there is no email prop', () => {
|
||||
const page = mount(<ForgotPasswordPage />);
|
||||
|
||||
expect(page.find('ForgotPasswordForm').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the email sent text when the email prop is present', () => {
|
||||
const email = 'hi@thegnar.co';
|
||||
const page = mount(<ForgotPasswordPage email={email} />);
|
||||
|
||||
expect(page.find('ForgotPasswordForm').length).toEqual(0);
|
||||
expect(page.text()).toInclude(`An email was sent to ${email}.`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -9,6 +9,34 @@ export default {
|
|||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
emailSentButtonWrapperStyles: {
|
||||
backgroundColor: color.successLight,
|
||||
borderRadius: border.radius.base,
|
||||
color: color.white,
|
||||
padding: padding.base,
|
||||
position: 'relative',
|
||||
textAlign: 'center',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
emailSentIconStyles: {
|
||||
height: '35px',
|
||||
left: '18px',
|
||||
position: 'absolute',
|
||||
top: '14px',
|
||||
width: '35px',
|
||||
},
|
||||
emailSentTextStyles: {
|
||||
fontSize: font.medium,
|
||||
},
|
||||
emailSentTextWrapperStyles: {
|
||||
padding: padding.base,
|
||||
backgroundColor: color.accentLight,
|
||||
borderRadius: border.radius.base,
|
||||
marginBottom: padding.base,
|
||||
},
|
||||
emailTextStyles: {
|
||||
color: color.link,
|
||||
},
|
||||
forgotPasswordStyles: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: color.white,
|
||||
|
|
|
|||
16
frontend/redux/middlewares/auth.js
Normal file
16
frontend/redux/middlewares/auth.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import kolide from '../../kolide';
|
||||
import { LOGIN_SUCCESS } from '../nodes/auth/actions';
|
||||
import local from '../../utilities/local';
|
||||
|
||||
const authMiddleware = store => next => action => {
|
||||
if (action.type === LOGIN_SUCCESS) {
|
||||
const { token } = action.payload.data;
|
||||
local.setItem('auth_token', token);
|
||||
kolide.setBearerToken(token);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default authMiddleware;
|
||||
|
|
@ -27,7 +27,7 @@ export const loginUser = (formData) => {
|
|||
return (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch(loginRequest);
|
||||
Kolide.loginUser(formData)
|
||||
return Kolide.loginUser(formData)
|
||||
.then(user => {
|
||||
dispatch(loginSuccess(user));
|
||||
return resolve(user);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import configureStore from 'redux-mock-store';
|
||||
import expect from 'expect';
|
||||
import thunk from 'redux-thunk';
|
||||
import authMiddleware from '../../middlewares/auth';
|
||||
import kolide from '../../../kolide';
|
||||
import local from '../../../utilities/local';
|
||||
import { loginRequest, LOGIN_REQUEST, LOGIN_SUCCESS, loginUser } from './actions';
|
||||
import reducer, { initialState } from './reducer';
|
||||
import { loginRequest } from './actions';
|
||||
import { validLoginRequest, validUser } from '../../../test/mocks';
|
||||
|
||||
describe('Auth - reducer', () => {
|
||||
it('sets the initial state', () => {
|
||||
|
|
@ -17,4 +23,71 @@ describe('Auth - reducer', () => {
|
|||
loading: true,
|
||||
});
|
||||
});
|
||||
|
||||
context('loginUser action', () => {
|
||||
const formData = {
|
||||
username: 'username',
|
||||
password: 'p@ssw0rd',
|
||||
};
|
||||
const middlewares = [thunk, authMiddleware];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const store = mockStore({});
|
||||
|
||||
|
||||
it('calls the api login endpoint', (done) => {
|
||||
const loginRequestMock = validLoginRequest();
|
||||
store.dispatch(loginUser(formData))
|
||||
.then(user => {
|
||||
expect(loginRequestMock.isDone()).toEqual(true);
|
||||
expect(local.getItem('auth_token')).toEqual(user.token);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('returns the authenticated user', (done) => {
|
||||
validLoginRequest();
|
||||
|
||||
store.dispatch(loginUser(formData))
|
||||
.then(user => {
|
||||
expect(user).toEqual(validUser);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('sets the users auth token in local storage', (done) => {
|
||||
validLoginRequest();
|
||||
|
||||
store.dispatch(loginUser(formData))
|
||||
.then(user => {
|
||||
expect(local.getItem('auth_token')).toEqual(user.token);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('sets the api client bearerToken', (done) => {
|
||||
validLoginRequest();
|
||||
|
||||
store.dispatch(loginUser(formData))
|
||||
.then(user => {
|
||||
expect(kolide.bearerToken).toEqual(user.token);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('dispatches LOGIN_REQUEST and LOGIN_SUCCESS actions', (done) => {
|
||||
validLoginRequest();
|
||||
|
||||
store.dispatch(loginUser(formData))
|
||||
.then(() => {
|
||||
const actionTypes = store.getActions().map(a => a.type);
|
||||
expect(actionTypes).toInclude(LOGIN_REQUEST, LOGIN_SUCCESS);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import Kolide from '../../../../kolide';
|
||||
|
||||
export const CLEAR_FORGOT_PASSWORD_ERRORS = 'CLEAR_FORGOT_PASSWORD_ERRORS';
|
||||
export const FORGOT_PASSWORD_REQUEST = 'FORGOT_PASSWORD_REQUEST';
|
||||
export const FORGOT_PASSWORD_SUCCESS = 'FORGOT_PASSWORD_SUCCESS';
|
||||
export const FORGOT_PASSWORD_ERROR = 'FORGOT_PASSWORD_ERROR';
|
||||
|
||||
export const clearForgotPasswordErrors = { type: CLEAR_FORGOT_PASSWORD_ERRORS };
|
||||
export const forgotPasswordRequestAction = { type: FORGOT_PASSWORD_REQUEST };
|
||||
export const forgotPasswordSuccessAction = (email) => {
|
||||
return {
|
||||
type: FORGOT_PASSWORD_SUCCESS,
|
||||
payload: {
|
||||
data: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
export const forgotPasswordErrorAction = (error) => {
|
||||
return {
|
||||
type: FORGOT_PASSWORD_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// formData should be { email: <string> }
|
||||
export const forgotPasswordAction = (formData) => {
|
||||
return (dispatch) => {
|
||||
dispatch(forgotPasswordRequestAction);
|
||||
return Kolide.forgotPassword(formData)
|
||||
.then(() => {
|
||||
const { email } = formData;
|
||||
|
||||
return dispatch(forgotPasswordSuccessAction(email));
|
||||
})
|
||||
.catch(response => {
|
||||
const { error } = response;
|
||||
|
||||
dispatch(forgotPasswordErrorAction(error));
|
||||
throw response;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
CLEAR_FORGOT_PASSWORD_ERRORS,
|
||||
FORGOT_PASSWORD_ERROR,
|
||||
FORGOT_PASSWORD_REQUEST,
|
||||
FORGOT_PASSWORD_SUCCESS,
|
||||
} from './actions';
|
||||
|
||||
export const initialState = {
|
||||
email: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
const reducer = (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case CLEAR_FORGOT_PASSWORD_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
case FORGOT_PASSWORD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
};
|
||||
case FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
email: payload.data.email,
|
||||
error: null,
|
||||
loading: false,
|
||||
};
|
||||
case FORGOT_PASSWORD_ERROR:
|
||||
return {
|
||||
...state,
|
||||
email: null,
|
||||
error: payload.error,
|
||||
loading: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import configureStore from 'redux-mock-store';
|
||||
import expect from 'expect';
|
||||
import thunk from 'redux-thunk';
|
||||
import {
|
||||
clearForgotPasswordErrors,
|
||||
forgotPasswordAction,
|
||||
forgotPasswordRequestAction,
|
||||
forgotPasswordSuccessAction,
|
||||
forgotPasswordErrorAction,
|
||||
} from './actions';
|
||||
import { invalidPasswordResetRequest, validPasswordResetRequest } from '../../../../test/mocks';
|
||||
import reducer, { initialState } from './reducer';
|
||||
|
||||
describe('ForgotPasswordPage - reducer', () => {
|
||||
describe('initial state', () => {
|
||||
it('sets the initial state', () => {
|
||||
expect(reducer(undefined, { type: 'FAKE-ACTION' })).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearForgotPasswordErrors', () => {
|
||||
it('changes the loading state to true', () => {
|
||||
const errorState = {
|
||||
...initialState,
|
||||
error: 'Something went wrong',
|
||||
};
|
||||
|
||||
expect(reducer(errorState, clearForgotPasswordErrors)).toEqual({
|
||||
...errorState,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgotPasswordRequestAction', () => {
|
||||
it('changes the loading state to true', () => {
|
||||
expect(reducer(initialState, forgotPasswordRequestAction)).toEqual({
|
||||
...initialState,
|
||||
loading: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgotPasswordSuccessAction', () => {
|
||||
it('changes the loading state to false and emailSent to true', () => {
|
||||
const email = 'hi@thegnar.co';
|
||||
|
||||
expect(reducer(initialState, forgotPasswordSuccessAction(email))).toEqual({
|
||||
...initialState,
|
||||
email,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgotPasswordErrorAction', () => {
|
||||
it('changes the loading state to false and sets the error state', () => {
|
||||
const error = 'There was an error with your request';
|
||||
|
||||
expect(reducer(initialState, forgotPasswordErrorAction(error))).toEqual({
|
||||
...initialState,
|
||||
error,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgotPasswordAction', () => {
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
it('dispatches the appropriate actions when successful', (done) => {
|
||||
const formData = { email: 'hi@thegnar.co' };
|
||||
const request = validPasswordResetRequest();
|
||||
const store = mockStore({});
|
||||
|
||||
store.dispatch(forgotPasswordAction(formData))
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toInclude(forgotPasswordRequestAction);
|
||||
expect(actions).toInclude(forgotPasswordSuccessAction(formData.email));
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('dispatches the appropriate actions when unsuccessful', (done) => {
|
||||
const formData = { email: 'hi@thegnar.co' };
|
||||
const error = 'Something went wrong';
|
||||
const invalidRequest = invalidPasswordResetRequest(error);
|
||||
const store = mockStore({});
|
||||
|
||||
store.dispatch(forgotPasswordAction(formData))
|
||||
.then(done)
|
||||
.catch(errorResponse => {
|
||||
const actions = store.getActions();
|
||||
const { response } = errorResponse;
|
||||
|
||||
expect(response).toEqual({ error });
|
||||
expect(actions).toInclude(forgotPasswordErrorAction(error));
|
||||
expect(invalidRequest.isDone()).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
7
frontend/redux/nodes/components/reducer.js
Normal file
7
frontend/redux/nodes/components/reducer.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import ForgotPasswordPage from './ForgotPasswordPage/reducer';
|
||||
|
||||
export default combineReducers({
|
||||
ForgotPasswordPage,
|
||||
});
|
||||
|
||||
|
|
@ -2,9 +2,11 @@ import { combineReducers } from 'redux';
|
|||
import { routerReducer } from 'react-router-redux';
|
||||
import app from './nodes/app/reducer';
|
||||
import auth from './nodes/auth/reducer';
|
||||
import components from './nodes/components/reducer';
|
||||
|
||||
export default combineReducers({
|
||||
app,
|
||||
auth,
|
||||
components,
|
||||
routing: routerReducer,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,18 +2,21 @@ import { createStore, applyMiddleware, compose } from 'redux';
|
|||
import thunkMiddleware from 'redux-thunk';
|
||||
import { browserHistory } from 'react-router';
|
||||
import { routerMiddleware } from 'react-router-redux';
|
||||
import authMiddleware from './middlewares/auth';
|
||||
import reducers from './reducers';
|
||||
|
||||
const initialState = {};
|
||||
const middleware = [
|
||||
|
||||
const appliedMiddleware = applyMiddleware(
|
||||
thunkMiddleware,
|
||||
routerMiddleware(browserHistory),
|
||||
];
|
||||
const appliedMiddleware = applyMiddleware(...middleware);
|
||||
authMiddleware,
|
||||
);
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
compose(appliedMiddleware),
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
const grey = '#66696f';
|
||||
|
||||
export default {
|
||||
accentLight: '#EAEDFB',
|
||||
brightPurple: '#AE6DDF',
|
||||
darkGrey: '#202532',
|
||||
green: '#4FD061',
|
||||
grey,
|
||||
lightGrey: '#B4B4B4',
|
||||
link: '#4A90E2',
|
||||
logoPurple: '#9651CA',
|
||||
mediumGrey: '#6F737F',
|
||||
primary: grey,
|
||||
purple: '#c38dec',
|
||||
purpleGrey: '#858495',
|
||||
red: '#FF5850',
|
||||
successLight: '#94E39F',
|
||||
white: '#FFF',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,37 @@
|
|||
import nock from 'nock';
|
||||
|
||||
export const validUser = {
|
||||
token: 'auth_token',
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@kolide.co',
|
||||
name: '',
|
||||
admin: true,
|
||||
enabled: true,
|
||||
needs_password_reset: false,
|
||||
};
|
||||
|
||||
export const validLoginRequest = () => {
|
||||
return nock('http://localhost:8080')
|
||||
.post('/api/v1/kolide/login')
|
||||
.reply(200, {
|
||||
token: 'auth_token',
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@kolide.co',
|
||||
name: '',
|
||||
admin: true,
|
||||
enabled: true,
|
||||
needs_password_reset: false,
|
||||
});
|
||||
.reply(200, validUser);
|
||||
};
|
||||
|
||||
export const validPasswordResetRequest = () => {
|
||||
return nock('http://localhost:8080')
|
||||
.post('/api/v1/kolide/forgot_password')
|
||||
.reply(200, validUser);
|
||||
};
|
||||
|
||||
export const invalidPasswordResetRequest = (error) => {
|
||||
return nock('http://localhost:8080')
|
||||
.post('/api/v1/kolide/forgot_password')
|
||||
.reply(422, { error });
|
||||
};
|
||||
|
||||
export default {
|
||||
invalidPasswordResetRequest,
|
||||
validLoginRequest,
|
||||
validPasswordResetRequest,
|
||||
validUser,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue