Skip to content
This repository has been archived by the owner on Jan 2, 2020. It is now read-only.

Commit

Permalink
[#935] Shows snackbar error response on new password submit
Browse files Browse the repository at this point in the history
  • Loading branch information
Sriram Viswanathan committed Apr 19, 2017
1 parent ac7df6f commit 559d816
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 32 deletions.
1 change: 1 addition & 0 deletions web-ui/app/locales/en_US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"general": "Problems talking to server",
"parse": "Got invalid response from server",
"auth": "Invalid email or password",
"recovery-auth": "Invalid email or recovery code",
"login": {
"title": "Oh, something went wrong :(",
"message": "Try to login again in a few minutes. If the problem persists, contact your account administrator."
Expand Down
1 change: 1 addition & 0 deletions web-ui/app/locales/pt_BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"general": "Problemas ao se comunicar com o servidor",
"parse": "Obteve uma resposta inválida do servidor",
"auth": "E-mail ou senha inválidos",
"recovery-auth": "E-mail ou código de recuperação inválidos",
"login": {
"title": "Ops, algo deu errado :(",
"message": "Tente entrar novamente em alguns minutos. Se o problema persistir, contate o administrador da sua conta."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ export class NewPasswordForm extends React.Component {
userCode: this.props.userCode,
password: this.state.password,
confirmPassword: this.state.confirmPassword
}).then(() => this.props.next());
}
}).then((response) => {
if (response.ok) this.props.next();
else if (response.status === 401) this.props.onError('error.recovery-auth');
else this.props.onError('error.general');
});
};

handleChangePassword = (event) => {
this.setState({ password: event.target.value });
Expand Down Expand Up @@ -107,7 +111,8 @@ NewPasswordForm.propTypes = {
next: React.PropTypes.func.isRequired,
previous: React.PropTypes.func.isRequired,
username: React.PropTypes.string.isRequired,
userCode: React.PropTypes.string.isRequired
userCode: React.PropTypes.string.isRequired,
onError: React.PropTypes.func.isRequired
};

export default translate('', { wait: true })(NewPasswordForm);
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@ describe('NewPasswordForm', () => {
let newPasswordForm;
let mockPrevious;
let mockNext;
let mockOnError;
let mockTranslations;

beforeEach(() => {
mockTranslations = key => key;
mockPrevious = expect.createSpy();
mockNext = expect.createSpy();
mockOnError = expect.createSpy();
newPasswordForm = shallow(
<NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' username='alice' />
<NewPasswordForm
t={mockTranslations}
previous={mockPrevious}
next={mockNext}
onError={mockOnError}
userCode='def234'
username='alice'
/>
);
});

Expand Down Expand Up @@ -42,35 +52,88 @@ describe('NewPasswordForm', () => {
});

describe('Submit', () => {
beforeEach((done) => {
mockNext = expect.createSpy().andCall(() => done());
newPasswordForm = shallow(
<NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' next={mockNext} username='alice' />
);
fetchMock.post('/account-recovery', 200);
const submitForm = () => {
newPasswordForm.find('InputField[name="new-password"]').simulate('change', { target: { value: '123' } });
newPasswordForm.find('InputField[name="confirm-password"]').simulate('change', { target: { value: '456' } });
newPasswordForm.find('form').simulate('submit', { preventDefault: expect.createSpy() });
});
};

it('posts to account recovery', () => {
expect(fetchMock.called('/account-recovery')).toBe(true, 'POST was not called');
});
const createNewPasswordForm = () => {
newPasswordForm = shallow(
<NewPasswordForm
t={mockTranslations}
previous={mockPrevious}
next={mockNext}
onError={mockOnError}
userCode='def234'
username='alice'
/>
);
};

context('on success', () => {
beforeEach((done) => {
mockNext = expect.createSpy().andCall(() => done());
createNewPasswordForm();
fetchMock.post('/account-recovery', 200);
submitForm();
});

it('sends username as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"username":"alice"');
});
it('posts to account recovery', () => {
expect(fetchMock.called('/account-recovery')).toBe(true, 'POST was not called');
});

it('sends username as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"username":"alice"');
});

it('sends user code as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"userCode":"def234"');
});

it('sends password as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"password":"123"');
});

it('sends user code as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"userCode":"def234"');
it('sends confirm password as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"confirmPassword":"456"');
});

it('calls next handler on success', () => {
expect(mockNext).toHaveBeenCalled();
});

afterEach(fetchMock.restore);
});

it('sends password as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"password":"123"');
context('on unauthorized error', () => {
beforeEach((done) => {
mockOnError.andCall(() => done());
createNewPasswordForm();
fetchMock.post('/account-recovery', 401);
submitForm();
});

it('shows error message on 401', () => {
expect(mockOnError).toHaveBeenCalledWith('error.recovery-auth');
});

afterEach(fetchMock.restore);
});

it('sends confirm password as content', () => {
expect(fetchMock.lastOptions('/account-recovery').body).toContain('"confirmPassword":"456"');
context('on server error', () => {
beforeEach((done) => {
mockOnError.andCall(() => done());
createNewPasswordForm();
fetchMock.post('/account-recovery', 500);
submitForm();
});

it('shows error message on 500', () => {
expect(mockOnError).toHaveBeenCalledWith('error.general');
});

afterEach(fetchMock.restore);
});
});

Expand Down Expand Up @@ -157,9 +220,5 @@ describe('NewPasswordForm', () => {
expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
});
});

it('calls next handler on success', () => {
expect(mockNext).toHaveBeenCalled();
});
});
});
24 changes: 19 additions & 5 deletions web-ui/src/account_recovery/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import NewPasswordForm from 'src/account_recovery/new_password_form/new_password
import BackupAccountStep from 'src/account_recovery/backup_account_step/backup_account_step';
import Footer from 'src/common/footer/footer';
import Util from 'src/common/util';
import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';

import 'font-awesome/scss/font-awesome.scss';
import './page.scss';
Expand All @@ -34,7 +35,7 @@ export class Page extends React.Component {

constructor(props) {
super(props);
this.state = { step: 0, userCode: '', username: this.setUsername() };
this.state = { step: 0, userCode: '', username: this.setUsername(), errorMessage: '' };
}

setUsername = () => (Util.getQueryParameter('username') || '');
Expand All @@ -44,15 +45,19 @@ export class Page extends React.Component {
event.preventDefault();
}
this.setState({ step: this.state.step + 1 });
}
};

previousStep = () => {
this.setState({ step: this.state.step - 1 });
}
};

saveUserCode = (event) => {
this.setState({ userCode: event.target.value });
}
};

errorHandler = (errorMessage) => {
this.setState({ errorMessage });
};

steps = () => ({
0: <AdminRecoveryCodeForm next={this.nextStep} />,
Expand All @@ -68,12 +73,20 @@ export class Page extends React.Component {
userCode={this.state.userCode}
next={this.nextStep}
username={this.state.username}
onError={this.errorHandler}
/>),
3: <BackupAccountStep />
})
});

mainContent = () => this.steps()[this.state.step];

showSnackbarOnError = (t) => {
if (this.state.errorMessage) {
return <SnackbarNotification message={t(this.state.errorMessage)} isError />;
}
return undefined; // To satisfy eslint error - consistent-return
};

render() {
const t = this.props.t;
return (
Expand All @@ -85,6 +98,7 @@ export class Page extends React.Component {
{this.mainContent()}
</div>
</section>
{this.showSnackbarOnError(t)}
<Footer />
</div>
</DocumentTitle>
Expand Down
32 changes: 32 additions & 0 deletions web-ui/src/account_recovery/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import { Page } from 'src/account_recovery/page';
import Header from 'src/common/header/header';
import Footer from 'src/common/footer/footer';
import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';

import AdminRecoveryCodeFormWrapper from './admin_recovery_code_form/admin_recovery_code_form';
import UserRecoveryCodeFormWrapper from './user_recovery_code_form/user_recovery_code_form';
Expand Down Expand Up @@ -85,6 +86,13 @@ describe('Account Recovery Page', () => {
expect(page.find(NewPasswordFormWrapper).length).toEqual(1);
});

it('renders new password form with error handler', () => {
pageInstance.nextStep();
pageInstance.nextStep();

expect(page.find(NewPasswordFormWrapper).props().onError).toBeA(Function);
});

it('returns to user code form on new password form back link', () => {
pageInstance.nextStep();
pageInstance.nextStep();
Expand All @@ -101,4 +109,28 @@ describe('Account Recovery Page', () => {
expect(page.find(BackupAccountStepWrapper).length).toEqual(1);
});
});

context('on error', () => {
beforeEach(() => {
pageInstance.errorHandler('error message');
});

it('returns snackbar component on error', () => {
const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t);
expect(snackbar).toEqual(<SnackbarNotification message='error message' isError />);
});

it('returns nothing when there is no error', () => {
pageInstance.errorHandler('');
const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t);
expect(snackbar).toEqual(undefined);
});

it('renders snackbar notification on error', () => {
const snackbar = page.find(SnackbarNotification);
expect(snackbar).toExist();
expect(snackbar.props().message).toEqual('error message');
expect(snackbar.props().isError).toEqual(true);
});
});
});

0 comments on commit 559d816

Please sign in to comment.