* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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; | |||||
} |
/* | |||||
* 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)); |
/* | |||||
* 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} /> | |||||
); | |||||
} |
// 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> | |||||
`; |
{renderAdminRoutes()} | {renderAdminRoutes()} | ||||
</Route> | </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 | <Route | ||||
path="not_found" | path="not_found" | ||||
component={lazyLoadComponent(() => import('../components/NotFound'))} | component={lazyLoadComponent(() => import('../components/NotFound'))} |
import { Helmet } from 'react-helmet-async'; | import { Helmet } from 'react-helmet-async'; | ||||
import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||
import { translate } from 'sonar-ui-common/helpers/l10n'; | import { translate } from 'sonar-ui-common/helpers/l10n'; | ||||
import ResetPassword from '../../../components/common/ResetPassword'; | |||||
import { getCurrentUser, Store } from '../../../store/rootReducer'; | import { getCurrentUser, Store } from '../../../store/rootReducer'; | ||||
import Password from './Password'; | |||||
import Tokens from './Tokens'; | import Tokens from './Tokens'; | ||||
export interface SecurityProps { | export interface SecurityProps { | ||||
<div className="account-body account-container"> | <div className="account-body account-container"> | ||||
<Helmet defer={false} title={translate('my_account.security')} /> | <Helmet defer={false} title={translate('my_account.security')} /> | ||||
<Tokens login={user.login} /> | <Tokens login={user.login} /> | ||||
{user.local && <Password user={user} />} | |||||
{user.local && <ResetPassword user={user} />} | |||||
</div> | </div> | ||||
); | ); | ||||
} | } |
<Tokens | <Tokens | ||||
login="luke" | login="luke" | ||||
/> | /> | ||||
<Password | |||||
<ResetPassword | |||||
user={ | user={ | ||||
Object { | Object { | ||||
"groups": Array [], | "groups": Array [], |
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | ||||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | import { Alert } from 'sonar-ui-common/components/ui/Alert'; | ||||
import { translate } from 'sonar-ui-common/helpers/l10n'; | import { translate } from 'sonar-ui-common/helpers/l10n'; | ||||
import { changePassword } from '../../../api/users'; | |||||
import { changePassword } from '../../api/users'; | |||||
interface Props { | interface Props { | ||||
user: T.LoggedInUser; | user: T.LoggedInUser; | ||||
onPasswordChange?: () => void; | |||||
} | } | ||||
interface State { | interface State { | ||||
success: boolean; | 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 = { | state: State = { | ||||
success: false | success: false | ||||
}; | }; | ||||
handleSuccessfulChange = () => { | handleSuccessfulChange = () => { | ||||
if (!this.oldPassword || !this.password || !this.passwordConfirmation) { | |||||
return; | |||||
} | |||||
this.oldPassword.value = ''; | this.oldPassword.value = ''; | ||||
this.password.value = ''; | this.password.value = ''; | ||||
this.passwordConfirmation.value = ''; | this.passwordConfirmation.value = ''; | ||||
this.setState({ success: true, errors: undefined }); | this.setState({ success: true, errors: undefined }); | ||||
if (this.props.onPasswordChange) { | |||||
this.props.onPasswordChange(); | |||||
} | |||||
}; | }; | ||||
setErrors = (errors: string[]) => { | setErrors = (errors: string[]) => { | ||||
handleChangePassword = (event: React.FormEvent) => { | handleChangePassword = (event: React.FormEvent) => { | ||||
event.preventDefault(); | event.preventDefault(); | ||||
if (!this.oldPassword || !this.password || !this.passwordConfirmation) { | |||||
return; | |||||
} | |||||
const { user } = this.props; | const { user } = this.props; | ||||
const previousPassword = this.oldPassword.value; | const previousPassword = this.oldPassword.value; | ||||
const password = this.password.value; | const password = this.password.value; | ||||
} else { | } else { | ||||
changePassword({ login: user.login, password, previousPassword }).then( | changePassword({ login: user.login, password, previousPassword }).then( | ||||
this.handleSuccessfulChange, | this.handleSuccessfulChange, | ||||
() => {} | |||||
() => { | |||||
// error already reported. | |||||
} | |||||
); | ); | ||||
} | } | ||||
}; | }; | ||||
{errors && | {errors && | ||||
errors.map((e, i) => ( | errors.map((e, i) => ( | ||||
/* eslint-disable-next-line react/no-array-index-key */ | |||||
<Alert key={i} variant="error"> | <Alert key={i} variant="error"> | ||||
{e} | {e} | ||||
</Alert> | </Alert> | ||||
autoComplete="off" | autoComplete="off" | ||||
id="old_password" | id="old_password" | ||||
name="old_password" | name="old_password" | ||||
ref={elem => (this.oldPassword = elem!)} | |||||
ref={elem => (this.oldPassword = elem)} | |||||
required={true} | required={true} | ||||
type="password" | type="password" | ||||
/> | /> | ||||
autoComplete="off" | autoComplete="off" | ||||
id="password" | id="password" | ||||
name="password" | name="password" | ||||
ref={elem => (this.password = elem!)} | |||||
ref={elem => (this.password = elem)} | |||||
required={true} | required={true} | ||||
type="password" | type="password" | ||||
/> | /> | ||||
autoComplete="off" | autoComplete="off" | ||||
id="password_confirmation" | id="password_confirmation" | ||||
name="password_confirmation" | name="password_confirmation" | ||||
ref={elem => (this.passwordConfirmation = elem!)} | |||||
ref={elem => (this.passwordConfirmation = elem)} | |||||
required={true} | required={true} | ||||
type="password" | type="password" | ||||
/> | /> | ||||
</div> | </div> | ||||
<div className="form-field"> | <div className="form-field"> | ||||
<SubmitButton id="change-password"> | |||||
{translate('my_profile.password.submit')} | |||||
</SubmitButton> | |||||
<SubmitButton id="change-password">{translate('update_verb')}</SubmitButton> | |||||
</div> | </div> | ||||
</form> | </form> | ||||
</section> | </section> |
/* | |||||
* 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} />); | |||||
} |
<SubmitButton | <SubmitButton | ||||
id="change-password" | id="change-password" | ||||
> | > | ||||
my_profile.password.submit | |||||
update_verb | |||||
</SubmitButton> | </SubmitButton> | ||||
</div> | </div> | ||||
</form> | </form> |
my_profile.groups=Groups | my_profile.groups=Groups | ||||
my_profile.scm_accounts=SCM Accounts | 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.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.old=Old Password | ||||
my_profile.password.new=New Password | my_profile.password.new=New Password | ||||
my_profile.password.confirm=Confirm Password | my_profile.password.confirm=Confirm Password | ||||
my_profile.password.submit=Change password | |||||
my_profile.password.changed=The password has been changed! | my_profile.password.changed=The password has been changed! | ||||
my_profile.notifications.submit=Save changes | my_profile.notifications.submit=Save changes | ||||
my_profile.overall_notifications.title=Overall notifications | my_profile.overall_notifications.title=Overall notifications | ||||
my_account.add_project.bitbucket=Bitbucket | my_account.add_project.bitbucket=Bitbucket | ||||
my_account.add_project.github=GitHub | my_account.add_project.github=GitHub | ||||
my_account.add_project.gitlab=GitLab | 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 | my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application | ||||