]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14175 Adding the reset password form.
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 25 Nov 2020 14:09:54 +0000 (15:09 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 2 Dec 2020 20:06:57 +0000 (20:06 +0000)
14 files changed:
server/sonar-web/src/main/js/app/components/ResetPassword.css [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/ResetPassword.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/account/components/Password.tsx [deleted file]
server/sonar-web/src/main/js/apps/account/components/Security.tsx
server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap
server/sonar-web/src/main/js/components/common/ResetPassword.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/app/components/ResetPassword.css b/server/sonar-web/src/main/js/app/components/ResetPassword.css
new file mode 100644 (file)
index 0000000..39ff63b
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+.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 (file)
index 0000000..3d223ad
--- /dev/null
@@ -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 (file)
index 0000000..941cff4
--- /dev/null
@@ -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 (file)
index 0000000..ad2db13
--- /dev/null
@@ -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>
+`;
index c135674b791a07e392d877c8983cd3b2f341221a..d48e43e2a9f3ba0a4f5836bf0bbaeb4e34c34e93 100644 (file)
@@ -315,6 +315,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/Password.tsx b/server/sonar-web/src/main/js/apps/account/components/Password.tsx
deleted file mode 100644 (file)
index c689719..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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 { 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';
-
-interface Props {
-  user: T.LoggedInUser;
-}
-
-interface State {
-  errors?: string[];
-  success: boolean;
-}
-
-export default class Password extends React.Component<Props, State> {
-  oldPassword!: HTMLInputElement;
-  password!: HTMLInputElement;
-  passwordConfirmation!: HTMLInputElement;
-  state: State = {
-    success: false
-  };
-
-  handleSuccessfulChange = () => {
-    this.oldPassword.value = '';
-    this.password.value = '';
-    this.passwordConfirmation.value = '';
-    this.setState({ success: true, errors: undefined });
-  };
-
-  setErrors = (errors: string[]) => {
-    this.setState({ success: false, errors });
-  };
-
-  handleChangePassword = (event: React.FormEvent) => {
-    event.preventDefault();
-
-    const { user } = this.props;
-    const previousPassword = this.oldPassword.value;
-    const password = this.password.value;
-    const passwordConfirmation = this.passwordConfirmation.value;
-
-    if (password !== passwordConfirmation) {
-      this.password.focus();
-      this.setErrors([translate('user.password_doesnt_match_confirmation')]);
-    } else {
-      changePassword({ login: user.login, password, previousPassword }).then(
-        this.handleSuccessfulChange,
-        () => {}
-      );
-    }
-  };
-
-  render() {
-    const { success, errors } = this.state;
-
-    return (
-      <section className="boxed-group">
-        <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
-
-        <form className="boxed-group-inner" onSubmit={this.handleChangePassword}>
-          {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
-
-          {errors &&
-            errors.map((e, i) => (
-              <Alert key={i} variant="error">
-                {e}
-              </Alert>
-            ))}
-
-          <div className="form-field">
-            <label htmlFor="old_password">
-              {translate('my_profile.password.old')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              autoComplete="off"
-              id="old_password"
-              name="old_password"
-              ref={elem => (this.oldPassword = elem!)}
-              required={true}
-              type="password"
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="password">
-              {translate('my_profile.password.new')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              autoComplete="off"
-              id="password"
-              name="password"
-              ref={elem => (this.password = elem!)}
-              required={true}
-              type="password"
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="password_confirmation">
-              {translate('my_profile.password.confirm')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              autoComplete="off"
-              id="password_confirmation"
-              name="password_confirmation"
-              ref={elem => (this.passwordConfirmation = elem!)}
-              required={true}
-              type="password"
-            />
-          </div>
-          <div className="form-field">
-            <SubmitButton id="change-password">
-              {translate('my_profile.password.submit')}
-            </SubmitButton>
-          </div>
-        </form>
-      </section>
-    );
-  }
-}
index 49637c3ad6f8c5e9ada411bf4d2f515b6a84dfa5..8ac25c7955a7c6fa3c91987b5cfa6933b923cae0 100644 (file)
@@ -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__/Password-test.tsx b/server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx
deleted file mode 100644 (file)
index cfa8498..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 } from '../../../../helpers/testMocks';
-import Password from '../Password';
-
-it('renders correctly', () => {
-  expect(shallow(<Password user={mockLoggedInUser()} />)).toMatchSnapshot();
-});
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/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap
deleted file mode 100644 (file)
index e16485c..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly 1`] = `
-<section
-  className="boxed-group"
->
-  <h2
-    className="spacer-bottom"
-  >
-    my_profile.password.title
-  </h2>
-  <form
-    className="boxed-group-inner"
-    onSubmit={[Function]}
-  >
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="old_password"
-      >
-        my_profile.password.old
-        <em
-          className="mandatory"
-        >
-          *
-        </em>
-      </label>
-      <input
-        autoComplete="off"
-        id="old_password"
-        name="old_password"
-        required={true}
-        type="password"
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="password"
-      >
-        my_profile.password.new
-        <em
-          className="mandatory"
-        >
-          *
-        </em>
-      </label>
-      <input
-        autoComplete="off"
-        id="password"
-        name="password"
-        required={true}
-        type="password"
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="password_confirmation"
-      >
-        my_profile.password.confirm
-        <em
-          className="mandatory"
-        >
-          *
-        </em>
-      </label>
-      <input
-        autoComplete="off"
-        id="password_confirmation"
-        name="password_confirmation"
-        required={true}
-        type="password"
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <SubmitButton
-        id="change-password"
-      >
-        my_profile.password.submit
-      </SubmitButton>
-    </div>
-  </form>
-</section>
-`;
index 46d08174e686f9ba11715009e6cd1ecde4ed356c..4adb1b2c1c23c60bb95cebf8cbde3dc5844ca1c8 100644 (file)
@@ -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/components/common/ResetPassword.tsx b/server/sonar-web/src/main/js/components/common/ResetPassword.tsx
new file mode 100644 (file)
index 0000000..fd73381
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * 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 { 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';
+
+interface Props {
+  user: T.LoggedInUser;
+  onPasswordChange?: () => void;
+}
+
+interface State {
+  errors?: string[];
+  success: boolean;
+}
+
+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[]) => {
+    this.setState({ success: false, errors });
+  };
+
+  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;
+    const passwordConfirmation = this.passwordConfirmation.value;
+
+    if (password !== passwordConfirmation) {
+      this.password.focus();
+      this.setErrors([translate('user.password_doesnt_match_confirmation')]);
+    } else {
+      changePassword({ login: user.login, password, previousPassword }).then(
+        this.handleSuccessfulChange,
+        () => {
+          // error already reported.
+        }
+      );
+    }
+  };
+
+  render() {
+    const { success, errors } = this.state;
+
+    return (
+      <section className="boxed-group">
+        <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
+
+        <form className="boxed-group-inner" onSubmit={this.handleChangePassword}>
+          {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
+
+          {errors &&
+            errors.map((e, i) => (
+              /* eslint-disable-next-line react/no-array-index-key */
+              <Alert key={i} variant="error">
+                {e}
+              </Alert>
+            ))}
+
+          <div className="form-field">
+            <label htmlFor="old_password">
+              {translate('my_profile.password.old')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              autoComplete="off"
+              id="old_password"
+              name="old_password"
+              ref={elem => (this.oldPassword = elem)}
+              required={true}
+              type="password"
+            />
+          </div>
+          <div className="form-field">
+            <label htmlFor="password">
+              {translate('my_profile.password.new')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              autoComplete="off"
+              id="password"
+              name="password"
+              ref={elem => (this.password = elem)}
+              required={true}
+              type="password"
+            />
+          </div>
+          <div className="form-field">
+            <label htmlFor="password_confirmation">
+              {translate('my_profile.password.confirm')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              autoComplete="off"
+              id="password_confirmation"
+              name="password_confirmation"
+              ref={elem => (this.passwordConfirmation = elem)}
+              required={true}
+              type="password"
+            />
+          </div>
+          <div className="form-field">
+            <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 (file)
index 0000000..ac7bd63
--- /dev/null
@@ -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/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap
new file mode 100644 (file)
index 0000000..e4983b6
--- /dev/null
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<section
+  className="boxed-group"
+>
+  <h2
+    className="spacer-bottom"
+  >
+    my_profile.password.title
+  </h2>
+  <form
+    className="boxed-group-inner"
+    onSubmit={[Function]}
+  >
+    <div
+      className="form-field"
+    >
+      <label
+        htmlFor="old_password"
+      >
+        my_profile.password.old
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <input
+        autoComplete="off"
+        id="old_password"
+        name="old_password"
+        required={true}
+        type="password"
+      />
+    </div>
+    <div
+      className="form-field"
+    >
+      <label
+        htmlFor="password"
+      >
+        my_profile.password.new
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <input
+        autoComplete="off"
+        id="password"
+        name="password"
+        required={true}
+        type="password"
+      />
+    </div>
+    <div
+      className="form-field"
+    >
+      <label
+        htmlFor="password_confirmation"
+      >
+        my_profile.password.confirm
+        <em
+          className="mandatory"
+        >
+          *
+        </em>
+      </label>
+      <input
+        autoComplete="off"
+        id="password_confirmation"
+        name="password_confirmation"
+        required={true}
+        type="password"
+      />
+    </div>
+    <div
+      className="form-field"
+    >
+      <SubmitButton
+        id="change-password"
+      >
+        update_verb
+      </SubmitButton>
+    </div>
+  </form>
+</section>
+`;
index 1917398eb95423d2340f06ede85b4e0f69c218bb..38cefcb7b2241a04382a9cb0e43cc64ef4fd709f 100644 (file)
@@ -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