From 458cff671f946eecb5696d2f7f873e7004a81966 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Wed, 25 Nov 2020 15:09:54 +0100 Subject: [PATCH] SONAR-14175 Adding the reset password form. --- .../components/ResetPassword.css} | 23 ++++-- .../main/js/app/components/ResetPassword.tsx | 53 +++++++++++++ .../__tests__/ResetPassword-test.tsx | 33 ++++++++ .../__snapshots__/ResetPassword-test.tsx.snap | 35 ++++++++ .../src/main/js/app/utils/startReactApp.tsx | 6 ++ .../js/apps/account/components/Security.tsx | 4 +- .../__snapshots__/Security-test.tsx.snap | 2 +- .../common/ResetPassword.tsx} | 36 ++++++--- .../common/__tests__/ResetPassword-test.tsx | 79 +++++++++++++++++++ .../ResetPassword-test.tsx.snap} | 2 +- .../resources/org/sonar/l10n/core.properties | 5 +- 11 files changed, 252 insertions(+), 26 deletions(-) rename server/sonar-web/src/main/js/{apps/account/components/__tests__/Password-test.tsx => app/components/ResetPassword.css} (75%) create mode 100644 server/sonar-web/src/main/js/app/components/ResetPassword.tsx create mode 100644 server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx create mode 100644 server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap rename server/sonar-web/src/main/js/{apps/account/components/Password.tsx => components/common/ResetPassword.tsx} (80%) create mode 100644 server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx rename server/sonar-web/src/main/js/{apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap => components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap} (97%) 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 similarity index 75% rename from server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx rename to 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()).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 ( +
+

{translate('my_account.reset_password')}

+

+ {translate('my_account.reset_password.explain')} +

+ +
+ +
+
+ ); +} + +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 = {}) { + return shallow( + + ); +} 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`] = ` +
+

+ my_account.reset_password +

+

+ my_account.reset_password.explain +

+ +
+ +
+
+`; 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 @@ -315,6 +315,12 @@ export default function startReactApp( {renderAdminRoutes()} + import('../components/ResetPassword'))} + /> 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) {
- {user.local && } + {user.local && }
); } 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`] = ` - void; } interface State { @@ -32,19 +33,25 @@ interface State { success: boolean; } -export default class Password extends React.Component { - oldPassword!: HTMLInputElement; - password!: HTMLInputElement; - passwordConfirmation!: HTMLInputElement; +export default class ResetPassword extends React.Component { + 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 { 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 { } else { changePassword({ login: user.login, password, previousPassword }).then( this.handleSuccessfulChange, - () => {} + () => { + // error already reported. + } ); } }; @@ -82,6 +93,7 @@ export default class Password extends React.Component { {errors && errors.map((e, i) => ( + /* eslint-disable-next-line react/no-array-index-key */ {e} @@ -96,7 +108,7 @@ export default class Password extends React.Component { 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 { 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 { autoComplete="off" id="password_confirmation" name="password_confirmation" - ref={elem => (this.passwordConfirmation = elem!)} + ref={elem => (this.passwordConfirmation = elem)} required={true} type="password" />
- - {translate('my_profile.password.submit')} - + {translate('update_verb')}
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) { + return shallow(); +} 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 similarity index 97% rename from server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap rename to 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`] = ` - my_profile.password.submit + update_verb diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 1917398eb95..38cefcb7b22 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1824,11 +1824,10 @@ my_profile.email=Email my_profile.groups=Groups my_profile.scm_accounts=SCM Accounts my_profile.scm_accounts.tooltip=SCM accounts are used for automatic issue assignment. Login and email are automatically considered as SCM account. -my_profile.password.title=Change password +my_profile.password.title=Enter a new password my_profile.password.old=Old Password my_profile.password.new=New Password my_profile.password.confirm=Confirm Password -my_profile.password.submit=Change password my_profile.password.changed=The password has been changed! my_profile.notifications.submit=Save changes my_profile.overall_notifications.title=Overall notifications @@ -1866,6 +1865,8 @@ my_account.add_project.azure=Azure DevOps my_account.add_project.bitbucket=Bitbucket my_account.add_project.github=GitHub my_account.add_project.gitlab=GitLab +my_account.reset_password=Update your password +my_account.reset_password.explain=This account should not use the default password. my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application -- 2.39.5