]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20220 Show warnings for non compliant Quality Gate only when user can edit it
authorZipeng WU <zipeng.wu@sonarsource.com>
Tue, 22 Aug 2023 15:49:26 +0000 (17:49 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 30 Aug 2023 20:03:06 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx

index 3259ef0f2393451c97e627c4b08b413dfe758ac0..74bc38d4a1f56c9f0bb7dba9c404b259f4cf6e5f 100644 (file)
  */
 import { cloneDeep, flatten, omit, remove } from 'lodash';
 import { Project } from '../../apps/quality-gates/components/Projects';
-import { mockQualityGate } from '../../helpers/mocks/quality-gates';
+import {
+  mockQualityGate,
+  mockQualityGateApplicationStatus,
+  mockQualityGateProjectStatus,
+} from '../../helpers/mocks/quality-gates';
 import { mockUserBase } from '../../helpers/mocks/users';
 import { mockCondition, mockGroup } from '../../helpers/testMocks';
 import { MetricKey } from '../../types/metrics';
+import {
+  QualityGateApplicationStatus,
+  QualityGateProjectStatus,
+  SearchPermissionsParameters,
+} from '../../types/quality-gates';
 import { CaycStatus, Condition, QualityGate } from '../../types/types';
 import {
   addGroup,
@@ -36,7 +45,9 @@ import {
   dissociateGateWithProject,
   fetchQualityGate,
   fetchQualityGates,
+  getApplicationQualityGate,
   getGateForProject,
+  getQualityGateProjectStatus,
   renameQualityGate,
   searchGroups,
   searchProjects,
@@ -45,6 +56,8 @@ import {
   updateCondition,
 } from '../quality-gates';
 
+jest.mock('../quality-gates');
+
 export class QualityGatesServiceMock {
   isAdmin = false;
   readOnlyList: QualityGate[];
@@ -52,6 +65,8 @@ export class QualityGatesServiceMock {
   projects: Project[];
   getGateForProjectGateName: string;
   throwOnGetGateForProject: boolean;
+  qualityGateProjectStatus: QualityGateProjectStatus;
+  applicationQualityGate: QualityGateApplicationStatus;
 
   constructor(list?: QualityGate[]) {
     this.readOnlyList = list || [
@@ -198,22 +213,27 @@ export class QualityGatesServiceMock {
     this.getGateForProjectGateName = 'SonarSource way';
     this.throwOnGetGateForProject = false;
 
-    (fetchQualityGate as jest.Mock).mockImplementation(this.showHandler);
-    (fetchQualityGates as jest.Mock).mockImplementation(this.listHandler);
-    (createQualityGate as jest.Mock).mockImplementation(this.createHandler);
-    (deleteQualityGate as jest.Mock).mockImplementation(this.destroyHandler);
-    (copyQualityGate as jest.Mock).mockImplementation(this.copyHandler);
+    jest.mocked(fetchQualityGate).mockImplementation(this.showHandler);
+    jest.mocked(fetchQualityGates).mockImplementation(this.listHandler);
+    jest.mocked(createQualityGate).mockImplementation(this.createHandler);
+    jest.mocked(deleteQualityGate).mockImplementation(this.destroyHandler);
+    jest.mocked(copyQualityGate).mockImplementation(this.copyHandler);
     (renameQualityGate as jest.Mock).mockImplementation(this.renameHandler);
-    (createCondition as jest.Mock).mockImplementation(this.createConditionHandler);
-    (updateCondition as jest.Mock).mockImplementation(this.updateConditionHandler);
-    (deleteCondition as jest.Mock).mockImplementation(this.deleteConditionHandler);
-    (searchProjects as jest.Mock).mockImplementation(this.searchProjectsHandler);
-    (searchUsers as jest.Mock).mockImplementation(this.searchUsersHandler);
-    (searchGroups as jest.Mock).mockImplementation(this.searchGroupsHandler);
-    (associateGateWithProject as jest.Mock).mockImplementation(this.selectHandler);
-    (dissociateGateWithProject as jest.Mock).mockImplementation(this.deSelectHandler);
-    (setQualityGateAsDefault as jest.Mock).mockImplementation(this.setDefaultHandler);
+    jest.mocked(createCondition).mockImplementation(this.createConditionHandler);
+    jest.mocked(updateCondition).mockImplementation(this.updateConditionHandler);
+    jest.mocked(deleteCondition).mockImplementation(this.deleteConditionHandler);
+    jest.mocked(searchProjects).mockImplementation(this.searchProjectsHandler);
+    jest.mocked(searchUsers).mockImplementation(this.searchUsersHandler);
+    jest.mocked(searchGroups).mockImplementation(this.searchGroupsHandler);
+    jest.mocked(associateGateWithProject).mockImplementation(this.selectHandler);
+    jest.mocked(dissociateGateWithProject).mockImplementation(this.deSelectHandler);
+    jest.mocked(setQualityGateAsDefault).mockImplementation(this.setDefaultHandler);
     (getGateForProject as jest.Mock).mockImplementation(this.projectGateHandler);
+    jest.mocked(getQualityGateProjectStatus).mockImplementation(this.handleQualityGetProjectStatus);
+    jest.mocked(getApplicationQualityGate).mockImplementation(this.handleGetApplicationQualityGate);
+
+    this.qualityGateProjectStatus = mockQualityGateProjectStatus({});
+    this.applicationQualityGate = mockQualityGateApplicationStatus({});
 
     // To be implemented.
     (addUser as jest.Mock).mockResolvedValue({});
@@ -416,6 +436,7 @@ export class QualityGatesServiceMock {
     selected,
     query,
   }: {
+    gateName: string;
     selected: string;
     query: string | undefined;
   }) => {
@@ -437,7 +458,7 @@ export class QualityGatesServiceMock {
     return this.reply(response);
   };
 
-  searchUsersHandler = ({ selected }: { selected: string }) => {
+  searchUsersHandler = ({ selected }: SearchPermissionsParameters) => {
     if (selected === 'selected') {
       return this.reply({ users: [] });
     }
@@ -445,7 +466,7 @@ export class QualityGatesServiceMock {
     return this.reply({ users: [mockUserBase()] });
   };
 
-  searchGroupsHandler = ({ selected }: { selected: string }) => {
+  searchGroupsHandler = ({ selected }: SearchPermissionsParameters) => {
     if (selected === 'selected') {
       return this.reply({ groups: [] });
     }
@@ -477,6 +498,22 @@ export class QualityGatesServiceMock {
     return this.reply(this.list.find((qg) => qg.name === this.getGateForProjectGateName));
   };
 
+  handleGetApplicationQualityGate = () => {
+    return this.reply(this.applicationQualityGate);
+  };
+
+  setApplicationQualityGateStatus = (status: QualityGateApplicationStatus) => {
+    this.applicationQualityGate = mockQualityGateApplicationStatus(status);
+  };
+
+  handleQualityGetProjectStatus = () => {
+    return this.reply(this.qualityGateProjectStatus);
+  };
+
+  setQualityGateProjectStatus = (status: QualityGateProjectStatus) => {
+    this.qualityGateProjectStatus = mockQualityGateProjectStatus(status);
+  };
+
   reply<T>(response: T): Promise<T> {
     return Promise.resolve(cloneDeep(response));
   }
index 2a3c7a4bf694453d7f58b31f8bcede8e75e4c723..4f63afdbb615498c248b67d95493053dba54a085 100644 (file)
@@ -81,13 +81,12 @@ export function deleteCondition(data: { id: string }): Promise<void> {
   return post('/api/qualitygates/delete_condition', data);
 }
 
-export function getGateForProject(data: { project: string }): Promise<QualityGate | undefined> {
+export function getGateForProject(data: { project: string }): Promise<QualityGate> {
   return getJSON('/api/qualitygates/get_by_project', data).then(
-    ({ qualityGate }) =>
-      qualityGate && {
-        ...qualityGate,
-        isDefault: qualityGate.default,
-      },
+    ({ qualityGate }) => ({
+      ...qualityGate,
+      isDefault: qualityGate.default,
+    }),
     throwGlobalError
   );
 }
index 8c0cb2c306a886069cfe8ed7eeb46404507d9661..820159a466a321b9b0460a3f5d5d7d87eaddc04b 100644 (file)
@@ -22,7 +22,12 @@ import * as React from 'react';
 import { getApplicationDetails, getApplicationLeak } from '../../../api/application';
 import { getMeasuresWithPeriodAndMetrics } from '../../../api/measures';
 import { getProjectActivity } from '../../../api/projectActivity';
-import { getApplicationQualityGate, getQualityGateProjectStatus } from '../../../api/quality-gates';
+import {
+  fetchQualityGate,
+  getApplicationQualityGate,
+  getGateForProject,
+  getQualityGateProjectStatus,
+} from '../../../api/quality-gates';
 import { getAllTimeMachineData } from '../../../api/time-machine';
 import {
   getActivityGraph,
@@ -47,7 +52,7 @@ import { ComponentQualifier } from '../../../types/component';
 import { MetricKey } from '../../../types/metrics';
 import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
 import { QualityGateStatus, QualityGateStatusCondition } from '../../../types/quality-gates';
-import { Component, MeasureEnhanced, Metric, Period } from '../../../types/types';
+import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
 import '../styles.css';
 import { HISTORY_METRICS_LIST, METRICS } from '../utils';
 import BranchOverviewRenderer from './BranchOverviewRenderer';
@@ -70,6 +75,7 @@ interface State {
   metrics?: Metric[];
   period?: Period;
   qgStatuses?: QualityGateStatus[];
+  qualityGate?: QualityGate;
 }
 
 export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph';
@@ -111,6 +117,7 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
       this.loadApplicationStatus();
     } else {
       this.loadProjectStatus();
+      this.loadProjectQualityGate();
     }
   };
 
@@ -268,6 +275,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
     );
   };
 
+  loadProjectQualityGate = async () => {
+    const { component } = this.props;
+    const qualityGate = await getGateForProject({ project: component.key });
+    const qgDetails = await fetchQualityGate({ name: qualityGate.name });
+    this.setState({ qualityGate: qgDetails });
+  };
+
   loadMeasuresAndMeta = (
     componentKey: string,
     branchLike?: BranchLike,
@@ -409,6 +423,7 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
       metrics,
       period,
       qgStatuses,
+      qualityGate,
     } = this.state;
 
     const projectIsEmpty =
@@ -436,6 +451,7 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
         period={period}
         projectIsEmpty={projectIsEmpty}
         qgStatuses={qgStatuses}
+        qualityGate={qualityGate}
       />
     );
   }
index 9a7654ecceada73985bfe48b9b8d09d47408bf9e..7e41afbe80c489bad3279f6467ff80411b165ecc 100644 (file)
@@ -26,7 +26,7 @@ import { Branch } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
 import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
 import { QualityGateStatus } from '../../../types/quality-gates';
-import { Component, MeasureEnhanced, Metric, Period } from '../../../types/types';
+import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
 import ActivityPanel from './ActivityPanel';
 import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
 import MeasuresPanel from './MeasuresPanel';
@@ -50,6 +50,7 @@ export interface BranchOverviewRendererProps {
   period?: Period;
   projectIsEmpty?: boolean;
   qgStatuses?: QualityGateStatus[];
+  qualityGate?: QualityGate;
 }
 
 export default function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
@@ -70,6 +71,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
     period,
     projectIsEmpty,
     qgStatuses,
+    qualityGate,
   } = props;
 
   const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
@@ -95,6 +97,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
                     component={component}
                     loading={loadingStatus}
                     qgStatuses={qgStatuses}
+                    qualityGate={qualityGate}
                   />
                 </div>
 
index a06e88baa3f98a41291292d9bda86605fa61edcc..0e708a4be2b32fc3400e79680d6305ddebb76677 100644 (file)
@@ -23,7 +23,7 @@ import { flatMap } from 'lodash';
 import * as React from 'react';
 import { ComponentQualifier, isApplication } from '../../../types/component';
 import { QualityGateStatus } from '../../../types/quality-gates';
-import { CaycStatus, Component } from '../../../types/types';
+import { CaycStatus, Component, QualityGate } from '../../../types/types';
 import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
 import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
 import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
@@ -37,10 +37,11 @@ export interface QualityGatePanelProps {
   component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>;
   loading?: boolean;
   qgStatuses?: QualityGateStatus[];
+  qualityGate?: QualityGate;
 }
 
 export function QualityGatePanel(props: QualityGatePanelProps) {
-  const { component, loading, qgStatuses = [] } = props;
+  const { component, loading, qgStatuses = [], qualityGate } = props;
 
   if (qgStatuses === undefined) {
     return null;
@@ -112,6 +113,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
 
       {qgStatuses.length === 1 &&
         qgStatuses[0].caycStatus === CaycStatus.NonCompliant &&
+        qualityGate?.actions?.manageConditions &&
         !isApp && (
           <Card className="sw-mt-4 sw-body-sm">
             <CleanAsYouCodeWarning component={component} />
index ab51129a308555aa68bfa2271bdc592516e597b0..0448c68e0ac146cb44b141051ca5c6e0c5a9353a 100644 (file)
@@ -23,11 +23,9 @@ import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures';
 import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
+import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
 import { getProjectActivity } from '../../../../api/projectActivity';
-import {
-  getApplicationQualityGate,
-  getQualityGateProjectStatus,
-} from '../../../../api/quality-gates';
+import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
 import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils';
 import { isDiffMetric } from '../../../../helpers/measures';
@@ -84,47 +82,6 @@ jest.mock('../../../../api/measures', () => {
   };
 });
 
-jest.mock('../../../../api/quality-gates', () => {
-  const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus } = jest.requireActual(
-    '../../../../helpers/mocks/quality-gates'
-  );
-  const { MetricKey } = jest.requireActual('../../../../types/metrics');
-  return {
-    getQualityGateProjectStatus: jest.fn().mockResolvedValue(
-      mockQualityGateProjectStatus({
-        status: 'ERROR',
-        conditions: [
-          {
-            actualValue: '2',
-            comparator: 'GT',
-            errorThreshold: '1',
-            metricKey: MetricKey.new_reliability_rating,
-            periodIndex: 1,
-            status: 'ERROR',
-          },
-          {
-            actualValue: '5',
-            comparator: 'GT',
-            errorThreshold: '2.0',
-            metricKey: MetricKey.bugs,
-            periodIndex: 0,
-            status: 'ERROR',
-          },
-          {
-            actualValue: '2',
-            comparator: 'GT',
-            errorThreshold: '1.0',
-            metricKey: 'unknown_metric',
-            periodIndex: 0,
-            status: 'ERROR',
-          },
-        ],
-      })
-    ),
-    getApplicationQualityGate: jest.fn().mockResolvedValue(mockQualityGateApplicationStatus()),
-  };
-});
-
 jest.mock('../../../../api/time-machine', () => {
   const { MetricKey } = jest.requireActual('../../../../types/metrics');
   return {
@@ -195,19 +152,58 @@ jest.mock('../../../../components/activity-graph/utils', () => {
 });
 
 const almHandler = new AlmSettingsServiceMock();
-
-beforeEach(jest.clearAllMocks);
+let qualityGatesMock: QualityGatesServiceMock;
+
+beforeAll(() => {
+  qualityGatesMock = new QualityGatesServiceMock();
+  qualityGatesMock.setQualityGateProjectStatus(
+    mockQualityGateProjectStatus({
+      status: 'ERROR',
+      conditions: [
+        {
+          actualValue: '2',
+          comparator: 'GT',
+          errorThreshold: '1',
+          metricKey: MetricKey.new_reliability_rating,
+          periodIndex: 1,
+          status: 'ERROR',
+        },
+        {
+          actualValue: '5',
+          comparator: 'GT',
+          errorThreshold: '2.0',
+          metricKey: MetricKey.bugs,
+          periodIndex: 0,
+          status: 'ERROR',
+        },
+        {
+          actualValue: '2',
+          comparator: 'GT',
+          errorThreshold: '1.0',
+          metricKey: 'unknown_metric',
+          periodIndex: 0,
+          status: 'ERROR',
+        },
+      ],
+    })
+  );
+  qualityGatesMock.setApplicationQualityGateStatus(mockQualityGateApplicationStatus());
+});
 
 afterEach(() => {
+  jest.clearAllMocks();
+  qualityGatesMock.reset();
   almHandler.reset();
 });
 
 describe('project overview', () => {
   it('should show a successful QG', async () => {
     const user = userEvent.setup();
-    jest
-      .mocked(getQualityGateProjectStatus)
-      .mockResolvedValueOnce(mockQualityGateProjectStatus({ status: 'OK' }));
+    qualityGatesMock.setQualityGateProjectStatus(
+      mockQualityGateProjectStatus({
+        status: 'OK',
+      })
+    );
     renderBranchOverview();
 
     // QG panel
@@ -236,10 +232,61 @@ describe('project overview', () => {
     renderBranchOverview();
 
     expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
-    expect(screen.getByText('overview.quality_gate.conditions.cayc.warning')).toBeInTheDocument();
+    expect(
+      screen.queryByText('overview.quality_gate.conditions.cayc.warning')
+    ).not.toBeInTheDocument();
+  });
+
+  it('should show a successful non-compliant QG as admin', async () => {
+    jest
+      .mocked(getQualityGateProjectStatus)
+      .mockResolvedValueOnce(
+        mockQualityGateProjectStatus({ status: 'OK', caycStatus: CaycStatus.NonCompliant })
+      );
+    qualityGatesMock.setIsAdmin(true);
+    qualityGatesMock.setGetGateForProjectName('Non Cayc QG');
+
+    renderBranchOverview();
+
+    await screen.findByText('metric.level.OK');
+    expect(
+      await screen.findByText('overview.quality_gate.conditions.cayc.warning')
+    ).toBeInTheDocument();
   });
 
   it('should show a failed QG', async () => {
+    qualityGatesMock.setQualityGateProjectStatus(
+      mockQualityGateProjectStatus({
+        status: 'ERROR',
+        conditions: [
+          {
+            actualValue: '2',
+            comparator: 'GT',
+            errorThreshold: '1',
+            metricKey: MetricKey.new_reliability_rating,
+            periodIndex: 1,
+            status: 'ERROR',
+          },
+          {
+            actualValue: '5',
+            comparator: 'GT',
+            errorThreshold: '2.0',
+            metricKey: MetricKey.bugs,
+            periodIndex: 0,
+            status: 'ERROR',
+          },
+          {
+            actualValue: '2',
+            comparator: 'GT',
+            errorThreshold: '1.0',
+            metricKey: 'unknown_metric',
+            periodIndex: 0,
+            status: 'ERROR',
+          },
+        ],
+      })
+    );
+
     renderBranchOverview();
 
     expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
@@ -312,7 +359,7 @@ describe('application overview', () => {
         },
       ],
     });
-    jest.mocked(getApplicationQualityGate).mockResolvedValueOnce(appStatus);
+    qualityGatesMock.setApplicationQualityGateStatus(appStatus);
 
     renderBranchOverview({ component });
     expect(
@@ -380,7 +427,7 @@ it('should correctly handle graph type storage', async () => {
 });
 
 function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
-  renderComponent(
+  return renderComponent(
     <CurrentUserContextProvider currentUser={mockLoggedInUser()}>
       <BranchOverview
         branch={mockMainBranch()}
index 2bc8876a107fc7aaf61f9e85c48e3fb3f5105fae..f5327e6480fc3b1b78bbe3e176d80a97d8a12b3d 100644 (file)
@@ -26,8 +26,8 @@ import withAvailableFeatures, {
 import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
 import DocLink from '../../../components/common/DocLink';
 import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
-import { Button } from '../../../components/controls/buttons';
 import ModalButton, { ModalProps } from '../../../components/controls/ModalButton';
+import { Button } from '../../../components/controls/buttons';
 import { Alert } from '../../../components/ui/Alert';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { Feature } from '../../../types/features';
@@ -194,8 +194,8 @@ export class Conditions extends React.PureComponent<Props, State> {
           </Alert>
         )}
 
-        {qualityGate.caycStatus === CaycStatus.NonCompliant && (
-          <Alert className="big-spacer-top big-spacer-bottom" variant="warning">
+        {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
+          <Alert className="big-spacer-top big-spacer-bottom" variant="warning" role="alert">
             <h4 className="spacer-bottom cayc-warning-header">
               {translate('quality_gates.cayc_missing.banner.title')}
             </h4>
index cf246948bbbdfdc41631c3b44a804789eb3fbcf4..ebbfcd3de91c8e0e3a2005e8de9d1ae7abf8ef43 100644 (file)
@@ -63,6 +63,7 @@ export default class Details extends React.PureComponent<Props, State> {
 
   fetchDetails = () => {
     const { qualityGateName } = this.props;
+
     this.setState({ loading: true });
     return fetchQualityGate({ name: qualityGateName }).then(
       (qualityGate) => {
index cca136bfc21d64eac84fcf00c81c592f3e05544d..20ff6b285363ff3c87ba12796c21938ef6585eb4 100644 (file)
@@ -19,9 +19,9 @@
  */
 import * as React from 'react';
 import { setQualityGateAsDefault } from '../../../api/quality-gates';
-import { Button } from '../../../components/controls/buttons';
 import ModalButton from '../../../components/controls/ModalButton';
 import Tooltip from '../../../components/controls/Tooltip';
+import { Button } from '../../../components/controls/buttons';
 import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
 import { translate } from '../../../helpers/l10n';
 import { CaycStatus, QualityGate } from '../../../types/types';
@@ -64,6 +64,7 @@ export default class DetailsHeader extends React.PureComponent<Props> {
   render() {
     const { qualityGate } = this.props;
     const actions = qualityGate.actions || ({} as any);
+    const canEdit = Boolean(actions?.manageConditions);
 
     return (
       <div className="layout-page-header-panel layout-page-main-header issues-main-header">
@@ -72,7 +73,7 @@ export default class DetailsHeader extends React.PureComponent<Props> {
             <div className="pull-left display-flex-center">
               <h2>{qualityGate.name}</h2>
               {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />}
-              {qualityGate.caycStatus === CaycStatus.NonCompliant && (
+              {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
                 <Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}>
                   <AlertWarnIcon className="spacer-left" description={<CaycBadgeTooltip />} />
                 </Tooltip>
index 979aff85c9c97a2b1b09b0caecfa5bd86b6de3a5..4cd78cc3a2616528bc5ffbbefb74f691468eea29 100644 (file)
@@ -49,14 +49,15 @@ export default function List({ qualityGates, currentQualityGate }: Props) {
           )}
           {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />}
 
-          {qualityGate.caycStatus === CaycStatus.NonCompliant && (
-            <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
-              <AlertWarnIcon
-                className="spacer-left"
-                description={translate('quality_gates.cayc.tooltip.message')}
-              />
-            </Tooltip>
-          )}
+          {qualityGate.caycStatus === CaycStatus.NonCompliant &&
+            qualityGate.actions?.manageConditions && (
+              <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
+                <AlertWarnIcon
+                  className="spacer-left"
+                  description={translate('quality_gates.cayc.tooltip.message')}
+                />
+              </Tooltip>
+            )}
         </NavLink>
       ))}
     </div>
index cea9b441406b96ebe2b1ef3a5824e67e2924a925..c786418b0104a39468b483258793eb6cd1c6f059 100644 (file)
@@ -22,12 +22,10 @@ import userEvent from '@testing-library/user-event';
 import selectEvent from 'react-select-event';
 import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
 import { searchProjects, searchUsers } from '../../../../api/quality-gates';
-import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils';
+import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
 import { Feature } from '../../../../types/features';
 import routes from '../../routes';
 
-jest.mock('../../../../api/quality-gates');
-
 let handler: QualityGatesServiceMock;
 
 beforeAll(() => {
@@ -346,6 +344,33 @@ it('should show warning banner when CAYC condition is not properly set and shoul
   expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument();
 });
 
+it('should not warn user when quality gate is not CAYC compliant and user has no permission to edit it', async () => {
+  const user = userEvent.setup();
+  renderQualityGateApp();
+
+  const nonCompliantQualityGate = await screen.findByRole('link', { name: 'Non Cayc QG' });
+
+  await user.click(nonCompliantQualityGate);
+
+  expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+  expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument();
+});
+
+it('should warn user when quality gate is not CAYC compliant and user has permission to edit it', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const nonCompliantQualityGate = await screen.findByRole('link', { name: /Non Cayc QG/ });
+
+  await user.click(nonCompliantQualityGate);
+
+  expect(await screen.findByRole('alert')).toHaveTextContent(
+    /quality_gates.cayc_missing.banner.title/
+  );
+  expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0);
+});
+
 it('should show success banner when quality gate is CAYC compliant', async () => {
   const user = userEvent.setup();
   handler.setIsAdmin(true);
@@ -626,5 +651,5 @@ describe('The Permissions section', () => {
 });
 
 function renderQualityGateApp(context?: RenderContext) {
-  renderAppRoutes('quality_gates', routes, context);
+  return renderAppRoutes('quality_gates', routes, context);
 }