aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2020-11-25 15:09:54 +0100
committersonartech <sonartech@sonarsource.com>2020-12-02 20:06:57 +0000
commit458cff671f946eecb5696d2f7f873e7004a81966 (patch)
treeffab993df100b8e8d1013efd242445fd70ec350e /server/sonar-web
parentf1c787ccd9b21b6af6859c74814e45c1e7faea01 (diff)
downloadsonarqube-458cff671f946eecb5696d2f7f873e7004a81966.tar.gz
sonarqube-458cff671f946eecb5696d2f7f873e7004a81966.zip
SONAR-14175 Adding the reset password form.
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/app/components/ResetPassword.css (renamed from server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx)23
-rw-r--r--server/sonar-web/src/main/js/app/components/ResetPassword.tsx53
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap35
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Security.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/common/ResetPassword.tsx (renamed from server/sonar-web/src/main/js/apps/account/components/Password.tsx)36
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx79
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap)2
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>