diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2020-11-25 15:09:54 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-12-02 20:06:57 +0000 |
commit | 458cff671f946eecb5696d2f7f873e7004a81966 (patch) | |
tree | ffab993df100b8e8d1013efd242445fd70ec350e /server/sonar-web | |
parent | f1c787ccd9b21b6af6859c74814e45c1e7faea01 (diff) | |
download | sonarqube-458cff671f946eecb5696d2f7f873e7004a81966.tar.gz sonarqube-458cff671f946eecb5696d2f7f873e7004a81966.zip |
SONAR-14175 Adding the reset password form.
Diffstat (limited to 'server/sonar-web')
10 files changed, 249 insertions, 24 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx b/server/sonar-web/src/main/js/app/components/ResetPassword.css index cfa84980386..39ff63b5247 100644 --- a/server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx +++ b/server/sonar-web/src/main/js/app/components/ResetPassword.css @@ -17,11 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockLoggedInUser } from '../../../../helpers/testMocks'; -import Password from '../Password'; -it('renders correctly', () => { - expect(shallow(<Password user={mockLoggedInUser()} />)).toMatchSnapshot(); -}); +.reset-page { + padding-top: 10vh; +} + +.reset-page h1 { + line-height: 1.5; + font-size: 24px; + font-weight: 300; + text-align: center; +} + +.reset-form { + width: 300px; + margin-left: auto; + margin-right: auto; +} diff --git a/server/sonar-web/src/main/js/app/components/ResetPassword.tsx b/server/sonar-web/src/main/js/app/components/ResetPassword.tsx new file mode 100644 index 00000000000..3d223ad6449 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/ResetPassword.tsx @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import ResetPasswordForm from '../../components/common/ResetPassword'; +import { whenLoggedIn } from '../../components/hoc/whenLoggedIn'; +import { Router, withRouter } from '../../components/hoc/withRouter'; +import GlobalMessagesContainer from './GlobalMessagesContainer'; +import './ResetPassword.css'; + +export interface ResetPasswordProps { + currentUser: T.LoggedInUser; + router: Router; +} + +export function ResetPassword(props: ResetPasswordProps) { + const { router, currentUser } = props; + const redirect = () => { + router.replace('/'); + }; + + return ( + <div className="reset-page"> + <h1 className="text-center spacer-bottom">{translate('my_account.reset_password')}</h1> + <h2 className="text-center huge-spacer-bottom"> + {translate('my_account.reset_password.explain')} + </h2> + <GlobalMessagesContainer /> + <div className="reset-form"> + <ResetPasswordForm user={currentUser} onPasswordChange={redirect} /> + </div> + </div> + ); +} + +export default whenLoggedIn(withRouter(ResetPassword)); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx new file mode 100644 index 00000000000..941cff45923 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockLoggedInUser, mockRouter } from '../../../helpers/testMocks'; +import { ResetPassword, ResetPasswordProps } from '../ResetPassword'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<ResetPasswordProps> = {}) { + return shallow( + <ResetPassword currentUser={mockLoggedInUser()} router={mockRouter()} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap new file mode 100644 index 00000000000..ad2db130e85 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="reset-page" +> + <h1 + className="text-center spacer-bottom" + > + my_account.reset_password + </h1> + <h2 + className="text-center huge-spacer-bottom" + > + my_account.reset_password.explain + </h2> + <Connect(GlobalMessages) /> + <div + className="reset-form" + > + <ResetPassword + onPasswordChange={[Function]} + user={ + Object { + "groups": Array [], + "isLoggedIn": true, + "login": "luke", + "name": "Skywalker", + "scmAccounts": Array [], + } + } + /> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index c135674b791..d48e43e2a9f 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -316,6 +316,12 @@ export default function startReactApp( {renderAdminRoutes()} </Route> <Route + // We don't want this route to have any menu. + // That is why we can not have it under the accountRoutes + path="account/reset_password" + component={lazyLoadComponent(() => import('../components/ResetPassword'))} + /> + <Route path="not_found" component={lazyLoadComponent(() => import('../components/NotFound'))} /> diff --git a/server/sonar-web/src/main/js/apps/account/components/Security.tsx b/server/sonar-web/src/main/js/apps/account/components/Security.tsx index 49637c3ad6f..8ac25c7955a 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Security.tsx +++ b/server/sonar-web/src/main/js/apps/account/components/Security.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { connect } from 'react-redux'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import ResetPassword from '../../../components/common/ResetPassword'; import { getCurrentUser, Store } from '../../../store/rootReducer'; -import Password from './Password'; import Tokens from './Tokens'; export interface SecurityProps { @@ -34,7 +34,7 @@ export function Security({ user }: SecurityProps) { <div className="account-body account-container"> <Helmet defer={false} title={translate('my_account.security')} /> <Tokens login={user.login} /> - {user.local && <Password user={user} />} + {user.local && <ResetPassword user={user} />} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap index 46d08174e68..4adb1b2c1c2 100644 --- a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap @@ -12,7 +12,7 @@ exports[`should render correctly: local user 1`] = ` <Tokens login="luke" /> - <Password + <ResetPassword user={ Object { "groups": Array [], diff --git a/server/sonar-web/src/main/js/apps/account/components/Password.tsx b/server/sonar-web/src/main/js/components/common/ResetPassword.tsx index c689719ece1..fd73381b933 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Password.tsx +++ b/server/sonar-web/src/main/js/components/common/ResetPassword.tsx @@ -21,10 +21,11 @@ import * as React from 'react'; import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { changePassword } from '../../../api/users'; +import { changePassword } from '../../api/users'; interface Props { user: T.LoggedInUser; + onPasswordChange?: () => void; } interface State { @@ -32,19 +33,25 @@ interface State { success: boolean; } -export default class Password extends React.Component<Props, State> { - oldPassword!: HTMLInputElement; - password!: HTMLInputElement; - passwordConfirmation!: HTMLInputElement; +export default class ResetPassword extends React.Component<Props, State> { + oldPassword: HTMLInputElement | null = null; + password: HTMLInputElement | null = null; + passwordConfirmation: HTMLInputElement | null = null; state: State = { success: false }; handleSuccessfulChange = () => { + if (!this.oldPassword || !this.password || !this.passwordConfirmation) { + return; + } this.oldPassword.value = ''; this.password.value = ''; this.passwordConfirmation.value = ''; this.setState({ success: true, errors: undefined }); + if (this.props.onPasswordChange) { + this.props.onPasswordChange(); + } }; setErrors = (errors: string[]) => { @@ -53,7 +60,9 @@ export default class Password extends React.Component<Props, State> { handleChangePassword = (event: React.FormEvent) => { event.preventDefault(); - + if (!this.oldPassword || !this.password || !this.passwordConfirmation) { + return; + } const { user } = this.props; const previousPassword = this.oldPassword.value; const password = this.password.value; @@ -65,7 +74,9 @@ export default class Password extends React.Component<Props, State> { } else { changePassword({ login: user.login, password, previousPassword }).then( this.handleSuccessfulChange, - () => {} + () => { + // error already reported. + } ); } }; @@ -82,6 +93,7 @@ export default class Password extends React.Component<Props, State> { {errors && errors.map((e, i) => ( + /* eslint-disable-next-line react/no-array-index-key */ <Alert key={i} variant="error"> {e} </Alert> @@ -96,7 +108,7 @@ export default class Password extends React.Component<Props, State> { autoComplete="off" id="old_password" name="old_password" - ref={elem => (this.oldPassword = elem!)} + ref={elem => (this.oldPassword = elem)} required={true} type="password" /> @@ -110,7 +122,7 @@ export default class Password extends React.Component<Props, State> { autoComplete="off" id="password" name="password" - ref={elem => (this.password = elem!)} + ref={elem => (this.password = elem)} required={true} type="password" /> @@ -124,15 +136,13 @@ export default class Password extends React.Component<Props, State> { autoComplete="off" id="password_confirmation" name="password_confirmation" - ref={elem => (this.passwordConfirmation = elem!)} + ref={elem => (this.passwordConfirmation = elem)} required={true} type="password" /> </div> <div className="form-field"> - <SubmitButton id="change-password"> - {translate('my_profile.password.submit')} - </SubmitButton> + <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton> </div> </form> </section> diff --git a/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx new file mode 100644 index 00000000000..ac7bd639d49 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { changePassword } from '../../../api/users'; +import { mockEvent, mockLoggedInUser } from '../../../helpers/testMocks'; +import ResetPassword from '../ResetPassword'; + +jest.mock('../../../api/users', () => ({ + changePassword: jest.fn().mockResolvedValue({}) +})); + +it('should trigger on password change prop', () => { + const onPasswordChange = jest.fn(); + const wrapper = shallowRender({ onPasswordChange }); + wrapper.instance().handleSuccessfulChange(); + expect(onPasswordChange).not.toBeCalled(); + wrapper.instance().oldPassword = { value: '' } as HTMLInputElement; + wrapper.instance().password = { value: '' } as HTMLInputElement; + wrapper.instance().passwordConfirmation = { value: '' } as HTMLInputElement; + wrapper.instance().handleSuccessfulChange(); + expect(onPasswordChange).toBeCalled(); +}); + +it('should not trigger password change', () => { + const wrapper = shallowRender(); + wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement; + wrapper.instance().password = { value: 'test', focus: () => {} } as HTMLInputElement; + wrapper.instance().passwordConfirmation = { value: 'test1' } as HTMLInputElement; + wrapper.instance().handleChangePassword(mockEvent()); + expect(changePassword).not.toBeCalled(); + expect(wrapper.state().errors).toBeDefined(); +}); + +it('should trigger password change', async () => { + const user = mockLoggedInUser(); + const wrapper = shallowRender({ user }); + wrapper.instance().handleChangePassword(mockEvent()); + await waitAndUpdate(wrapper); + expect(changePassword).not.toBeCalled(); + + wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement; + wrapper.instance().password = { value: 'test' } as HTMLInputElement; + wrapper.instance().passwordConfirmation = { value: 'test' } as HTMLInputElement; + wrapper.instance().handleChangePassword(mockEvent()); + await waitAndUpdate(wrapper); + + expect(changePassword).toBeCalledWith({ + login: user.login, + password: 'test', + previousPassword: 'testold' + }); +}); + +it('renders correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +function shallowRender(props?: Partial<ResetPassword['props']>) { + return shallow<ResetPassword>(<ResetPassword user={mockLoggedInUser()} {...props} />); +} diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap index e16485c7246..e4983b61dc3 100644 --- a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap @@ -82,7 +82,7 @@ exports[`renders correctly 1`] = ` <SubmitButton id="change-password" > - my_profile.password.submit + update_verb </SubmitButton> </div> </form> |