*/
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,
dissociateGateWithProject,
fetchQualityGate,
fetchQualityGates,
+ getApplicationQualityGate,
getGateForProject,
+ getQualityGateProjectStatus,
renameQualityGate,
searchGroups,
searchProjects,
updateCondition,
} from '../quality-gates';
+jest.mock('../quality-gates');
+
export class QualityGatesServiceMock {
isAdmin = false;
readOnlyList: QualityGate[];
projects: Project[];
getGateForProjectGateName: string;
throwOnGetGateForProject: boolean;
+ qualityGateProjectStatus: QualityGateProjectStatus;
+ applicationQualityGate: QualityGateApplicationStatus;
constructor(list?: QualityGate[]) {
this.readOnlyList = list || [
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({});
selected,
query,
}: {
+ gateName: string;
selected: string;
query: string | undefined;
}) => {
return this.reply(response);
};
- searchUsersHandler = ({ selected }: { selected: string }) => {
+ searchUsersHandler = ({ selected }: SearchPermissionsParameters) => {
if (selected === 'selected') {
return this.reply({ users: [] });
}
return this.reply({ users: [mockUserBase()] });
};
- searchGroupsHandler = ({ selected }: { selected: string }) => {
+ searchGroupsHandler = ({ selected }: SearchPermissionsParameters) => {
if (selected === 'selected') {
return this.reply({ groups: [] });
}
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));
}
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
);
}
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,
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';
metrics?: Metric[];
period?: Period;
qgStatuses?: QualityGateStatus[];
+ qualityGate?: QualityGate;
}
export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph';
this.loadApplicationStatus();
} else {
this.loadProjectStatus();
+ this.loadProjectQualityGate();
}
};
);
};
+ 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,
metrics,
period,
qgStatuses,
+ qualityGate,
} = this.state;
const projectIsEmpty =
period={period}
projectIsEmpty={projectIsEmpty}
qgStatuses={qgStatuses}
+ qualityGate={qualityGate}
/>
);
}
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';
period?: Period;
projectIsEmpty?: boolean;
qgStatuses?: QualityGateStatus[];
+ qualityGate?: QualityGate;
}
export default function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
period,
projectIsEmpty,
qgStatuses,
+ qualityGate,
} = props;
const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
component={component}
loading={loadingStatus}
qgStatuses={qgStatuses}
+ qualityGate={qualityGate}
/>
</div>
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';
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;
{qgStatuses.length === 1 &&
qgStatuses[0].caycStatus === CaycStatus.NonCompliant &&
+ qualityGate?.actions?.manageConditions &&
!isApp && (
<Card className="sw-mt-4 sw-body-sm">
<CleanAsYouCodeWarning component={component} />
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';
};
});
-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 {
});
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
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();
},
],
});
- jest.mocked(getApplicationQualityGate).mockResolvedValueOnce(appStatus);
+ qualityGatesMock.setApplicationQualityGateStatus(appStatus);
renderBranchOverview({ component });
expect(
});
function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
- renderComponent(
+ return renderComponent(
<CurrentUserContextProvider currentUser={mockLoggedInUser()}>
<BranchOverview
branch={mockMainBranch()}
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';
</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>
fetchDetails = () => {
const { qualityGateName } = this.props;
+
this.setState({ loading: true });
return fetchQualityGate({ name: qualityGateName }).then(
(qualityGate) => {
*/
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';
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">
<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>
)}
{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>
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(() => {
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);
});
function renderQualityGateApp(context?: RenderContext) {
- renderAppRoutes('quality_gates', routes, context);
+ return renderAppRoutes('quality_gates', routes, context);
}