diff options
12 files changed, 148 insertions, 57 deletions
diff --git a/service/pixelated/resources/account_recovery_resource.py b/service/pixelated/resources/account_recovery_resource.py index 8cb10fc8..39ebb8d0 100644 --- a/service/pixelated/resources/account_recovery_resource.py +++ b/service/pixelated/resources/account_recovery_resource.py @@ -21,6 +21,8 @@ from twisted.python.filepath import FilePath from pixelated.resources import get_public_static_folder from twisted.web.http import OK from twisted.web.template import Element, XMLFile, renderElement +from twisted.web.server import NOT_DONE_YET +from twisted.internet import defer class AccountRecoveryPage(Element): @@ -44,3 +46,16 @@ class AccountRecoveryResource(BaseResource): def _render_template(self, request): site = AccountRecoveryPage() return renderElement(request, site) + + def render_POST(self, request): + def success_response(response): + request.setResponseCode(OK) + request.finish() + + def error_response(response): + request.setResponseCode(INTERNAL_SERVER_ERROR) + request.finish() + + d = defer.succeed('Done!') + d.addCallbacks(success_response, error_response) + return NOT_DONE_YET diff --git a/service/test/unit/resources/test_account_recovery_resource.py b/service/test/unit/resources/test_account_recovery_resource.py index d4df7716..cd9acae7 100644 --- a/service/test/unit/resources/test_account_recovery_resource.py +++ b/service/test/unit/resources/test_account_recovery_resource.py @@ -42,3 +42,14 @@ class TestAccountRecoveryResource(unittest.TestCase): d.addCallback(assert_200_when_user_logged_in) return d + + def test_post_returns_successfully(self): + request = DummyRequest(['/account-recovery']) + request.method = 'POST' + d = self.web.get(request) + + def assert_successful_response(_): + self.assertEqual(200, request.responseCode) + + d.addCallback(assert_successful_response) + return d diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.js b/web-ui/src/account_recovery/new_password_form/new_password_form.js index f1097b0b..b18bc8ff 100644 --- a/web-ui/src/account_recovery/new_password_form/new_password_form.js +++ b/web-ui/src/account_recovery/new_password_form/new_password_form.js @@ -15,17 +15,23 @@ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. */ +import 'isomorphic-fetch'; import React from 'react'; import { translate } from 'react-i18next'; +import { submitForm } from 'src/common/util'; import InputField from 'src/common/input_field/input_field'; import SubmitButton from 'src/common/submit_button/submit_button'; import BackLink from 'src/common/back_link/back_link'; import './new_password_form.scss'; -export const NewPasswordForm = ({ t, previous }) => ( - <form className='account-recovery-form new-password'> +const submitHandler = code => event => ( + submitForm(event, '/account-recovery', { userCode: code }) +); + +export const NewPasswordForm = ({ t, previous, userCode }) => ( + <form className='account-recovery-form new-password' onSubmit={submitHandler(userCode)}> <img className='account-recovery-progress' src='/public/images/account-recovery/step_3.svg' @@ -47,7 +53,8 @@ export const NewPasswordForm = ({ t, previous }) => ( NewPasswordForm.propTypes = { t: React.PropTypes.func.isRequired, - previous: React.PropTypes.func.isRequired + previous: React.PropTypes.func.isRequired, + userCode: React.PropTypes.string.isRequired }; export default translate('', { wait: true })(NewPasswordForm); diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js index d2bd350c..5ffd9720 100644 --- a/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js +++ b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js @@ -1,6 +1,7 @@ import { shallow } from 'enzyme'; import expect from 'expect'; import React from 'react'; +import fetchMock from 'fetch-mock'; import { NewPasswordForm } from './new_password_form'; describe('NewPasswordForm', () => { @@ -11,7 +12,7 @@ describe('NewPasswordForm', () => { const mockTranslations = key => key; mockPrevious = expect.createSpy(); newPasswordForm = shallow( - <NewPasswordForm t={mockTranslations} previous={mockPrevious} /> + <NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' /> ); }); @@ -37,4 +38,19 @@ describe('NewPasswordForm', () => { newPasswordForm.find('BackLink').simulate('click'); expect(mockPrevious).toHaveBeenCalled(); }); + + describe('Submit', () => { + beforeEach(() => { + fetchMock.post('/account-recovery', 200); + newPasswordForm.find('form').simulate('submit', { preventDefault: expect.createSpy() }); + }); + + it('posts to account recovery', () => { + expect(fetchMock.called('/account-recovery')).toBe(true, 'POST was not called'); + }); + + it('sends user code as content', () => { + expect(fetchMock.lastOptions('/account-recovery').body).toContain('"userCode":"def234"'); + }); + }); }); diff --git a/web-ui/src/account_recovery/page.js b/web-ui/src/account_recovery/page.js index 579f17cc..2d33e2fb 100644 --- a/web-ui/src/account_recovery/page.js +++ b/web-ui/src/account_recovery/page.js @@ -32,7 +32,7 @@ export class Page extends React.Component { constructor(props) { super(props); - this.state = { step: 0 }; + this.state = { step: 0, userCode: '' }; } nextStep = (event) => { @@ -44,13 +44,19 @@ export class Page extends React.Component { this.setState({ step: this.state.step - 1 }); } - steps = { - 0: <AdminRecoveryCodeForm next={this.nextStep} />, - 1: <UserRecoveryCodeForm previous={this.previousStep} next={this.nextStep} />, - 2: <NewPasswordForm previous={this.previousStep} /> + saveUserCode = (event) => { + this.setState({ userCode: event.target.value }); } - mainContent = () => this.steps[this.state.step]; + steps = () => ({ + 0: <AdminRecoveryCodeForm next={this.nextStep} />, + 1: (<UserRecoveryCodeForm + previous={this.previousStep} next={this.nextStep} saveCode={this.saveUserCode} + />), + 2: <NewPasswordForm previous={this.previousStep} userCode={this.state.userCode} /> + }) + + mainContent = () => this.steps()[this.state.step]; render() { const t = this.props.t; diff --git a/web-ui/src/account_recovery/page.spec.js b/web-ui/src/account_recovery/page.spec.js index 68debba0..31a748be 100644 --- a/web-ui/src/account_recovery/page.spec.js +++ b/web-ui/src/account_recovery/page.spec.js @@ -12,10 +12,12 @@ import NewPasswordFormWrapper from './new_password_form/new_password_form'; describe('Account Recovery Page', () => { let page; + let pageInstance; beforeEach(() => { const mockTranslations = key => key; page = shallow(<Page t={mockTranslations} />); + pageInstance = page.instance(); }); it('renders account recovery page title', () => { @@ -30,13 +32,12 @@ describe('Account Recovery Page', () => { expect(page.find(Footer).length).toEqual(1); }); - context('main content', () => { - let pageInstance; - - beforeEach(() => { - pageInstance = page.instance(); - }); + it('saves user code', () => { + pageInstance.saveUserCode({ target: { value: '123' } }); + expect(pageInstance.state.userCode).toEqual('123'); + }); + context('main content', () => { it('renders admin recovery code form as default form', () => { expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1); expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(0); diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js index a4119885..c39c894d 100644 --- a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js +++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js @@ -24,7 +24,7 @@ import BackLink from 'src/common/back_link/back_link'; import './user_recovery_code_form.scss'; -export const UserRecoveryCodeForm = ({ t, previous, next }) => ( +export const UserRecoveryCodeForm = ({ t, previous, next, saveCode }) => ( <form className='account-recovery-form user-code' onSubmit={next}> <img className='account-recovery-progress' @@ -40,7 +40,10 @@ export const UserRecoveryCodeForm = ({ t, previous, next }) => ( /> <p>{t('account-recovery.user-form.description')}</p> </div> - <InputField name='admin-code' label={t('account-recovery.user-form.input-label')} /> + <InputField + name='user-code' label={t('account-recovery.user-form.input-label')} + onChange={saveCode} + /> <SubmitButton buttonText={t('account-recovery.button-next')} /> <BackLink text={t('account-recovery.back')} onClick={previous} /> </form> @@ -49,7 +52,8 @@ export const UserRecoveryCodeForm = ({ t, previous, next }) => ( UserRecoveryCodeForm.propTypes = { t: React.PropTypes.func.isRequired, previous: React.PropTypes.func.isRequired, - next: React.PropTypes.func.isRequired + next: React.PropTypes.func.isRequired, + saveCode: React.PropTypes.func.isRequired }; export default translate('', { wait: true })(UserRecoveryCodeForm); diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js index e47f2e6c..386c3a19 100644 --- a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js +++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js @@ -7,14 +7,17 @@ describe('UserRecoveryCodeForm', () => { let userRecoveryCodeForm; let mockNext; let mockPrevious; + let mockSaveCode; beforeEach(() => { const mockTranslations = key => key; mockNext = expect.createSpy(); mockPrevious = expect.createSpy(); + mockSaveCode = expect.createSpy(); userRecoveryCodeForm = shallow( <UserRecoveryCodeForm - t={mockTranslations} next={mockNext} previous={mockPrevious} + t={mockTranslations} next={mockNext} + previous={mockPrevious} saveCode={mockSaveCode} /> ); }); @@ -44,4 +47,9 @@ describe('UserRecoveryCodeForm', () => { userRecoveryCodeForm.find('BackLink').simulate('click'); expect(mockPrevious).toHaveBeenCalled(); }); + + it('saves code on input change', () => { + userRecoveryCodeForm.find('InputField').simulate('change', '123'); + expect(mockSaveCode).toHaveBeenCalledWith('123'); + }); }); diff --git a/web-ui/src/backup_account/backup_email/backup_email.js b/web-ui/src/backup_account/backup_email/backup_email.js index 8fa71191..ac64f02e 100644 --- a/web-ui/src/backup_account/backup_email/backup_email.js +++ b/web-ui/src/backup_account/backup_email/backup_email.js @@ -19,8 +19,8 @@ import 'isomorphic-fetch'; import React from 'react'; import { translate } from 'react-i18next'; import validator from 'validator'; -import browser from 'helpers/browser'; +import { submitForm } from 'src/common/util'; import SubmitButton from 'src/common/submit_button/submit_button'; import InputField from 'src/common/input_field/input_field'; import BackLink from 'src/common/back_link/back_link'; @@ -45,24 +45,11 @@ export class BackupEmail extends React.Component { }; submitHandler = (event) => { - event.preventDefault(); - - fetch('/backup-account', { - credentials: 'same-origin', - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - csrftoken: [browser.getCookie('XSRF-TOKEN')], - backupEmail: this.state.backupEmail - }) + submitForm(event, '/backup-account', { + backupEmail: this.state.backupEmail }).then((response) => { - if (response.ok) { - this.props.onSubmit('success'); - } else { - this.props.onSubmit('error'); - } + if (response.ok) this.props.onSubmit('success'); + else this.props.onSubmit('error'); }); }; diff --git a/web-ui/src/backup_account/backup_email/backup_email.spec.js b/web-ui/src/backup_account/backup_email/backup_email.spec.js index b23aadaa..a34afa06 100644 --- a/web-ui/src/backup_account/backup_email/backup_email.spec.js +++ b/web-ui/src/backup_account/backup_email/backup_email.spec.js @@ -3,7 +3,6 @@ import expect from 'expect'; import React from 'react'; import fetchMock from 'fetch-mock'; import { BackupEmail } from 'src/backup_account/backup_email/backup_email'; -import browser from 'helpers/browser'; describe('BackupEmail', () => { let backupEmail; @@ -104,7 +103,6 @@ describe('BackupEmail', () => { context('on success', () => { beforeEach((done) => { mockOnSubmit = expect.createSpy().andCall(() => done()); - expect.spyOn(browser, 'getCookie').andReturn('abc123'); fetchMock.post('/backup-account', 204); backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />); @@ -117,26 +115,10 @@ describe('BackupEmail', () => { expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called'); }); - it('sends csrftoken as content', () => { - expect(fetchMock.lastOptions('/backup-account').body).toContain('"csrftoken":["abc123"]'); - }); - it('sends user email as content', () => { expect(fetchMock.lastOptions('/backup-account').body).toContain('"backupEmail":"test@test.com"'); }); - it('sends content-type header', () => { - expect(fetchMock.lastOptions('/backup-account').headers['Content-Type']).toEqual('application/json'); - }); - - it('sends same origin headers', () => { - expect(fetchMock.lastOptions('/backup-account').credentials).toEqual('same-origin'); - }); - - it('prevents default call to refresh page', () => { - expect(preventDefaultSpy).toHaveBeenCalled(); - }); - it('calls onSubmit from props with success', () => { expect(mockOnSubmit).toHaveBeenCalledWith('success'); }); diff --git a/web-ui/src/common/util.js b/web-ui/src/common/util.js index effb3d9c..c70a8444 100644 --- a/web-ui/src/common/util.js +++ b/web-ui/src/common/util.js @@ -1,8 +1,27 @@ +import browser from 'helpers/browser'; + export const hasQueryParameter = (param) => { const decodedUri = decodeURIComponent(window.location.search.substring(1)); return !(decodedUri.split('&').indexOf(param) < 0); }; +export const submitForm = (event, url, body = {}) => { + event.preventDefault(); + + return fetch(url, { + credentials: 'same-origin', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + csrftoken: [browser.getCookie('XSRF-TOKEN')], + ...body + }) + }); +}; + export default { - hasQueryParameter + hasQueryParameter, + submitForm }; diff --git a/web-ui/src/common/util.spec.js b/web-ui/src/common/util.spec.js index 805d9dd5..a79859a0 100644 --- a/web-ui/src/common/util.spec.js +++ b/web-ui/src/common/util.spec.js @@ -1,4 +1,7 @@ import expect from 'expect'; +import fetchMock from 'fetch-mock'; + +import browser from 'helpers/browser'; import Util from 'src/common/util'; describe('Utils', () => { @@ -17,4 +20,36 @@ describe('Utils', () => { expect(Util.hasQueryParameter('error')).toBe(false); }); }); + + describe('submitForm', () => { + const event = {}; + + beforeEach(() => { + event.preventDefault = expect.createSpy(); + expect.spyOn(browser, 'getCookie').andReturn('abc123'); + + fetchMock.post('/some-url', 200); + Util.submitForm(event, '/some-url', { userCode: '123' }); + }); + + it('sends csrftoken as content', () => { + expect(fetchMock.lastOptions('/some-url').body).toContain('"csrftoken":["abc123"]'); + }); + + it('sends body as content', () => { + expect(fetchMock.lastOptions('/some-url').body).toContain('"userCode":"123"'); + }); + + it('sends content-type header', () => { + expect(fetchMock.lastOptions('/some-url').headers['Content-Type']).toEqual('application/json'); + }); + + it('sends same origin headers', () => { + expect(fetchMock.lastOptions('/some-url').credentials).toEqual('same-origin'); + }); + + it('prevents default call to refresh page', () => { + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); }); |