Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions src/components/LoginForm/LoginForm.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.login} style={{ marginTop: this.props.marginTop || '-220px' }}>
<Icon width={80} height={80} name="infinity" fill="#093A59" />
<form method="post" ref={this.formRef} action={this.props.endpoint} className={styles.form}>
<form method="post" action={this.props.endpoint} className={styles.form}>
<CSRFInput />
<div className={styles.header}>{this.props.header}</div>
{this.props.children}
Expand All @@ -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}
Expand Down
59 changes: 59 additions & 0 deletions src/lib/tests/LoginForm.test.js
Original file line number Diff line number Diff line change
@@ -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(<LoginForm endpoint="login" action="Log In" {...props} />, {
// 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();
});
});