diff --git a/src/components/LoginForm/LoginForm.react.js b/src/components/LoginForm/LoginForm.react.js index bc897fa745..a008bfaeef 100644 --- a/src/components/LoginForm/LoginForm.react.js +++ b/src/components/LoginForm/LoginForm.react.js @@ -11,17 +11,12 @@ import React from 'react'; import styles from 'components/LoginForm/LoginForm.scss'; import baseStyles from 'stylesheets/base.scss'; -// Class-style component, because we need refs export default class LoginForm extends React.Component { - constructor() { - super(); - this.formRef = React.createRef(); - } render() { return (
-
+
{this.props.header}
{this.props.children} @@ -37,8 +32,12 @@ export default class LoginForm extends React.Component { if (this.props.disableSubmit) { return; } + // The native type="submit" posts the form after this handler runs, so + // only perform the pre-submit side effect here. Also calling + // this.formRef.current.submit() would post the form a second time. During + // MFA the first post authenticates and Passport regenerates the session, + // invalidating the CSRF token, so the duplicate post fails CSRF validation. this.props.formSubmit(); - this.formRef.current.submit(); }} className={styles.submit} value={this.props.action} diff --git a/src/lib/tests/LoginForm.test.js b/src/lib/tests/LoginForm.test.js new file mode 100644 index 0000000000..11934a306f --- /dev/null +++ b/src/lib/tests/LoginForm.test.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + * + * @jest-environment jsdom + */ +jest.dontMock('../../components/LoginForm/LoginForm.react'); + +import React from 'react'; +import renderer from 'react-test-renderer'; +const LoginForm = require('../../components/LoginForm/LoginForm.react').default; + +function renderForm(props) { + const submit = jest.fn(); + const component = renderer.create(, { + // Provide a stub host node so any form.submit() call is observable. + createNodeMock: element => (element.type === 'form' ? { submit } : null), + }); + const submitButton = component.root.find( + node => node.type === 'input' && node.props.type === 'submit' + ); + return { component, submit, submitButton }; +} + +describe('LoginForm', () => { + it('renders a native submit button that posts the form', () => { + const { component, submitButton } = renderForm({ formSubmit: jest.fn() }); + const form = component.root.find(node => node.type === 'form'); + expect(form.props.method).toBe('post'); + expect(form.props.action).toBe('login'); + expect(submitButton.props.type).toBe('submit'); + }); + + it('posts the form once on click and does not submit a second time programmatically', () => { + const formSubmit = jest.fn(); + const { submit, submitButton } = renderForm({ formSubmit }); + + submitButton.props.onClick(); + + // The pre-submit side effect runs exactly once. + expect(formSubmit).toHaveBeenCalledTimes(1); + // The native type="submit" posts the form; calling form.submit() here too would + // post a second time and break MFA login with a CSRF error. + expect(submit).not.toHaveBeenCalled(); + }); + + it('does not submit when disabled', () => { + const formSubmit = jest.fn(); + const { submit, submitButton } = renderForm({ formSubmit, disableSubmit: true }); + + submitButton.props.onClick(); + + expect(formSubmit).not.toHaveBeenCalled(); + expect(submit).not.toHaveBeenCalled(); + }); +});