@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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. | |||
*/ | |||
package org.sonarqube.qa.util.pageobjects; | |||
import static com.codeborne.selenide.Condition.text; | |||
import static com.codeborne.selenide.Condition.visible; | |||
import static com.codeborne.selenide.Selenide.$; | |||
public class EmailAlreadyExistsPage extends Navigation { | |||
public EmailAlreadyExistsPage shouldHaveExistingAccount(String login) { | |||
$(".js-existing-account").shouldHave(text(login)); | |||
return this; | |||
} | |||
public EmailAlreadyExistsPage shouldHaveNewAccount(String login) { | |||
$(".js-new-account").shouldHave(text(login)); | |||
return this; | |||
} | |||
public void clickContinue() { | |||
$(".js-continue").click(); | |||
$(".js-continue").shouldNotBe(visible); | |||
} | |||
public void clickCancel() { | |||
$(".js-cancel").click(); | |||
$(".js-cancel").shouldNotBe(visible); | |||
} | |||
} |
@@ -258,6 +258,10 @@ public class Navigation { | |||
return Selenide.$("#error"); | |||
} | |||
public EmailAlreadyExistsPage asEmailAlreadyExistsPage() { | |||
return new EmailAlreadyExistsPage(); | |||
} | |||
private static SelenideElement logInLink() { | |||
return Selenide.$(By.linkText("Log in")); | |||
} | |||
@@ -268,6 +272,7 @@ public class Navigation { | |||
/** | |||
* Safe encoding for URL parameters | |||
* | |||
* @param parameter the parameter to escape value | |||
* @return the escaped value of parameter | |||
*/ |
@@ -35,7 +35,7 @@ const config = getConfig({ production: false }); | |||
const port = process.env.PORT || 3000; | |||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; | |||
const host = process.env.HOST || 'localhost'; | |||
const proxy = 'http://localhost:9000'; | |||
const proxy = process.env.PROXY || 'http://localhost:9000'; | |||
const compiler = setupCompiler(host, port, protocol); | |||
@@ -0,0 +1,138 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 { FormattedMessage } from 'react-intl'; | |||
import { getIdentityProviders, IdentityProvider } from '../../../api/users'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
interface Props { | |||
location: { | |||
query: { | |||
email: string; | |||
login: string; | |||
provider: string; | |||
existingLogin: string; | |||
existingProvider: string; | |||
}; | |||
}; | |||
} | |||
interface State { | |||
identityProviders: IdentityProvider[]; | |||
loading: boolean; | |||
} | |||
export default class EmailAlreadyExists extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { identityProviders: [], loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchIdentityProviders(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchIdentityProviders = () => { | |||
this.setState({ loading: true }); | |||
getIdentityProviders().then( | |||
({ identityProviders }) => { | |||
if (this.mounted) { | |||
this.setState({ identityProviders, loading: false }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
renderIdentityProvier = (provider: string, login: string) => { | |||
const identityProvider = this.state.identityProviders.find(p => p.key === provider); | |||
return identityProvider ? ( | |||
<div | |||
className="identity-provider" | |||
style={{ backgroundColor: identityProvider.backgroundColor }}> | |||
<img | |||
alt={identityProvider.name} | |||
className="little-spacer-right" | |||
src={getBaseUrl() + identityProvider.iconPath} | |||
width="14" | |||
height="14" | |||
/> | |||
{login} | |||
</div> | |||
) : ( | |||
<div> | |||
{provider !== 'sonarqube' && provider} {login} | |||
</div> | |||
); | |||
}; | |||
render() { | |||
const { query } = this.props.location; | |||
return ( | |||
<div> | |||
<div className="big-spacer-bottom js-existing-account"> | |||
<p className="little-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('sessions.email_already_exists.1')} | |||
id="sessions.email_already_exists.1" | |||
values={{ email: <strong>{query.email}</strong> }} | |||
/> | |||
</p> | |||
{this.renderIdentityProvier(query.existingProvider, query.existingLogin)} | |||
</div> | |||
<div className="big-spacer-bottom js-new-account"> | |||
<p className="little-spacer-bottom">{translate('sessions.email_already_exists.2')}</p> | |||
{this.renderIdentityProvier(query.provider, query.login)} | |||
</div> | |||
<div className="alert alert-warning"> | |||
{translate('sessions.email_already_exists.3')} | |||
<ul className="list-styled"> | |||
<li className="spacer-top">{translate('sessions.email_already_exists.4')}</li> | |||
<li className="spacer-top">{translate('sessions.email_already_exists.5')}</li> | |||
<li className="spacer-top">{translate('sessions.email_already_exists.6')}</li> | |||
</ul> | |||
</div> | |||
<div className="big-spacer-top text-right"> | |||
<a | |||
className="button js-continue" | |||
href={`${getBaseUrl()}/sessions/init/${query.provider}?allowEmailShift=true`}> | |||
{translate('continue')} | |||
</a> | |||
<a className="big-spacer-left js-cancel" href={getBaseUrl() + '/'}> | |||
{translate('cancel')} | |||
</a> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -21,7 +21,7 @@ import * as React from 'react'; | |||
import SimpleContainer from '../../../app/components/SimpleContainer'; | |||
interface Props { | |||
children?: React.ReactElement<any>; | |||
children?: React.ReactNode; | |||
} | |||
export default function SimpleSessionsContainer({ children }: Props) { |
@@ -0,0 +1,58 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme'; | |||
import EmailAlreadyExists from '../EmailAlreadyExists'; | |||
jest.mock('../../../../api/users', () => ({ | |||
getIdentityProviders: () => | |||
Promise.resolve({ | |||
identityProviders: [ | |||
{ | |||
key: 'bitbucket', | |||
name: 'Bitbucket', | |||
iconPath: '/static/authbitbucket/bitbucket.svg', | |||
backgroundColor: '#205081' | |||
}, | |||
{ | |||
key: 'github', | |||
name: 'GitHub', | |||
iconPath: '/static/authgithub/github.svg', | |||
backgroundColor: '#444444' | |||
} | |||
] | |||
}) | |||
})); | |||
it('render', async () => { | |||
const query = { | |||
email: 'mail@example.com', | |||
login: 'foo', | |||
provider: 'github', | |||
existingLogin: 'bar', | |||
existingProvider: 'bitbucket' | |||
}; | |||
const wrapper = shallow(<EmailAlreadyExists location={{ query }} />); | |||
(wrapper.instance() as EmailAlreadyExists).mounted = true; | |||
(wrapper.instance() as EmailAlreadyExists).fetchIdentityProviders(); | |||
await new Promise(setImmediate); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,108 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render 1`] = ` | |||
<div> | |||
<div | |||
className="big-spacer-bottom js-existing-account" | |||
> | |||
<p | |||
className="little-spacer-bottom" | |||
> | |||
<FormattedMessage | |||
defaultMessage="sessions.email_already_exists.1" | |||
id="sessions.email_already_exists.1" | |||
values={ | |||
Object { | |||
"email": <strong> | |||
mail@example.com | |||
</strong>, | |||
} | |||
} | |||
/> | |||
</p> | |||
<div | |||
className="identity-provider" | |||
style={ | |||
Object { | |||
"backgroundColor": "#205081", | |||
} | |||
} | |||
> | |||
<img | |||
alt="Bitbucket" | |||
className="little-spacer-right" | |||
height="14" | |||
src="/static/authbitbucket/bitbucket.svg" | |||
width="14" | |||
/> | |||
bar | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-bottom js-new-account" | |||
> | |||
<p | |||
className="little-spacer-bottom" | |||
> | |||
sessions.email_already_exists.2 | |||
</p> | |||
<div | |||
className="identity-provider" | |||
style={ | |||
Object { | |||
"backgroundColor": "#444444", | |||
} | |||
} | |||
> | |||
<img | |||
alt="GitHub" | |||
className="little-spacer-right" | |||
height="14" | |||
src="/static/authgithub/github.svg" | |||
width="14" | |||
/> | |||
foo | |||
</div> | |||
</div> | |||
<div | |||
className="alert alert-warning" | |||
> | |||
sessions.email_already_exists.3 | |||
<ul | |||
className="list-styled" | |||
> | |||
<li | |||
className="spacer-top" | |||
> | |||
sessions.email_already_exists.4 | |||
</li> | |||
<li | |||
className="spacer-top" | |||
> | |||
sessions.email_already_exists.5 | |||
</li> | |||
<li | |||
className="spacer-top" | |||
> | |||
sessions.email_already_exists.6 | |||
</li> | |||
</ul> | |||
</div> | |||
<div | |||
className="big-spacer-top text-right" | |||
> | |||
<a | |||
className="button js-continue" | |||
href="/sessions/init/github?allowEmailShift=true" | |||
> | |||
continue | |||
</a> | |||
<a | |||
className="big-spacer-left js-cancel" | |||
href="/" | |||
> | |||
cancel | |||
</a> | |||
</div> | |||
</div> | |||
`; |
@@ -37,6 +37,12 @@ const routes = [ | |||
getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { | |||
import('./components/Unauthorized').then(i => callback(null, i.default)); | |||
} | |||
}, | |||
{ | |||
path: 'email_already_exists', | |||
getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { | |||
import('./components/EmailAlreadyExists').then(i => callback(null, i.default)); | |||
} | |||
} | |||
]; | |||
@@ -527,6 +527,12 @@ process.fail=Failed | |||
#------------------------------------------------------------------------------ | |||
sessions.log_in=Log in | |||
sessions.email_already_exists.1=The email address {email} is already associated to this user account: | |||
sessions.email_already_exists.2=By clicking on "Continue" you will associate this email address to a new user account: | |||
sessions.email_already_exists.3=This means the following: | |||
sessions.email_already_exists.4=Your email address will be erased from this account. | |||
sessions.email_already_exists.5=You will no longer receive email notifications from this account. | |||
sessions.email_already_exists.6=Issues won't be automatically assigned on the first account anymore. | |||
#------------------------------------------------------------------------------ |