mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Allows users to update their email address (#1232)
This commit is contained in:
parent
1bb1c959ae
commit
cc37cfa828
@ -0,0 +1,45 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import helpers from 'components/EmailTokenRedirect/helpers';
|
||||
import userInterface from 'interfaces/user';
|
||||
|
||||
export class EmailTokenRedirect extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
token: PropTypes.string.isRequired,
|
||||
user: userInterface,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { dispatch, token, user } = this.props;
|
||||
|
||||
return helpers.confirmEmailChange(dispatch, token, user);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch, token: newToken, user: newUser } = nextProps;
|
||||
const { token: oldToken, user: oldUser } = this.props;
|
||||
|
||||
const missingProps = !oldToken || !oldUser;
|
||||
|
||||
if (missingProps) {
|
||||
return helpers.confirmEmailChange(dispatch, newToken, newUser);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, { params }) => {
|
||||
const { token } = params;
|
||||
const { user } = state.auth;
|
||||
|
||||
return { token, user };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EmailTokenRedirect);
|
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import expect, { spyOn, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { connectedComponent, reduxMockStore } from 'test/helpers';
|
||||
import ConnectedEmailTokenRedirect, { EmailTokenRedirect } from 'components/EmailTokenRedirect/EmailTokenRedirect';
|
||||
import Kolide from 'kolide';
|
||||
import { userStub } from 'test/stubs';
|
||||
|
||||
describe('EmailTokenRedirect - component', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.users, 'confirmEmailChange')
|
||||
.andReturn(Promise.resolve({ ...userStub, email: 'new@email.com' }));
|
||||
});
|
||||
|
||||
const authStore = {
|
||||
auth: {
|
||||
user: userStub,
|
||||
},
|
||||
};
|
||||
const token = 'KFBR392';
|
||||
const defaultProps = {
|
||||
params: {
|
||||
token,
|
||||
},
|
||||
};
|
||||
|
||||
describe('componentWillMount', () => {
|
||||
it('calls the API when a token and user are present', () => {
|
||||
const mockStore = reduxMockStore(authStore);
|
||||
|
||||
mount(connectedComponent(ConnectedEmailTokenRedirect, {
|
||||
mockStore,
|
||||
props: defaultProps,
|
||||
}));
|
||||
|
||||
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(userStub, token);
|
||||
});
|
||||
|
||||
it('does not call the API when only a token is present', () => {
|
||||
const mockStore = reduxMockStore({ auth: {} });
|
||||
|
||||
mount(connectedComponent(ConnectedEmailTokenRedirect, {
|
||||
mockStore,
|
||||
props: defaultProps,
|
||||
}));
|
||||
|
||||
expect(Kolide.users.confirmEmailChange).toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentWillReceiveProps', () => {
|
||||
it('calls the API when a user is received', () => {
|
||||
const mockStore = reduxMockStore();
|
||||
const props = { dispatch: mockStore.dispatch, token };
|
||||
const Component = mount(<EmailTokenRedirect {...props} />);
|
||||
|
||||
expect(Kolide.users.confirmEmailChange).toNotHaveBeenCalled();
|
||||
|
||||
Component.setProps({ user: userStub });
|
||||
|
||||
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(userStub, token);
|
||||
});
|
||||
});
|
||||
});
|
25
frontend/components/EmailTokenRedirect/helpers.js
Normal file
25
frontend/components/EmailTokenRedirect/helpers.js
Normal file
@ -0,0 +1,25 @@
|
||||
import PATHS from 'router/paths';
|
||||
import { push } from 'react-router-redux';
|
||||
import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||
import userActions from 'redux/nodes/entities/users/actions';
|
||||
|
||||
const confirmEmailChange = (dispatch, token, user) => {
|
||||
if (user && token) {
|
||||
return dispatch(userActions.confirmEmailChange(user, token))
|
||||
.then(() => {
|
||||
dispatch(push(PATHS.USER_SETTINGS));
|
||||
dispatch(renderFlash('success', 'Email updated successfully!'));
|
||||
|
||||
return false;
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(push(PATHS.LOGIN));
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export default { confirmEmailChange };
|
122
frontend/components/EmailTokenRedirect/helpers.tests.js
Normal file
122
frontend/components/EmailTokenRedirect/helpers.tests.js
Normal file
@ -0,0 +1,122 @@
|
||||
import expect, { spyOn, restoreSpies } from 'expect';
|
||||
import { reduxMockStore } from 'test/helpers';
|
||||
|
||||
import helpers from 'components/EmailTokenRedirect/helpers';
|
||||
import Kolide from 'kolide';
|
||||
import { userStub } from 'test/stubs';
|
||||
|
||||
describe('EmailTokenRedirect - helpers', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
describe('#confirmEmailChage', () => {
|
||||
const { confirmEmailChange } = helpers;
|
||||
const token = 'KFBR392';
|
||||
const authStore = {
|
||||
auth: {
|
||||
user: userStub,
|
||||
},
|
||||
};
|
||||
|
||||
describe('successfully dispatching the confirmEmailChange action', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.users, 'confirmEmailChange')
|
||||
.andReturn(Promise.resolve({ ...userStub, email: 'new@email.com' }));
|
||||
});
|
||||
|
||||
it('pushes the user to the settings page', (done) => {
|
||||
const mockStore = reduxMockStore(authStore);
|
||||
const { dispatch } = mockStore;
|
||||
|
||||
confirmEmailChange(dispatch, userStub, token)
|
||||
.then(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toInclude({
|
||||
type: '@@router/CALL_HISTORY_METHOD',
|
||||
payload: {
|
||||
method: 'push',
|
||||
args: ['/settings'],
|
||||
},
|
||||
});
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessfully dispatching the confirmEmailChange action', () => {
|
||||
beforeEach(() => {
|
||||
const errors = [
|
||||
{
|
||||
name: 'base',
|
||||
reason: 'Unable to confirm your email address',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to confirm email address',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
|
||||
spyOn(Kolide.users, 'confirmEmailChange')
|
||||
.andReturn(Promise.reject(errorResponse));
|
||||
});
|
||||
|
||||
it('pushes the user to the login page', (done) => {
|
||||
const mockStore = reduxMockStore(authStore);
|
||||
const { dispatch } = mockStore;
|
||||
|
||||
confirmEmailChange(dispatch, userStub, token)
|
||||
.then(done)
|
||||
.catch(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toInclude({
|
||||
type: '@@router/CALL_HISTORY_METHOD',
|
||||
payload: {
|
||||
method: 'push',
|
||||
args: ['/login'],
|
||||
},
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user or token are not present', () => {
|
||||
it('does not dispatch any actions when the user is not present', (done) => {
|
||||
const mockStore = reduxMockStore(authStore);
|
||||
const { dispatch } = mockStore;
|
||||
|
||||
confirmEmailChange(dispatch, undefined, token)
|
||||
.then(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toEqual([]);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('does not dispatch any actions when the token is not present', (done) => {
|
||||
const mockStore = reduxMockStore(authStore);
|
||||
const { dispatch } = mockStore;
|
||||
|
||||
confirmEmailChange(dispatch, userStub, undefined)
|
||||
.then(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toEqual([]);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
1
frontend/components/EmailTokenRedirect/index.js
Normal file
1
frontend/components/EmailTokenRedirect/index.js
Normal file
@ -0,0 +1 @@
|
||||
export default from './EmailTokenRedirect';
|
@ -0,0 +1,53 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import Form from 'components/forms/Form';
|
||||
import formFieldInterface from 'interfaces/form_field';
|
||||
import InputField from 'components/forms/fields/InputField';
|
||||
|
||||
const baseClass = 'change-email-form';
|
||||
|
||||
class ChangeEmailForm extends Component {
|
||||
static propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
password: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { fields, handleSubmit, onCancel } = this.props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
{...fields.password}
|
||||
autofocus
|
||||
label="Password"
|
||||
type="password"
|
||||
/>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
|
||||
Submit
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse" className={`${baseClass}__btn`}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(ChangeEmailForm, {
|
||||
fields: ['password'],
|
||||
validate: (formData) => {
|
||||
if (!formData.password) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: { password: 'Password must be present' },
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, errors: {} };
|
||||
},
|
||||
});
|
15
frontend/components/forms/ChangeEmailForm/_styles.scss
Normal file
15
frontend/components/forms/ChangeEmailForm/_styles.scss
Normal file
@ -0,0 +1,15 @@
|
||||
.change-email-form {
|
||||
&__btn-wrap {
|
||||
@include display(flex);
|
||||
@include flex-direction(row-reverse);
|
||||
}
|
||||
|
||||
&__btn {
|
||||
font-size: $small;
|
||||
height: 38px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 15px;
|
||||
padding: 0;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
1
frontend/components/forms/ChangeEmailForm/index.js
Normal file
1
frontend/components/forms/ChangeEmailForm/index.js
Normal file
@ -0,0 +1 @@
|
||||
export default from './ChangeEmailForm';
|
@ -18,11 +18,27 @@ class UserSettingsForm extends Component {
|
||||
username: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
pendingEmail: PropTypes.string,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
renderEmailHint = () => {
|
||||
const { pendingEmail } = this.props;
|
||||
|
||||
if (!pendingEmail) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<i className={`${baseClass}__email-hint`}>
|
||||
Pending change to <b>{pendingEmail}</b>
|
||||
</i>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { fields, handleSubmit, onCancel } = this.props;
|
||||
const { renderEmailHint } = this;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={baseClass}>
|
||||
@ -34,6 +50,7 @@ class UserSettingsForm extends Component {
|
||||
<InputField
|
||||
{...fields.email}
|
||||
label="Email (required)"
|
||||
hint={renderEmailHint()}
|
||||
/>
|
||||
<InputField
|
||||
{...fields.name}
|
||||
|
@ -3,6 +3,10 @@
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
&__email-hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
width: 75%;
|
||||
border-top: 1px solid $accent-medium;
|
||||
|
@ -6,6 +6,8 @@
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-top: 10px;
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ export default {
|
||||
CHANGE_PASSWORD: '/v1/kolide/change_password',
|
||||
CONFIG: '/v1/kolide/config',
|
||||
CONFIG_OPTIONS: '/v1/kolide/options',
|
||||
CONFIRM_EMAIL_CHANGE: (token) => {
|
||||
return `/v1/kolide/email/change/${token}`;
|
||||
},
|
||||
ENABLE_USER: (id) => {
|
||||
return `/v1/kolide/users/${id}/enable`;
|
||||
},
|
||||
|
@ -204,6 +204,15 @@ class Kolide extends Base {
|
||||
|
||||
return this.authenticatedPost(this.endpoint(CHANGE_PASSWORD), JSON.stringify(passwordParams));
|
||||
},
|
||||
confirmEmailChange: (user, token) => {
|
||||
const { CONFIRM_EMAIL_CHANGE } = endpoints;
|
||||
const endpoint = this.endpoint(CONFIRM_EMAIL_CHANGE(token));
|
||||
|
||||
return this.authenticatedGet(endpoint)
|
||||
.then((response) => {
|
||||
return { ...user, email: response.new_email };
|
||||
});
|
||||
},
|
||||
enable: (user, { enabled }) => {
|
||||
const { ENABLE_USER } = endpoints;
|
||||
|
||||
|
@ -19,6 +19,7 @@ const {
|
||||
invalidForgotPasswordRequest,
|
||||
invalidResetPasswordRequest,
|
||||
validChangePasswordRequest,
|
||||
validConfirmEmailChangeRequest,
|
||||
validCreateLabelRequest,
|
||||
validCreateLicenseRequest,
|
||||
validCreatePackRequest,
|
||||
@ -544,6 +545,23 @@ describe('Kolide - API client', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#confirmEmailChange', () => {
|
||||
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||
const token = 'KFBR392';
|
||||
const request = validConfirmEmailChangeRequest(bearerToken, token);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.users.confirmEmailChange(userStub, token)
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('Expected request to have been stubbed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#enable', () => {
|
||||
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||
const enableParams = { enabled: true };
|
||||
|
@ -5,6 +5,7 @@ import moment from 'moment';
|
||||
|
||||
import Avatar from 'components/Avatar';
|
||||
import Button from 'components/buttons/Button';
|
||||
import ChangeEmailForm from 'components/forms/ChangeEmailForm';
|
||||
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
|
||||
import deepDifference from 'utilities/deep_difference';
|
||||
import Icon from 'components/icons/Icon';
|
||||
@ -35,7 +36,12 @@ export class UserSettingsPage extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = { showModal: false };
|
||||
this.state = {
|
||||
pendingEmail: undefined,
|
||||
showEmailModal: false,
|
||||
showPasswordModal: false,
|
||||
updatedUser: {},
|
||||
};
|
||||
}
|
||||
|
||||
onCancel = (evt) => {
|
||||
@ -61,17 +67,28 @@ export class UserSettingsPage extends Component {
|
||||
onShowModal = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({ showModal: true });
|
||||
this.setState({ showPasswordModal: true });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onToggleModal = (evt) => {
|
||||
onToggleEmailModal = (updatedUser = {}) => {
|
||||
const { showEmailModal } = this.state;
|
||||
|
||||
this.setState({
|
||||
showEmailModal: !showEmailModal,
|
||||
updatedUser,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onTogglePasswordModal = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { showModal } = this.state;
|
||||
const { showPasswordModal } = this.state;
|
||||
|
||||
this.setState({ showModal: !showModal });
|
||||
this.setState({ showPasswordModal: !showPasswordModal });
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -80,9 +97,19 @@ export class UserSettingsPage extends Component {
|
||||
const { dispatch, user } = this.props;
|
||||
const updatedUser = deepDifference(formData, user);
|
||||
|
||||
if (updatedUser.email && !updatedUser.password) {
|
||||
return this.onToggleEmailModal(updatedUser);
|
||||
}
|
||||
|
||||
return dispatch(updateUser(user, updatedUser))
|
||||
.then(() => {
|
||||
return dispatch(renderFlash('success', 'Account updated!'));
|
||||
if (updatedUser.email) {
|
||||
this.setState({ pendingEmail: updatedUser.email });
|
||||
}
|
||||
|
||||
dispatch(renderFlash('success', 'Account updated!'));
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(() => false);
|
||||
}
|
||||
@ -93,29 +120,60 @@ export class UserSettingsPage extends Component {
|
||||
return dispatch(userActions.changePassword(user, formData))
|
||||
.then(() => {
|
||||
dispatch(renderFlash('success', 'Password changed successfully'));
|
||||
this.setState({ showModal: false });
|
||||
this.setState({ showPasswordModal: false });
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
renderModal = () => {
|
||||
const { userErrors } = this.props;
|
||||
const { showModal } = this.state;
|
||||
const { handleSubmitPasswordForm, onToggleModal } = this;
|
||||
renderEmailModal = () => {
|
||||
const { errors } = this.props;
|
||||
const { updatedUser, showEmailModal } = this.state;
|
||||
const { handleSubmit, onToggleEmailModal } = this;
|
||||
|
||||
if (!showModal) {
|
||||
const emailSubmit = (formData) => {
|
||||
handleSubmit(formData)
|
||||
.then((r) => {
|
||||
return r ? onToggleEmailModal() : false;
|
||||
});
|
||||
};
|
||||
|
||||
if (!showEmailModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="To change your email you must supply your password"
|
||||
onExit={onToggleEmailModal}
|
||||
>
|
||||
<ChangeEmailForm
|
||||
formData={updatedUser}
|
||||
handleSubmit={emailSubmit}
|
||||
onCancel={onToggleEmailModal}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderPasswordModal = () => {
|
||||
const { userErrors } = this.props;
|
||||
const { showPasswordModal } = this.state;
|
||||
const { handleSubmitPasswordForm, onTogglePasswordModal } = this;
|
||||
|
||||
if (!showPasswordModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Change Password"
|
||||
onExit={onToggleModal}
|
||||
onExit={onTogglePasswordModal}
|
||||
>
|
||||
<ChangePasswordForm
|
||||
handleSubmit={handleSubmitPasswordForm}
|
||||
onCancel={onToggleModal}
|
||||
onCancel={onTogglePasswordModal}
|
||||
serverErrors={userErrors}
|
||||
/>
|
||||
</Modal>
|
||||
@ -123,8 +181,16 @@ export class UserSettingsPage extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { handleSubmit, onCancel, onLogout, onShowModal, renderModal } = this;
|
||||
const {
|
||||
handleSubmit,
|
||||
onCancel,
|
||||
onLogout,
|
||||
onShowModal,
|
||||
renderEmailModal,
|
||||
renderPasswordModal,
|
||||
} = this;
|
||||
const { errors, user } = this.props;
|
||||
const { pendingEmail } = this.state;
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
@ -142,6 +208,7 @@ export class UserSettingsPage extends Component {
|
||||
formData={user}
|
||||
handleSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
pendingEmail={pendingEmail}
|
||||
serverErrors={errors}
|
||||
/>
|
||||
</div>
|
||||
@ -169,7 +236,8 @@ export class UserSettingsPage extends Component {
|
||||
LOGOUT
|
||||
</Button>
|
||||
</div>
|
||||
{renderModal()}
|
||||
{renderEmailModal()}
|
||||
{renderPasswordModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,18 +8,22 @@ import testHelpers from 'test/helpers';
|
||||
import { userStub } from 'test/stubs';
|
||||
import * as authActions from 'redux/nodes/auth/actions';
|
||||
|
||||
const { connectedComponent, reduxMockStore } = testHelpers;
|
||||
const {
|
||||
connectedComponent,
|
||||
fillInFormInput,
|
||||
reduxMockStore,
|
||||
} = testHelpers;
|
||||
|
||||
describe('UserSettingsPage - component', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('renders a UserSettingsForm component', () => {
|
||||
const store = { auth: { user: userStub }, entities: { users: {} } };
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
const page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
it('renders a UserSettingsForm component', () => {
|
||||
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
|
||||
expect(page.find('UserSettingsForm').length).toEqual(1);
|
||||
expect(Page.find('UserSettingsForm').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders a UserSettingsForm component', () => {
|
||||
@ -46,4 +50,45 @@ describe('UserSettingsPage - component', () => {
|
||||
|
||||
expect(authActions.updateUser).toHaveBeenCalledWith(userStub, updatedAttrs);
|
||||
});
|
||||
|
||||
describe('changing email address', () => {
|
||||
it('renders the ChangeEmailForm when the user changes their email', () => {
|
||||
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
const UserSettingsForm = Page.find('UserSettingsForm');
|
||||
const emailInput = UserSettingsForm.find({ name: 'email' });
|
||||
|
||||
expect(Page.find('ChangeEmailForm').length).toEqual(0, 'Expected the ChangeEmailForm to not render');
|
||||
|
||||
fillInFormInput(emailInput, 'new@email.org');
|
||||
UserSettingsForm.simulate('submit');
|
||||
|
||||
expect(Page.find('ChangeEmailForm').length).toEqual(1, 'Expected the ChangeEmailForm to render');
|
||||
});
|
||||
|
||||
it('does not render the ChangeEmailForm when the user does not change their email', () => {
|
||||
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||
const UserSettingsForm = Page.find('UserSettingsForm');
|
||||
const emailInput = UserSettingsForm.find({ name: 'email' });
|
||||
|
||||
expect(Page.find('ChangeEmailForm').length).toEqual(0, 'Expected the ChangeEmailForm to not render');
|
||||
|
||||
fillInFormInput(emailInput, userStub.email);
|
||||
UserSettingsForm.simulate('submit');
|
||||
|
||||
expect(Page.find('ChangeEmailForm').length).toEqual(0, 'Expected the ChangeEmailForm to not render');
|
||||
});
|
||||
|
||||
it('displays pending email text when the user is pending an email change', () => {
|
||||
const props = { dispatch: noop, user: userStub };
|
||||
const Page = mount(<UserSettingsPage {...props} />);
|
||||
const UserSettingsForm = () => Page.find('UserSettingsForm');
|
||||
const emailHint = () => UserSettingsForm().find('.manage-user__email-hint');
|
||||
|
||||
expect(emailHint().length).toEqual(0, 'Expected the form to not render an email hint');
|
||||
|
||||
Page.setState({ pendingEmail: 'new@email.org' });
|
||||
|
||||
expect(emailHint().length).toEqual(1, 'Expected the form to render an email hint');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -292,7 +292,7 @@ const reduxConfig = ({
|
||||
|
||||
dispatch(updateFailure(errorsObject));
|
||||
|
||||
throw errorsObject;
|
||||
throw response;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import Kolide from 'kolide';
|
||||
|
||||
import config from 'redux/nodes/entities/users/config';
|
||||
import { formatErrorResponse } from 'redux/nodes/entities/base/helpers';
|
||||
import { logoutUser, updateUserSuccess } from 'redux/nodes/auth/actions';
|
||||
|
||||
const { extendedActions } = config;
|
||||
|
||||
@ -43,6 +44,29 @@ export const changePassword = (user, { new_password: newPassword, old_password:
|
||||
};
|
||||
};
|
||||
|
||||
export const confirmEmailChange = (user, token) => {
|
||||
const { loadRequest, successAction, updateFailure, updateSuccess } = extendedActions;
|
||||
|
||||
return (dispatch) => {
|
||||
dispatch(loadRequest);
|
||||
|
||||
return Kolide.users.confirmEmailChange(user, token)
|
||||
.then((updatedUser) => {
|
||||
dispatch(successAction(updatedUser, updateSuccess));
|
||||
dispatch(updateUserSuccess(updatedUser));
|
||||
|
||||
return updatedUser;
|
||||
})
|
||||
.catch((response) => {
|
||||
const errorsObject = formatErrorResponse(response);
|
||||
|
||||
dispatch(updateFailure(errorsObject));
|
||||
|
||||
return dispatch(logoutUser());
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const enableUser = (user, { enabled }) => {
|
||||
const { successAction, updateFailure, updateSuccess } = extendedActions;
|
||||
|
||||
@ -96,4 +120,11 @@ export const updateAdmin = (user, { admin }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default { ...config.actions, changePassword, enableUser, requirePasswordReset, updateAdmin };
|
||||
export default {
|
||||
...config.actions,
|
||||
changePassword,
|
||||
confirmEmailChange,
|
||||
enableUser,
|
||||
requirePasswordReset,
|
||||
updateAdmin,
|
||||
};
|
||||
|
@ -3,9 +3,11 @@ import expect, { restoreSpies, spyOn } from 'expect';
|
||||
import * as Kolide from 'kolide';
|
||||
|
||||
import { reduxMockStore } from 'test/helpers';
|
||||
import { updateUserSuccess } from 'redux/nodes/auth/actions';
|
||||
|
||||
import {
|
||||
changePassword,
|
||||
confirmEmailChange,
|
||||
enableUser,
|
||||
requirePasswordReset,
|
||||
REQUIRE_PASSWORD_RESET_FAILURE,
|
||||
@ -211,6 +213,117 @@ describe('Users - actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirmEmailChange', () => {
|
||||
const token = 'KFBR392';
|
||||
const updatedUser = { ...user, email: 'new@email.com' };
|
||||
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.users, 'confirmEmailChange').andCall(() => {
|
||||
return Promise.resolve(updatedUser);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('calls the API', (done) => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(confirmEmailChange(user, token))
|
||||
.then(() => {
|
||||
expect(Kolide.default.users.confirmEmailChange).toHaveBeenCalledWith(user, token);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', (done) => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(confirmEmailChange(user, token))
|
||||
.then(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
config.extendedActions.loadRequest,
|
||||
config.extendedActions.updateSuccess({
|
||||
users: {
|
||||
[user.id]: updatedUser,
|
||||
},
|
||||
}),
|
||||
updateUserSuccess(updatedUser),
|
||||
]);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessful request', () => {
|
||||
const errors = [
|
||||
{
|
||||
name: 'base',
|
||||
reason: 'Unable to confirm your email address',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to confirm email address',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.users, 'confirmEmailChange').andCall(() => {
|
||||
return Promise.reject(errorResponse);
|
||||
});
|
||||
spyOn(Kolide.default, 'logout').andCall(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('calls the API', (done) => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(confirmEmailChange(user, token))
|
||||
.then(() => {
|
||||
expect(Kolide.default.users.confirmEmailChange).toHaveBeenCalledWith(user, token);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', (done) => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(confirmEmailChange(user, token))
|
||||
.then(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
config.extendedActions.loadRequest,
|
||||
config.extendedActions.updateFailure({ base: 'Unable to confirm your email address', http_status: 422 }),
|
||||
{ type: 'LOGOUT_REQUEST' },
|
||||
{
|
||||
type: '@@router/CALL_HISTORY_METHOD',
|
||||
payload: {
|
||||
method: 'push',
|
||||
args: ['/login'],
|
||||
},
|
||||
},
|
||||
{ type: 'LOGOUT_SUCCESS' },
|
||||
]);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAdmin', () => {
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -13,6 +13,7 @@ import ConfigOptionsPage from 'pages/config/ConfigOptionsPage';
|
||||
import ConfirmInvitePage from 'pages/ConfirmInvitePage';
|
||||
import CoreLayout from 'layouts/CoreLayout';
|
||||
import EditPackPage from 'pages/packs/EditPackPage';
|
||||
import EmailTokenRedirect from 'components/EmailTokenRedirect';
|
||||
import LicensePage from 'pages/LicensePage';
|
||||
import LoginRoutes from 'components/LoginRoutes';
|
||||
import LogoutPage from 'pages/LogoutPage';
|
||||
@ -42,6 +43,7 @@ const routes = (
|
||||
<Route path="reset" />
|
||||
</Route>
|
||||
<Route component={AuthenticatedRoutes}>
|
||||
<Route path="email/change/:token" component={EmailTokenRedirect} />
|
||||
<Route path="logout" component={LogoutPage} />
|
||||
<Route component={CoreLayout}>
|
||||
<IndexRedirect to="/hosts/manage" />
|
||||
|
@ -16,4 +16,5 @@ export default {
|
||||
NEW_QUERY: '/queries/new',
|
||||
RESET_PASSWORD: '/login/reset',
|
||||
SETUP: '/setup',
|
||||
USER_SETTINGS: '/settings',
|
||||
};
|
||||
|
@ -24,6 +24,16 @@ export const validChangePasswordRequest = (bearerToken, params) => {
|
||||
.reply(200, {});
|
||||
};
|
||||
|
||||
export const validConfirmEmailChangeRequest = (bearerToken, token) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
})
|
||||
.get(`/api/v1/kolide/email/change/${token}`)
|
||||
.reply(200, { new_email: 'new@email.com' });
|
||||
};
|
||||
|
||||
export const validCreateLabelRequest = (bearerToken, labelParams) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
@ -518,6 +528,7 @@ export default {
|
||||
invalidGetQueryRequest,
|
||||
invalidResetPasswordRequest,
|
||||
validChangePasswordRequest,
|
||||
validConfirmEmailChangeRequest,
|
||||
validCreateLabelRequest,
|
||||
validCreateLicenseRequest,
|
||||
validCreatePackRequest,
|
||||
|
Loading…
Reference in New Issue
Block a user