Browse Source

SONAR-14175 Adding the reset password form.

tags/8.6.0.39681
Mathieu Suen 3 years ago
parent
commit
458cff671f

server/sonar-web/src/main/js/apps/account/components/__tests__/Password-test.tsx → server/sonar-web/src/main/js/app/components/ResetPassword.css View File

@@ -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;
}

+ 53
- 0
server/sonar-web/src/main/js/app/components/ResetPassword.tsx View File

@@ -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));

+ 33
- 0
server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx View File

@@ -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} />
);
}

+ 35
- 0
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap View File

@@ -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>
`;

+ 6
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.tsx View 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'))}

+ 2
- 2
server/sonar-web/src/main/js/apps/account/components/Security.tsx View 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>
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap View File

@@ -12,7 +12,7 @@ exports[`should render correctly: local user 1`] = `
<Tokens
login="luke"
/>
<Password
<ResetPassword
user={
Object {
"groups": Array [],

server/sonar-web/src/main/js/apps/account/components/Password.tsx → server/sonar-web/src/main/js/components/common/ResetPassword.tsx View File

@@ -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>

+ 79
- 0
server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx View File

@@ -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} />);
}

server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Password-test.tsx.snap → server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap View File

@@ -82,7 +82,7 @@ exports[`renders correctly 1`] = `
<SubmitButton
id="change-password"
>
my_profile.password.submit
update_verb
</SubmitButton>
</div>
</form>

+ 3
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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


Loading…
Cancel
Save