import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import it.Category4Suite;
-import it.user.ForceAuthenticationTest;
import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.WsResponse;
-import org.sonarqube.ws.client.setting.SetRequest;
import pageobjects.Navigation;
import util.ItUtils;
import static com.codeborne.selenide.WebDriverRunner.url;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.projectDir;
+import static util.ItUtils.resetSettings;
import static util.ItUtils.setServerProperty;
public class UiTest {
@Rule
public Navigation nav = Navigation.get(ORCHESTRATOR);
+ @Before
+ @After
+ public void resetData() throws Exception {
+ resetSettings(ORCHESTRATOR, null, "sonar.forceAuthentication");
+ }
+
@Test
public void footer_contains_information() {
nav.getFooter()
nav.getFooter()
.shouldNot(hasText("About"))
.shouldNot(hasText("Web API"));
- setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", null);
}
@Test
import java.io.File;
import org.apache.commons.io.FileUtils;
import org.junit.After;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
adminWsClient = newAdminWsClient(ORCHESTRATOR);
}
+ @Before
@After
- public void cleanUpUsersAndGroupsAndProperties() throws Exception {
+ public void resetData() throws Exception {
userRule.resetUsers();
userRule.removeGroups(GROUP1, GROUP2, GROUP3);
- resetSettings(ORCHESTRATOR, null, "sonar.auth.fake-base-id-provider.enabled", "sonar.auth.fake-base-id-provider.user",
- "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "sonar.auth.fake-base-id-provider.enabledGroupsSync", "sonar.auth.fake-base-id-provider.groups",
+ resetSettings(ORCHESTRATOR, null,
+ "sonar.auth.fake-base-id-provider.enabled",
+ "sonar.auth.fake-base-id-provider.user",
+ "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage",
+ "sonar.auth.fake-base-id-provider.enabledGroupsSync",
+ "sonar.auth.fake-base-id-provider.groups",
"sonar.auth.fake-base-id-provider.allowsUsersToSignUp");
}
String fakeServerAuthProviderUrl;
@BeforeClass
- public static void resetData() {
+ public static void initData() {
ORCHESTRATOR.resetData();
adminWsClient = newAdminWsClient(ORCHESTRATOR);
}
- @After
- public void resetUsers() throws Exception {
- userRule.resetUsers();
- }
-
@Before
public void setUp() throws Exception {
fakeServerAuthProvider = new MockWebServer();
fakeServerAuthProvider.start();
fakeServerAuthProviderUrl = fakeServerAuthProvider.url("").url().toString();
- userRule.resetUsers();
- resetSettings(ORCHESTRATOR, null, "sonar.auth.fake-oauth2-id-provider.enabled",
- "sonar.auth.fake-oauth2-id-provider.url",
- "sonar.auth.fake-oauth2-id-provider.user",
- "sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage",
- "sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp");
+ resetData();
}
@After
public void tearDown() throws Exception {
+ resetData();
fakeServerAuthProvider.shutdown();
}
+ private void resetData(){
+ userRule.resetUsers();
+ resetSettings(ORCHESTRATOR, null,
+ "sonar.auth.fake-oauth2-id-provider.enabled",
+ "sonar.auth.fake-oauth2-id-provider.url",
+ "sonar.auth.fake-oauth2-id-provider.user",
+ "sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage",
+ "sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp");
+ }
+
@Test
public void create_new_user_when_authenticate() throws Exception {
simulateRedirectionToCallback();
fetchMyOrganizations: () => Promise<*>,
location: Object,
organizations: Array<{ key: string, name: string }>,
- router: { push: string => void }
+ router: { push: string => void },
+ sonarCloud: boolean
};
type State = {
}
renderAnonymous() {
- return (
- <li>
- <a onClick={this.handleLogin} href="#">{translate('layout.login')}</a>
- </li>
- );
+ return this.props.sonarCloud
+ ? <li>
+ <a href="/sessions/init/github">
+ <img
+ alt="GitHub"
+ className="navbar-global-login-github"
+ width="14"
+ height="14"
+ src="/static/authgithub/github.svg"
+ />
+ {translate('layout.login')}
+ </a>
+ </li>
+ : <li>
+ <a onClick={this.handleLogin} href="#">
+ {translate('layout.login')}
+ </a>
+ </li>;
}
render() {
<Redirect from="/profiles/index" to="/profiles" />
<Redirect from="/quality_gates/index" to="/quality_gates" />
<Redirect from="/settings/index" to="/settings" />
+ <Redirect from="/sessions/login" to="/sessions/new" />
<Redirect from="/system/index" to="/system" />
<Route path="markdown/help" component={MarkdownHelp} />
<Route component={MigrationContainer}>
<Route component={SimpleSessionsContainer}>
- <Route path="/sessions">{sessionsRoutes}</Route>
+ <Route path="/sessions" childRoutes={sessionsRoutes} />
</Route>
<Route path="/" component={App}>
*/
// @flow
import React from 'react';
-import { Link } from 'react-router';
import AboutProjects from './AboutProjects';
import EntryIssueTypesForSonarQubeDotCom from './EntryIssueTypesForSonarQubeDotCom';
import AboutRulesForSonarQubeDotCom from './AboutRulesForSonarQubeDotCom';
import AboutLeakPeriod from './AboutLeakPeriod';
import AboutStandards from './AboutStandards';
import AboutScanners from './AboutScanners';
-import { translate } from '../../../helpers/l10n';
import '../sonarqube-dot-com-styles.css';
type Props = {
<h1 className="big-spacer-bottom">
Continuous Code Quality<br />as a Service
</h1>
- <a
- className="button button-active"
- href="https://about.sonarcloud.io/get-started/"
- target="_blank">
- Get Started
- </a>
{!props.currentUser.isLoggedIn &&
- <Link to="/sessions/new" className="button big-spacer-left">
- {translate('layout.login')}
- </Link>}
+ <a className="sonarcloud-about-github-button" href="/sessions/init/github">
+ <img alt="GitHub" width="20" height="20" src="/static/authgithub/github.svg" />
+ Connect With GitHub to Get Started
+ </a>}
</div>
<div className="sqcom-about-page-instance">
font-weight: 300;
}
-.sqcom-about-page-intro > .button {
+.sonarcloud-about-github-button {
+ display: inline-block;
height: 44px;
- line-height: 42px;
+ line-height: 46px;
padding-left: 20px;
padding-right: 20px;
- border-color: #fff;
+ border: none;
border-radius: 3px;
- color: #fff;
- font-size: 16px;
+ background-color: #444;
+ color: #fff !important;
+ font-size: 15px;
font-weight: 500;
text-transform: uppercase;
- transition: none;
-}
-
-.sqcom-about-page-intro > .button:hover {
- background-color: #fff;
- color: #4b9fd5;
}
-.sqcom-about-page-intro > .button-active {
- border-color: #b0eb41;
- background-color: #b0eb41;
- color: #225463;
+.sonarcloud-about-github-button:hover,
+.sonarcloud-about-github-button:focus {
+ background-color: #333;
}
-.sqcom-about-page-intro > .button-active:hover {
- border-color: #91d315;
- background-color: #91d315;
- color: #225463;
+.sonarcloud-about-github-button img {
+ margin-top: 12px;
+ margin-right: 10px;
}
.sqcom-about-page-instance {
import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer';
import { translate } from '../../../helpers/l10n';
+type Props = {
+ identityProviders: Array<{
+ backgroundColor: string,
+ iconPath: string,
+ key: string,
+ name: string
+ }>,
+ onSubmit: (string, string) => void
+};
+
+type State = {
+ collapsed: boolean,
+ login: string,
+ password: string
+};
+
export default class LoginForm extends React.PureComponent {
- static propTypes = {
- identityProviders: React.PropTypes.array.isRequired,
- onSubmit: React.PropTypes.func.isRequired
- };
+ props: Props;
+ state: State;
- state = {
- login: '',
- password: ''
- };
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ collapsed: props.identityProviders.length > 0,
+ login: '',
+ password: ''
+ };
+ }
- handleSubmit = (e: Object) => {
- e.preventDefault();
+ handleSubmit = (event: Event) => {
+ event.preventDefault();
this.props.onSubmit(this.state.login, this.state.password);
};
+ handleMoreOptionsClick = (event: Event) => {
+ event.preventDefault();
+ this.setState({ collapsed: false });
+ };
+
render() {
return (
- <div>
- <h1 className="maintenance-title text-center">Log In to SonarQube</h1>
+ <div id="login_form">
+ <h1 className="maintenance-title text-center">{translate('login.login_to_sonarqube')}</h1>
{this.props.identityProviders.length > 0 &&
<section className="oauth-providers">
</ul>
</section>}
- <form id="login_form" onSubmit={this.handleSubmit}>
- <GlobalMessagesContainer />
+ {this.state.collapsed
+ ? <div className="text-center">
+ <a
+ className="small text-muted js-more-options"
+ href="#"
+ onClick={this.handleMoreOptionsClick}>
+ {translate('login.more_options')}
+ </a>
+ </div>
+ : <form onSubmit={this.handleSubmit}>
+ <GlobalMessagesContainer />
- <div className="big-spacer-bottom">
- <label htmlFor="login" className="login-label">{translate('login')}</label>
- <input
- type="text"
- id="login"
- name="login"
- className="login-input"
- maxLength="255"
- required={true}
- autoFocus={true}
- placeholder={translate('login')}
- value={this.state.login}
- onChange={e => this.setState({ login: e.target.value })}
- />
- </div>
+ <div className="big-spacer-bottom">
+ <label htmlFor="login" className="login-label">{translate('login')}</label>
+ <input
+ type="text"
+ id="login"
+ name="login"
+ className="login-input"
+ maxLength="255"
+ required={true}
+ autoFocus={true}
+ placeholder={translate('login')}
+ value={this.state.login}
+ onChange={e => this.setState({ login: e.target.value })}
+ />
+ </div>
- <div className="big-spacer-bottom">
- <label htmlFor="password" className="login-label">{translate('password')}</label>
- <input
- type="password"
- id="password"
- name="password"
- className="login-input"
- required={true}
- placeholder={translate('password')}
- value={this.state.password}
- onChange={e => this.setState({ password: e.target.value })}
- />
- </div>
+ <div className="big-spacer-bottom">
+ <label htmlFor="password" className="login-label">{translate('password')}</label>
+ <input
+ type="password"
+ id="password"
+ name="password"
+ className="login-input"
+ required={true}
+ placeholder={translate('password')}
+ value={this.state.password}
+ onChange={e => this.setState({ password: e.target.value })}
+ />
+ </div>
- <div>
- <div className="text-right overflow-hidden">
- <button name="commit" type="submit">{translate('sessions.log_in')}</button>
- <a className="spacer-left" href={window.baseUrl + '/'}>{translate('cancel')}</a>
- </div>
- </div>
- </form>
+ <div>
+ <div className="text-right overflow-hidden">
+ <button name="commit" type="submit">{translate('sessions.log_in')}</button>
+ <a className="spacer-left" href={window.baseUrl + '/'}>{translate('cancel')}</a>
+ </div>
+ </div>
+ </form>}
</div>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import LoginForm from '../LoginForm';
+import { change, click, submit } from '../../../../helpers/testUtils';
+
+const identityProvider = {
+ backgroundColor: '#000',
+ iconPath: '/some/path',
+ key: 'foo',
+ name: 'foo'
+};
+
+it('logs in with simple credentials', () => {
+ const onSubmit = jest.fn();
+ const wrapper = shallow(<LoginForm identityProviders={[]} onSubmit={onSubmit} />);
+ expect(wrapper).toMatchSnapshot();
+
+ change(wrapper.find('#login'), 'admin');
+ change(wrapper.find('#password'), 'admin');
+ submit(wrapper.find('form'));
+
+ expect(onSubmit).toBeCalledWith('admin', 'admin');
+});
+
+it('logs in with identity provider', () => {
+ const wrapper = shallow(
+ <LoginForm identityProviders={[identityProvider]} onSubmit={jest.fn()} />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('expands more options', () => {
+ const wrapper = shallow(
+ <LoginForm identityProviders={[identityProvider]} onSubmit={jest.fn()} />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('.js-more-options'));
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`expands more options 1`] = `
+<div
+ id="login_form"
+>
+ <h1
+ className="maintenance-title text-center"
+ >
+ login.login_to_sonarqube
+ </h1>
+ <section
+ className="oauth-providers"
+ >
+ <ul>
+ <li>
+ <a
+ href="/sessions/init/foo"
+ style={
+ Object {
+ "backgroundColor": "#000",
+ }
+ }
+ title="Log in with foo"
+ >
+ <img
+ alt="foo"
+ height="20"
+ src="/some/path"
+ width="20"
+ />
+ <span>
+ Log in with
+ foo
+ </span>
+ </a>
+ </li>
+ </ul>
+ </section>
+ <div
+ className="text-center"
+ >
+ <a
+ className="small text-muted js-more-options"
+ href="#"
+ onClick={[Function]}
+ >
+ login.more_options
+ </a>
+ </div>
+</div>
+`;
+
+exports[`expands more options 2`] = `
+<div
+ id="login_form"
+>
+ <h1
+ className="maintenance-title text-center"
+ >
+ login.login_to_sonarqube
+ </h1>
+ <section
+ className="oauth-providers"
+ >
+ <ul>
+ <li>
+ <a
+ href="/sessions/init/foo"
+ style={
+ Object {
+ "backgroundColor": "#000",
+ }
+ }
+ title="Log in with foo"
+ >
+ <img
+ alt="foo"
+ height="20"
+ src="/some/path"
+ width="20"
+ />
+ <span>
+ Log in with
+ foo
+ </span>
+ </a>
+ </li>
+ </ul>
+ </section>
+ <form
+ onSubmit={[Function]}
+ >
+ <Connect(GlobalMessages) />
+ <div
+ className="big-spacer-bottom"
+ >
+ <label
+ className="login-label"
+ htmlFor="login"
+ >
+ login
+ </label>
+ <input
+ autoFocus={true}
+ className="login-input"
+ id="login"
+ maxLength="255"
+ name="login"
+ onChange={[Function]}
+ placeholder="login"
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <div
+ className="big-spacer-bottom"
+ >
+ <label
+ className="login-label"
+ htmlFor="password"
+ >
+ password
+ </label>
+ <input
+ className="login-input"
+ id="password"
+ name="password"
+ onChange={[Function]}
+ placeholder="password"
+ required={true}
+ type="password"
+ value=""
+ />
+ </div>
+ <div>
+ <div
+ className="text-right overflow-hidden"
+ >
+ <button
+ name="commit"
+ type="submit"
+ >
+ sessions.log_in
+ </button>
+ <a
+ className="spacer-left"
+ href="/"
+ >
+ cancel
+ </a>
+ </div>
+ </div>
+ </form>
+</div>
+`;
+
+exports[`logs in with identity provider 1`] = `
+<div
+ id="login_form"
+>
+ <h1
+ className="maintenance-title text-center"
+ >
+ login.login_to_sonarqube
+ </h1>
+ <section
+ className="oauth-providers"
+ >
+ <ul>
+ <li>
+ <a
+ href="/sessions/init/foo"
+ style={
+ Object {
+ "backgroundColor": "#000",
+ }
+ }
+ title="Log in with foo"
+ >
+ <img
+ alt="foo"
+ height="20"
+ src="/some/path"
+ width="20"
+ />
+ <span>
+ Log in with
+ foo
+ </span>
+ </a>
+ </li>
+ </ul>
+ </section>
+ <div
+ className="text-center"
+ >
+ <a
+ className="small text-muted js-more-options"
+ href="#"
+ onClick={[Function]}
+ >
+ login.more_options
+ </a>
+ </div>
+</div>
+`;
+
+exports[`logs in with simple credentials 1`] = `
+<div
+ id="login_form"
+>
+ <h1
+ className="maintenance-title text-center"
+ >
+ login.login_to_sonarqube
+ </h1>
+ <form
+ onSubmit={[Function]}
+ >
+ <Connect(GlobalMessages) />
+ <div
+ className="big-spacer-bottom"
+ >
+ <label
+ className="login-label"
+ htmlFor="login"
+ >
+ login
+ </label>
+ <input
+ autoFocus={true}
+ className="login-input"
+ id="login"
+ maxLength="255"
+ name="login"
+ onChange={[Function]}
+ placeholder="login"
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <div
+ className="big-spacer-bottom"
+ >
+ <label
+ className="login-label"
+ htmlFor="password"
+ >
+ password
+ </label>
+ <input
+ className="login-input"
+ id="password"
+ name="password"
+ onChange={[Function]}
+ placeholder="password"
+ required={true}
+ type="password"
+ value=""
+ />
+ </div>
+ <div>
+ <div
+ className="text-right overflow-hidden"
+ >
+ <button
+ name="commit"
+ type="submit"
+ >
+ sessions.log_in
+ </button>
+ <a
+ className="spacer-left"
+ href="/"
+ >
+ cancel
+ </a>
+ </div>
+ </div>
+ </form>
+</div>
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { Route, Redirect } from 'react-router';
-import LoginFormContainer from './components/LoginFormContainer';
-import Logout from './components/Logout';
-import Unauthorized from './components/Unauthorized';
-
-export default [
- <Redirect key="login" from="/sessions/login" to="/sessions/new" />,
- <Route key="new" path="new" component={LoginFormContainer} />,
- <Route key="logout" path="logout" component={Logout} />,
- <Route key="unauthorized" path="unauthorized" component={Unauthorized} />
+const routes = [
+ {
+ path: 'new',
+ getComponent(_, callback) {
+ require.ensure([], require => {
+ callback(null, require('./components/LoginFormContainer').default);
+ });
+ }
+ },
+ {
+ path: 'logout',
+ getComponent(_, callback) {
+ require.ensure([], require => {
+ callback(null, require('./components/Logout').default);
+ });
+ }
+ },
+ {
+ path: 'unauthorized',
+ getComponent(_, callback) {
+ require.ensure([], require => {
+ callback(null, require('./components/Unauthorized').default);
+ });
+ }
+ }
];
+
+export default routes;
}
}
+.navbar-global-login-github {
+ margin-top: 3px;
+ margin-right: 4px;
+}
+
.navbar-context {
position: static;
}
.oauth-providers {
- margin-bottom: 30px;
- border-bottom: 1px solid @barBorderColor;
-
& > ul {
display: flex;
justify-content: space-around;
}
}
}
+
+.oauth-providers + form {
+ padding-top: 30px;
+ border-top: 1px solid @barBorderColor;
+}
user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts
user.password_cant_be_changed_on_external_auth=Password cannot be changed when external authentication is used
+login.login_to_sonarqube=Log In to SonarQube
+login.more_options=More options
+
#------------------------------------------------------------------------------
#
# USERS PAGE