]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9565 Move the Quality Gates link to organization level
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 20 Jul 2017 11:27:32 +0000 (13:27 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 25 Jul 2017 07:20:30 +0000 (09:20 +0200)
21 files changed:
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap
server/sonar-web/src/main/js/apps/organizations/routes.js
server/sonar-web/src/main/js/apps/overview/meta/Meta.js
server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js
server/sonar-web/src/main/js/apps/quality-gates/components/Details.js
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js
server/sonar-web/src/main/js/apps/quality-gates/components/List.js
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
server/sonar-web/src/main/js/helpers/__tests__/urls-test.js
server/sonar-web/src/main/js/helpers/urls.js
tests/src/test/java/org/sonarqube/pageobjects/Navigation.java
tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java
tests/src/test/java/org/sonarqube/pageobjects/QualityGatePage.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/qualityGate/OrganizationQualityGateUiTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java

index 6535201176f21c0c952860b944248c40309e4400..7d7437238bf65b919ce3fd669fc51d93a26c6bf8 100644 (file)
@@ -20,6 +20,7 @@
 import React from 'react';
 import { Link } from 'react-router';
 import { translate } from '../../../../helpers/l10n';
+import { getQualityGatesUrl } from '../../../../helpers/urls';
 import { isMySet } from '../../../../apps/issues/utils';
 
 export default class GlobalNavMenu extends React.PureComponent {
@@ -98,7 +99,7 @@ export default class GlobalNavMenu extends React.PureComponent {
   renderQualityGatesLink() {
     return (
       <li>
-        <Link to="/quality_gates" activeClassName="active">
+        <Link to={getQualityGatesUrl()} activeClassName="active">
           {translate('quality_gates.page')}
         </Link>
       </li>
@@ -158,7 +159,7 @@ export default class GlobalNavMenu extends React.PureComponent {
         {this.renderIssuesLink()}
         {!organizationsEnabled && this.renderRulesLink()}
         {!organizationsEnabled && this.renderProfilesLink()}
-        {this.renderQualityGatesLink()}
+        {!organizationsEnabled && this.renderQualityGatesLink()}
         {this.renderAdministrationLink()}
         {this.renderMore()}
       </ul>
index 3315afa83e213930824a32a4163178dbca2d36cd..f3541ea9d6d19b773a87fc422ca21d20a9b28bf8 100644 (file)
@@ -55,7 +55,11 @@ exports[`should show administration menu if the user has the rights 1`] = `
       activeClassName="active"
       onlyActiveOnIndex={false}
       style={Object {}}
-      to="/quality_gates"
+      to={
+        Object {
+          "pathname": "/quality_gates",
+        }
+      }
     >
       quality_gates.page
     </Link>
@@ -129,7 +133,11 @@ exports[`should work with extensions 1`] = `
       activeClassName="active"
       onlyActiveOnIndex={false}
       style={Object {}}
-      to="/quality_gates"
+      to={
+        Object {
+          "pathname": "/quality_gates",
+        }
+      }
     >
       quality_gates.page
     </Link>
index 0d8939752c6c4f00c93f4643932a70f60c8736d8..be4670195a13f3d5faa264f763dc975f45e9f189 100644 (file)
@@ -26,6 +26,7 @@ import ContextNavBar from '../../../components/nav/ContextNavBar';
 import NavBarTabs from '../../../components/nav/NavBarTabs';
 import OrganizationIcon from '../../../components/icons-components/OrganizationIcon';
 import { isMySet } from '../../issues/utils';
+import { getQualityGatesUrl } from '../../../helpers/urls';
 import type { Organization } from '../../../store/organizations/duck';
 
 const ADMIN_PATHS = [
@@ -228,6 +229,11 @@ export default class OrganizationNavigation extends React.PureComponent {
               {translate('coding_rules.page')}
             </Link>
           </li>
+          <li>
+            <Link to={getQualityGatesUrl(organization.key)} activeClassName="active">
+              {translate('quality_gates.page')}
+            </Link>
+          </li>
           {this.renderExtensions(moreActive)}
           {organization.canAdmin && this.renderAdministration(adminActive)}
         </NavBarTabs>
index 389fae8e92fabc8916c3c9271c16e543f97f5a82..e3acf365788aff90e7ab699340ccf00574a9d800 100644 (file)
@@ -87,6 +87,20 @@ exports[`admin 1`] = `
         coding_rules.page
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/quality_gates",
+          }
+        }
+      >
+        quality_gates.page
+      </Link>
+    </li>
     <li
       className="dropdown"
     >
@@ -257,6 +271,20 @@ exports[`regular user 1`] = `
         coding_rules.page
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/quality_gates",
+          }
+        }
+      >
+        quality_gates.page
+      </Link>
+    </li>
   </NavBarTabs>
 </ContextNavBar>
 `;
@@ -348,6 +376,20 @@ exports[`undeletable org 1`] = `
         coding_rules.page
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to={
+          Object {
+            "pathname": "/organizations/foo/quality_gates",
+          }
+        }
+      >
+        quality_gates.page
+      </Link>
+    </li>
     <li
       className="dropdown"
     >
index 90c22b3270b2bc25aaa64ffaa4345239c5dbac0d..ecdd76eb7e166d6df382083166d843e2a7680ebd 100644 (file)
@@ -31,6 +31,7 @@ import OrganizationPermissions from './components/OrganizationPermissions';
 import OrganizationPermissionTemplates from './components/OrganizationPermissionTemplates';
 import OrganizationProjectsManagement from './components/OrganizationProjectsManagement';
 import OrganizationDelete from './components/OrganizationDelete';
+import qualityGatesRoutes from '../quality-gates/routes';
 import qualityProfilesRoutes from '../quality-profiles/routes';
 import issuesRoutes from '../issues/routes';
 
@@ -79,6 +80,11 @@ const routes = [
         path: 'quality_profiles',
         childRoutes: qualityProfilesRoutes
       },
+      {
+        path: 'quality_gates',
+        component: OrganizationContainer,
+        childRoutes: qualityGatesRoutes
+      },
       {
         path: 'extension/:pluginKey/:extensionKey',
         component: OrganizationPageExtension
index 1a57590524f605c3cb06257d5ed445f378854ee3..4d84a3f2e234ddf872680f9e7611f84dbf6d32bd 100644 (file)
@@ -43,7 +43,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
 
   const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles;
   const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate;
-  const shouldShowOrganizationKey = component.organization != null && areThereCustomOrganizations;
+  const hasOrganization = component.organization != null && areThereCustomOrganizations;
 
   return (
     <div className="overview-meta">
@@ -58,7 +58,11 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
 
       {isProject && <AnalysesList project={component.key} history={history} router={router} />}
 
-      {shouldShowQualityGate && <MetaQualityGate gate={qualityGate} />}
+      {shouldShowQualityGate &&
+        <MetaQualityGate
+          gate={qualityGate}
+          organization={hasOrganization && component.organization}
+        />}
 
       {shouldShowQualityProfiles &&
         <MetaQualityProfiles
@@ -71,7 +75,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
 
       <MetaKey component={component} />
 
-      {shouldShowOrganizationKey && <MetaOrganizationKey component={component} />}
+      {hasOrganization && <MetaOrganizationKey component={component} />}
     </div>
   );
 };
index 3bdda5734d10faf997b92d3c54d8f7c5aa37e100..f9c3a1db5f6ffdeb8930208a212705b30da06114 100644 (file)
@@ -22,7 +22,7 @@ import { Link } from 'react-router';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
 
-const MetaQualityGate = ({ gate }) => {
+const MetaQualityGate = ({ gate, organization }) => {
   return (
     <div className="overview-meta-card">
       <h4 className="overview-meta-header">
@@ -35,7 +35,7 @@ const MetaQualityGate = ({ gate }) => {
             <span className="note spacer-right">
               {'(' + translate('default') + ')'}
             </span>}
-          <Link to={getQualityGateUrl(gate.key)}>
+          <Link to={getQualityGateUrl(gate.key, organization)}>
             {gate.name}
           </Link>
         </li>
index 8fd787e30305e417c13fa7116f2adc4aab11e8f0..8f52c7bbe6947c940aebdb3d3934c5b9ac76a9ad 100644 (file)
@@ -17,7 +17,7 @@
  * 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, { Component } from 'react';
+import React from 'react';
 import Helmet from 'react-helmet';
 import {
   fetchQualityGate,
@@ -29,8 +29,9 @@ import DetailsContent from './DetailsContent';
 import RenameView from '../views/rename-view';
 import CopyView from '../views/copy-view';
 import DeleteView from '../views/delete-view';
+import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls';
 
-export default class Details extends Component {
+export default class Details extends React.PureComponent {
   componentDidMount() {
     this.fetchDetails();
   }
@@ -62,14 +63,14 @@ export default class Details extends Component {
   }
 
   handleCopyClick() {
-    const { qualityGate, onCopy } = this.props;
+    const { qualityGate, onCopy, organization } = this.props;
     const { router } = this.context;
 
     new CopyView({
       qualityGate,
       onCopy: newQualityGate => {
         onCopy(newQualityGate);
-        router.push(`/quality_gates/show/${newQualityGate.id}`);
+        router.push(getQualityGateUrl(newQualityGate.id, organization && organization.key));
       }
     }).render();
   }
@@ -85,14 +86,13 @@ export default class Details extends Component {
   }
 
   handleDeleteClick() {
-    const { qualityGate, onDelete } = this.props;
+    const { qualityGate, onDelete, organization } = this.props;
     const { router } = this.context;
-
     new DeleteView({
       qualityGate,
       onDelete: qualityGate => {
         onDelete(qualityGate);
-        router.replace('/quality_gates');
+        router.replace(getQualityGatesUrl(organization && organization.key));
       }
     }).render();
   }
@@ -115,6 +115,7 @@ export default class Details extends Component {
           onCopy={this.handleCopyClick.bind(this)}
           onSetAsDefault={this.handleSetAsDefaultClick.bind(this)}
           onDelete={this.handleDeleteClick.bind(this)}
+          organization={this.props.organization}
         />
 
         <DetailsContent
index 3613be4e567441586e394cceaa7f203d4246d2fa..5c826c2c30227b9dd6fc26e278f055d631ed3f96 100644 (file)
  * 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, { Component } from 'react';
+import React from 'react';
 import Conditions from './Conditions';
 import Projects from './Projects';
 import { translate } from '../../../helpers/l10n';
 
-export default class DetailsContent extends Component {
+export default class DetailsContent extends React.PureComponent {
   render() {
     const { gate, canEdit, metrics } = this.props;
     const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props;
index 4d518e83238b48a93c4f86375a1a12802ba3864c..2f6091184d67daf3841f02bdd0edda38cbb59280 100644 (file)
 import React from 'react';
 import { translate } from '../../../helpers/l10n';
 
-export default function DetailsHeader({
-  qualityGate,
-  edit,
-  onRename,
-  onCopy,
-  onSetAsDefault,
-  onDelete
-}) {
-  function handleRenameClick(e) {
+export default class DetailsHeader extends React.PureComponent {
+  handleRenameClick = e => {
     e.preventDefault();
-    onRename();
-  }
+    this.props.onRename();
+  };
 
-  function handleCopyClick(e) {
+  handleCopyClick = e => {
     e.preventDefault();
-    onCopy();
-  }
+    this.props.onCopy();
+  };
 
-  function handleSetAsDefaultClick(e) {
+  handleSetAsDefaultClick = e => {
     e.preventDefault();
-    onSetAsDefault();
-  }
+    this.props.onSetAsDefault();
+  };
 
-  function handleDeleteClick(e) {
+  handleDeleteClick = e => {
     e.preventDefault();
-    onDelete();
-  }
+    this.props.onDelete();
+  };
+
+  render() {
+    const { qualityGate, edit } = this.props;
 
-  return (
-    <div className="layout-page-header-panel layout-page-main-header issues-main-header">
-      <div className="layout-page-header-panel-inner layout-page-main-header-inner">
-        <div className="layout-page-main-inner">
-          <h2 className="pull-left">
-            {qualityGate.name}
-          </h2>
-          {edit &&
-            <div className="pull-right">
-              <div className="button-group">
-                <button id="quality-gate-rename" onClick={handleRenameClick}>
-                  {translate('rename')}
-                </button>
-                <button id="quality-gate-copy" onClick={handleCopyClick}>
-                  {translate('copy')}
-                </button>
-                <button id="quality-gate-toggle-default" onClick={handleSetAsDefaultClick}>
-                  {qualityGate.isDefault
-                    ? translate('unset_as_default')
-                    : translate('set_as_default')}
-                </button>
-                <button id="quality-gate-delete" className="button-red" onClick={handleDeleteClick}>
-                  {translate('delete')}
-                </button>
-              </div>
-            </div>}
+    return (
+      <div className="layout-page-header-panel layout-page-main-header issues-main-header">
+        <div className="layout-page-header-panel-inner layout-page-main-header-inner">
+          <div className="layout-page-main-inner">
+            <h2 className="pull-left">
+              {qualityGate.name}
+            </h2>
+            {edit &&
+              <div className="pull-right">
+                <div className="button-group">
+                  <button id="quality-gate-rename" onClick={this.handleRenameClick}>
+                    {translate('rename')}
+                  </button>
+                  <button id="quality-gate-copy" onClick={this.handleCopyClick}>
+                    {translate('copy')}
+                  </button>
+                  <button id="quality-gate-toggle-default" onClick={this.handleSetAsDefaultClick}>
+                    {qualityGate.isDefault
+                      ? translate('unset_as_default')
+                      : translate('set_as_default')}
+                  </button>
+                  <button
+                    id="quality-gate-delete"
+                    className="button-red"
+                    onClick={this.handleDeleteClick}>
+                    {translate('delete')}
+                  </button>
+                </div>
+              </div>}
+          </div>
         </div>
       </div>
-    </div>
-  );
+    );
+  }
 }
index 9729392a1b8705e20db9f9ebddecfc8643683710..b6a4d3a8c232c66b05e821d37e69fcfcb7a4ab3e 100644 (file)
 import React from 'react';
 import { Link } from 'react-router';
 import { translate } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
 
-export default function List({ qualityGates }) {
+export default function List({ organization, qualityGates }) {
   return (
     <div className="list-group">
       {qualityGates.map(qualityGate =>
         <Link
           key={qualityGate.id}
-          to={`/quality_gates/show/${qualityGate.id}`}
+          to={getQualityGateUrl(qualityGate.id, organization && organization.key)}
           activeClassName="active"
           className="list-group-item"
           data-id={qualityGate.id}>
index a5c96cd79678d7125e6cfd9a292e826250eb0650..33a9c447d1319db31754c100bc86c8ab28530e79 100644 (file)
  * 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, { Component } from 'react';
+import React from 'react';
 import ProjectsView from '../views/gate-projects-view';
 
-export default class Projects extends Component {
+export default class Projects extends React.PureComponent {
   componentDidMount() {
     this.renderView();
   }
index ff9d95d7bd7db5029082f8dfe26fc874bd2c834e..3ac0da688ccc5ab70e229536be5233883684acaf 100644 (file)
@@ -26,9 +26,14 @@ import {
   fetchQualityGates as fetchQualityGatesAPI
 } from '../../../api/quality-gates';
 import { translate } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
 import '../styles.css';
 
 export default class QualityGatesApp extends Component {
+  static contextTypes = {
+    router: React.PropTypes.object.isRequired
+  };
+
   state = {};
 
   componentDidMount() {
@@ -45,37 +50,35 @@ export default class QualityGatesApp extends Component {
   }
 
   handleAdd(qualityGate) {
-    const { addQualityGate } = this.props;
+    const { addQualityGate, organization } = this.props;
     const { router } = this.context;
 
     addQualityGate(qualityGate);
-    router.push(`/quality_gates/show/${qualityGate.id}`);
+    router.push(getQualityGateUrl(qualityGate.id, organization && organization.key));
   }
 
   render() {
-    const { children, qualityGates, edit } = this.props;
+    const { children, qualityGates, edit, organization } = this.props;
     const defaultTitle = translate('quality_gates.page');
+    const top = organization ? 95 : 30;
     return (
       <div className="layout-page">
         <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
 
         <div className="layout-page-side-outer">
-          <div className="layout-page-side" style={{ top: 30 }}>
+          <div className="layout-page-side" style={{ top }}>
             <div className="layout-page-side-inner">
               <div className="layout-page-filters">
                 <ListHeader canEdit={edit} onAdd={this.handleAdd.bind(this)} />
-                {qualityGates && <List qualityGates={qualityGates} />}
+                {qualityGates && <List organization={organization} qualityGates={qualityGates} />}
               </div>
             </div>
           </div>
         </div>
 
-        {!!qualityGates && children}
+        {qualityGates != null &&
+          React.Children.map(children, child => React.cloneElement(child, { organization }))}
       </div>
     );
   }
 }
-
-QualityGatesApp.contextTypes = {
-  router: React.PropTypes.object.isRequired
-};
index c2047a572a488cce6e14c60e99fcbb25331d34f6..0f5fb66e39a554cb159dcda3a905a18c414d73f2 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { getComponentUrl, getComponentIssuesUrl, getComponentDrilldownUrl } from '../urls';
+import {
+  getComponentUrl,
+  getComponentIssuesUrl,
+  getComponentDrilldownUrl,
+  getQualityGatesUrl,
+  getQualityGateUrl
+} from '../urls';
 
 const SIMPLE_COMPONENT_KEY = 'sonarqube';
 const COMPLEX_COMPONENT_KEY = 'org.sonarsource.sonarqube:sonarqube';
@@ -77,10 +83,30 @@ describe('#getComponentDrilldownUrl', () => {
     });
   });
 
-  it('should encode component key', () => {
+  it('should not encode component key', () => {
     expect(getComponentDrilldownUrl(COMPLEX_COMPONENT_KEY, METRIC)).toEqual({
       pathname: '/component_measures/metric/' + METRIC,
       query: { id: COMPLEX_COMPONENT_KEY }
     });
   });
 });
+
+describe('#getQualityGate(s)Url', () => {
+  it('should take organization key into account', () => {
+    expect(getQualityGatesUrl()).toEqual({ pathname: '/quality_gates' });
+    expect(getQualityGatesUrl('foo')).toEqual({ pathname: '/organizations/foo/quality_gates' });
+    expect(getQualityGateUrl('bar')).toEqual({ pathname: '/quality_gates/show/bar' });
+    expect(getQualityGateUrl('bar', 'foo')).toEqual({
+      pathname: '/organizations/foo/quality_gates/show/bar'
+    });
+  });
+
+  it('should encode keys', () => {
+    expect(getQualityGatesUrl(COMPLEX_COMPONENT_KEY)).toEqual({
+      pathname: '/organizations/' + COMPLEX_COMPONENT_KEY_ENCODED + '/quality_gates'
+    });
+    expect(getQualityGateUrl(COMPLEX_COMPONENT_KEY)).toEqual({
+      pathname: '/quality_gates/show/' + COMPLEX_COMPONENT_KEY_ENCODED
+    });
+  });
+});
index 07103e12255008060eba77ab576f622fbc8d3243..27c2ec6ee6ce22006bda5fc3e497d6d19d3e0111 100644 (file)
@@ -100,16 +100,14 @@ export function getQualityProfileUrl(name, language, organization) {
   return getProfilePath(name, language, organization);
 }
 
-/**
- * Generate URL for a quality gate
- * @param {string} key
- * @returns {Object}
- */
-export function getQualityGateUrl(key) {
-  return {
-    pathname: '/quality_gates/show/' + encodeURIComponent(key)
-  };
-}
+export const getQualityGateUrl = (key: string, organization?: string) => ({
+  pathname: getQualityGatesUrl(organization).pathname + '/show/' + encodeURIComponent(key)
+});
+
+export const getQualityGatesUrl = (organization?: string) => ({
+  pathname:
+    (organization ? '/organizations/' + encodeURIComponent(organization) : '') + '/quality_gates'
+});
 
 /**
  * Generate URL for the rules page
index 6bdf9be6156a39341578c9dc0cc60ba98a8efb0d..b71307e15870bc61ad2fbaf42360930f4675cbec 100644 (file)
@@ -31,7 +31,6 @@ import javax.annotation.Nullable;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.html5.WebStorage;
-import org.sonarqube.pageobjects.issues.Issue;
 import org.sonarqube.tests.Tester;
 import org.sonarqube.pageobjects.issues.IssuesPage;
 import org.sonarqube.pageobjects.licenses.LicensesPage;
@@ -109,6 +108,16 @@ public class Navigation {
     return open(url, ProjectLinksPage.class);
   }
 
+  public QualityGatePage openQualityGates() {
+    String url = "/quality_gates";
+    return open(url, QualityGatePage.class);
+  }
+
+  public QualityGatePage openQualityGates(String organization) {
+    String url = "/organizations/" + organization + "/quality_gates";
+    return open(url, QualityGatePage.class);
+  }
+
   public ProjectQualityGatePage openProjectQualityGate(String projectKey) {
     // TODO encode projectKey
     String url = "/project/quality_gate?id=" + projectKey;
index 60969c43fa5de913a9c7a9080230295604f4f3a6..673e037c1937edf6b937b82628b5523355c94bed 100644 (file)
@@ -22,6 +22,7 @@ package org.sonarqube.pageobjects;
 import com.codeborne.selenide.ElementsCollection;
 import com.codeborne.selenide.SelenideElement;
 import java.util.Arrays;
+import org.openqa.selenium.By;
 
 import static com.codeborne.selenide.Condition.exist;
 import static com.codeborne.selenide.Condition.hasText;
@@ -29,6 +30,7 @@ import static com.codeborne.selenide.Condition.text;
 import static com.codeborne.selenide.Condition.visible;
 import static com.codeborne.selenide.Selenide.$;
 import static com.codeborne.selenide.Selenide.$$;
+import static org.assertj.core.api.Assertions.assertThat;
 
 public class ProjectDashboardPage {
 
@@ -97,4 +99,11 @@ public class ProjectDashboardPage {
     tagsInput.sendKeys(charSequences);
     return this;
   }
+
+  public ProjectDashboardPage hasQualityGateLink(String name, String link) {
+    SelenideElement elem = $(".overview-meta-header").should(exist)
+      .parent().find(By.linkText(name)).should(exist);
+    assertThat(elem.attr("href")).endsWith(link);
+    return this;
+  }
 }
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/QualityGatePage.java b/tests/src/test/java/org/sonarqube/pageobjects/QualityGatePage.java
new file mode 100644 (file)
index 0000000..3d7162f
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.sonarqube.pageobjects;
+
+import com.codeborne.selenide.Condition;
+
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class QualityGatePage {
+  public QualityGatePage() {
+    $(".quality-gates-results").shouldBe(Condition.visible);
+  }
+
+  public QualityGatePage countQualityGates(Integer count) {
+    $$(".quality-gates-results .list-group-item").shouldHaveSize(count);
+    return this;
+  }
+
+  public QualityGatePage canCreateQG() {
+    $("#quality-gate-add").should(Condition.exist).shouldBe(Condition.visible);
+    return this;
+  }
+
+  public QualityGatePage canNotCreateQG() {
+    $("#quality-gate-add").shouldNot(Condition.exist);
+    return this;
+  }
+}
index 69ea5870d2557153a669149c9be72bfdcd76adf6..206b264c24c72adaf1ec2abeef5032c7f55932d7 100644 (file)
@@ -40,6 +40,7 @@ import org.sonarqube.tests.projectAdministration.ProjectKeyUpdateTest;
 import org.sonarqube.tests.projectAdministration.ProjectProvisioningTest;
 import org.sonarqube.tests.projectSearch.LeakProjectsPageTest;
 import org.sonarqube.tests.projectSearch.SearchProjectsTest;
+import org.sonarqube.tests.qualityGate.OrganizationQualityGateUiTest;
 import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesTest;
 import org.sonarqube.tests.qualityProfile.CustomQualityProfilesTest;
 import org.sonarqube.tests.qualityProfile.OrganizationQualityProfilesUiTest;
@@ -61,6 +62,7 @@ import static util.ItUtils.xooPlugin;
   OrganizationIssuesPageTest.class,
   OrganizationMembershipTest.class,
   OrganizationMembershipUiTest.class,
+  OrganizationQualityGateUiTest.class,
   OrganizationQualityProfilesUiTest.class,
   OrganizationTest.class,
   RootUserOnOrganizationTest.class,
diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/OrganizationQualityGateUiTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/OrganizationQualityGateUiTest.java
new file mode 100644 (file)
index 0000000..01ab118
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+package org.sonarqube.tests.qualityGate;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.sonarqube.pageobjects.ProjectDashboardPage;
+import org.sonarqube.pageobjects.QualityGatePage;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsUsers;
+import util.issue.IssueRule;
+
+import static com.codeborne.selenide.Selenide.$;
+import static util.ItUtils.restoreProfile;
+import static util.ItUtils.runProjectAnalysis;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OrganizationQualityGateUiTest {
+  @ClassRule
+  public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Rule
+  public IssueRule issueRule = IssueRule.from(orchestrator);
+
+  private Organizations.Organization organization;
+  private WsUsers.CreateWsResponse.User user;
+
+  @Before
+  public void setUp() throws Exception {
+    organization = tester.organizations().generate();
+    user = tester.users().generate();
+    tester.organizations().addMember(organization, user);
+    restoreProfile(orchestrator, getClass().getResource("/issue/with-many-rules.xml"), organization.getKey());
+  }
+
+  @Test
+  public void should_have_a_link_to_quality_gates() {
+    tester.openBrowser()
+      .logIn().submitCredentials(user.getLogin())
+      .openQualityGates(organization.getKey());
+
+    SelenideElement element = $(".navbar-context .navbar-nav")
+      .find(By.linkText("Quality Gates"))
+      .should(Condition.exist);
+    assertThat(element.attr("href")).endsWith("/organizations/" + organization.getKey() + "/quality_gates");
+  }
+
+  @Test
+  public void should_display_available_quality_gates() {
+    QualityGatePage page = tester.openBrowser()
+      .logIn().submitCredentials(user.getLogin())
+      .openQualityGates(organization.getKey());
+    page.countQualityGates(1);
+  }
+
+  @Test
+  public void should_not_allow_random_user_to_create() {
+    tester.openBrowser()
+      .logIn().submitCredentials(user.getLogin())
+      .openQualityGates(organization.getKey())
+      .canNotCreateQG();
+    tester.openBrowser()
+      .logIn().submitCredentials("admin")
+      .openQualityGates(organization.getKey())
+      .canCreateQG();
+  }
+
+  @Test
+  public void quality_gate_link_on_project_dashboard_should_have_organization_context() {
+    String project = tester.projects().generate(organization).getKey();
+    runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample",
+      "sonar.projectKey", project,
+      "sonar.organization", organization.getKey(),
+      "sonar.login", "admin",
+      "sonar.password", "admin",
+      "sonar.scm.disabled", "false",
+      "sonar.scm.provider", "xoo");
+
+    String link = "/organizations/" + organization.getKey() + "/quality_gates/show/1";
+    ProjectDashboardPage page = tester.openBrowser()
+      .logIn().submitCredentials(user.getLogin())
+      .openProjectDashboard(project);
+    page.hasQualityGateLink("SonarQube way", link);
+  }
+}
index 4404dc7ca802c71aef2122df227cef11dd582054..acb207f52715ed40cc88f2e64122c5f85a6aa989 100644 (file)
  */
 package org.sonarqube.tests.qualityGate;
 
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.build.SonarScanner;
+import org.junit.Rule;
+import org.openqa.selenium.By;
 import org.sonarqube.tests.Category1Suite;
 import java.util.Date;
 import javax.annotation.Nullable;
@@ -37,8 +41,11 @@ import org.sonar.wsclient.qualitygate.QualityGateCondition;
 import org.sonar.wsclient.qualitygate.UpdateCondition;
 import org.sonarqube.pageobjects.Navigation;
 import org.sonarqube.pageobjects.ProjectActivityPage;
+import org.sonarqube.tests.Tester;
 
+import static com.codeborne.selenide.Selenide.$;
 import static org.apache.commons.lang.time.DateUtils.addDays;
+import static org.assertj.core.api.Assertions.assertThat;
 import static util.ItUtils.projectDir;
 import static util.ItUtils.resetPeriod;
 import static util.ItUtils.setServerProperty;
@@ -49,6 +56,9 @@ public class QualityGateUiTest {
   @ClassRule
   public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
 
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
   private static long DEFAULT_QUALITY_GATE;
 
   @BeforeClass
@@ -102,6 +112,32 @@ public class QualityGateUiTest {
     runSelenese(orchestrator, "/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html");
   }
 
+  @Test
+  public void should_have_a_global_link_to_quality_gates() {
+    String login = tester.users().generate().getLogin();
+    tester.openBrowser()
+      .logIn().submitCredentials(login)
+      .openQualityGates();
+
+    SelenideElement element = $(".navbar-global .navbar-nav")
+      .find(By.linkText("Quality Gates"))
+      .should(Condition.exist);
+    assertThat(element.attr("href")).endsWith("/quality_gates");
+  }
+
+  @Test
+  public void should_not_allow_random_user_to_create() {
+    String login = tester.users().generate().getLogin();
+    tester.openBrowser()
+      .logIn().submitCredentials(login)
+      .openQualityGates()
+      .canNotCreateQG();
+    tester.openBrowser()
+      .logIn().submitCredentials("admin")
+      .openQualityGates()
+      .canCreateQG();
+  }
+
   private void scanSampleWithDate(String date) {
     scanSample(date, null);
   }