You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BranchOverview-test.tsx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { screen } from '@testing-library/react';
  21. import userEvent from '@testing-library/user-event';
  22. import * as React from 'react';
  23. import selectEvent from 'react-select-event';
  24. import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures';
  25. import { getProjectActivity } from '../../../../api/projectActivity';
  26. import {
  27. getApplicationQualityGate,
  28. getQualityGateProjectStatus,
  29. } from '../../../../api/quality-gates';
  30. import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
  31. import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils';
  32. import { isDiffMetric } from '../../../../helpers/measures';
  33. import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
  34. import { mockComponent } from '../../../../helpers/mocks/component';
  35. import { mockAnalysis } from '../../../../helpers/mocks/project-activity';
  36. import {
  37. mockQualityGateApplicationStatus,
  38. mockQualityGateProjectStatus,
  39. } from '../../../../helpers/mocks/quality-gates';
  40. import { mockLoggedInUser, mockPeriod } from '../../../../helpers/testMocks';
  41. import { renderComponent } from '../../../../helpers/testReactTestingUtils';
  42. import { ComponentQualifier } from '../../../../types/component';
  43. import { MetricKey } from '../../../../types/metrics';
  44. import { GraphType } from '../../../../types/project-activity';
  45. import { CaycStatus, Measure, Metric } from '../../../../types/types';
  46. import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview';
  47. jest.mock('../../../../api/measures', () => {
  48. const { mockMeasure, mockMetric } = jest.requireActual('../../../../helpers/testMocks');
  49. return {
  50. getMeasuresWithPeriodAndMetrics: jest.fn((_, metricKeys: string[]) => {
  51. const metrics: Metric[] = [];
  52. const measures: Measure[] = [];
  53. metricKeys.forEach((key) => {
  54. if (key === 'unknown_metric') {
  55. return;
  56. }
  57. let type;
  58. if (/(coverage|duplication)$/.test(key)) {
  59. type = 'PERCENT';
  60. } else if (/_rating$/.test(key)) {
  61. type = 'RATING';
  62. } else {
  63. type = 'INT';
  64. }
  65. metrics.push(mockMetric({ key, id: key, name: key, type }));
  66. measures.push(
  67. mockMeasure({
  68. metric: key,
  69. ...(isDiffMetric(key) ? { leak: '1' } : { period: undefined }),
  70. })
  71. );
  72. });
  73. return Promise.resolve({
  74. component: {
  75. measures,
  76. name: 'foo',
  77. },
  78. metrics,
  79. });
  80. }),
  81. };
  82. });
  83. jest.mock('../../../../api/quality-gates', () => {
  84. const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus } = jest.requireActual(
  85. '../../../../helpers/mocks/quality-gates'
  86. );
  87. const { MetricKey } = jest.requireActual('../../../../types/metrics');
  88. return {
  89. getQualityGateProjectStatus: jest.fn().mockResolvedValue(
  90. mockQualityGateProjectStatus({
  91. status: 'ERROR',
  92. conditions: [
  93. {
  94. actualValue: '2',
  95. comparator: 'GT',
  96. errorThreshold: '1',
  97. metricKey: MetricKey.new_reliability_rating,
  98. periodIndex: 1,
  99. status: 'ERROR',
  100. },
  101. {
  102. actualValue: '5',
  103. comparator: 'GT',
  104. errorThreshold: '2.0',
  105. metricKey: MetricKey.bugs,
  106. periodIndex: 0,
  107. status: 'ERROR',
  108. },
  109. {
  110. actualValue: '2',
  111. comparator: 'GT',
  112. errorThreshold: '1.0',
  113. metricKey: 'unknown_metric',
  114. periodIndex: 0,
  115. status: 'ERROR',
  116. },
  117. ],
  118. })
  119. ),
  120. getApplicationQualityGate: jest.fn().mockResolvedValue(mockQualityGateApplicationStatus()),
  121. };
  122. });
  123. jest.mock('../../../../api/time-machine', () => {
  124. const { MetricKey } = jest.requireActual('../../../../types/metrics');
  125. return {
  126. getAllTimeMachineData: jest.fn().mockResolvedValue({
  127. measures: [
  128. { metric: MetricKey.bugs, history: [{ date: '2019-01-05', value: '2.0' }] },
  129. { metric: MetricKey.vulnerabilities, history: [{ date: '2019-01-05', value: '0' }] },
  130. { metric: MetricKey.sqale_index, history: [{ date: '2019-01-01', value: '1.0' }] },
  131. {
  132. metric: MetricKey.duplicated_lines_density,
  133. history: [{ date: '2019-01-02', value: '1.0' }],
  134. },
  135. { metric: MetricKey.ncloc, history: [{ date: '2019-01-03', value: '10000' }] },
  136. { metric: MetricKey.coverage, history: [{ date: '2019-01-04', value: '95.5' }] },
  137. ],
  138. }),
  139. };
  140. });
  141. jest.mock('../../../../api/projectActivity', () => {
  142. const { mockAnalysis } = jest.requireActual('../../../../helpers/mocks/project-activity');
  143. return {
  144. getProjectActivity: jest.fn().mockResolvedValue({
  145. analyses: [
  146. mockAnalysis({ detectedCI: 'Cirrus CI' }),
  147. mockAnalysis(),
  148. mockAnalysis(),
  149. mockAnalysis(),
  150. mockAnalysis(),
  151. ],
  152. }),
  153. };
  154. });
  155. jest.mock('../../../../api/application', () => ({
  156. getApplicationDetails: jest.fn().mockResolvedValue({
  157. branches: [],
  158. key: 'key-1',
  159. name: 'app',
  160. projects: [
  161. {
  162. branch: 'foo',
  163. key: 'KEY-P1',
  164. name: 'P1',
  165. },
  166. ],
  167. visibility: 'Private',
  168. }),
  169. getApplicationLeak: jest.fn().mockResolvedValue([
  170. {
  171. date: '2017-01-05',
  172. project: 'foo',
  173. projectName: 'Foo',
  174. },
  175. ]),
  176. }));
  177. jest.mock('../../../../components/activity-graph/utils', () => {
  178. const { MetricKey } = jest.requireActual('../../../../types/metrics');
  179. const { GraphType } = jest.requireActual('../../../../types/project-activity');
  180. const original = jest.requireActual('../../../../components/activity-graph/utils');
  181. return {
  182. ...original,
  183. getActivityGraph: jest.fn(() => ({ graph: GraphType.coverage })),
  184. saveActivityGraph: jest.fn(),
  185. getHistoryMetrics: jest.fn(() => [MetricKey.lines_to_cover, MetricKey.uncovered_lines]),
  186. };
  187. });
  188. beforeEach(jest.clearAllMocks);
  189. describe('project overview', () => {
  190. it('should show a successful QG', async () => {
  191. const user = userEvent.setup();
  192. jest
  193. .mocked(getQualityGateProjectStatus)
  194. .mockResolvedValueOnce(mockQualityGateProjectStatus({ status: 'OK' }));
  195. renderBranchOverview();
  196. // QG panel
  197. expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
  198. expect(screen.getByText('overview.quality_gate_all_conditions_passed')).toBeInTheDocument();
  199. expect(
  200. screen.queryByText('overview.quality_gate.conditions.cayc.warning')
  201. ).not.toBeInTheDocument();
  202. //Measures panel
  203. expect(screen.getByText('metric.new_vulnerabilities.name')).toBeInTheDocument();
  204. // go to overall
  205. await user.click(screen.getByText('overview.overall_code'));
  206. expect(screen.getByText('metric.vulnerabilities.name')).toBeInTheDocument();
  207. });
  208. it('should show a successful non-compliant QG', async () => {
  209. jest
  210. .mocked(getQualityGateProjectStatus)
  211. .mockResolvedValueOnce(
  212. mockQualityGateProjectStatus({ status: 'OK', caycStatus: CaycStatus.NonCompliant })
  213. );
  214. renderBranchOverview();
  215. expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
  216. expect(screen.getByText('overview.quality_gate.conditions.cayc.warning')).toBeInTheDocument();
  217. });
  218. it('should show a failed QG', async () => {
  219. renderBranchOverview();
  220. expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
  221. expect(screen.getByText('overview.X_conditions_failed.2')).toBeInTheDocument();
  222. });
  223. it('should correctly show a project as empty', async () => {
  224. jest.mocked(getMeasuresWithPeriodAndMetrics).mockResolvedValueOnce({
  225. component: { key: '', name: '', qualifier: ComponentQualifier.Project, measures: [] },
  226. metrics: [],
  227. period: mockPeriod(),
  228. });
  229. renderBranchOverview();
  230. expect(await screen.findByText('overview.project.main_branch_empty')).toBeInTheDocument();
  231. });
  232. });
  233. describe('application overview', () => {
  234. const component = mockComponent({
  235. breadcrumbs: [mockComponent({ key: 'foo', qualifier: ComponentQualifier.Application })],
  236. qualifier: ComponentQualifier.Application,
  237. });
  238. it('should show failed conditions for every project', async () => {
  239. renderBranchOverview({ component });
  240. expect(await screen.findByText('Foo')).toBeInTheDocument();
  241. expect(screen.getByText('Bar')).toBeInTheDocument();
  242. });
  243. it("should show projects that don't have a compliant quality gate", async () => {
  244. const appStatus = mockQualityGateApplicationStatus({
  245. projects: [
  246. {
  247. key: '1',
  248. name: 'first project',
  249. conditions: [],
  250. caycStatus: CaycStatus.NonCompliant,
  251. status: 'OK',
  252. },
  253. {
  254. key: '2',
  255. name: 'second',
  256. conditions: [],
  257. caycStatus: CaycStatus.Compliant,
  258. status: 'OK',
  259. },
  260. {
  261. key: '3',
  262. name: 'number 3',
  263. conditions: [],
  264. caycStatus: CaycStatus.NonCompliant,
  265. status: 'OK',
  266. },
  267. {
  268. key: '4',
  269. name: 'four',
  270. conditions: [
  271. {
  272. comparator: 'GT',
  273. metric: MetricKey.bugs,
  274. status: 'ERROR',
  275. value: '3',
  276. errorThreshold: '0',
  277. },
  278. ],
  279. caycStatus: CaycStatus.NonCompliant,
  280. status: 'ERROR',
  281. },
  282. ],
  283. });
  284. jest.mocked(getApplicationQualityGate).mockResolvedValueOnce(appStatus);
  285. renderBranchOverview({ component });
  286. expect(
  287. await screen.findByText('overview.quality_gate.application.non_cayc.projects_x.3')
  288. ).toBeInTheDocument();
  289. expect(screen.getByText('first project')).toBeInTheDocument();
  290. expect(screen.queryByText('second')).not.toBeInTheDocument();
  291. expect(screen.getByText('number 3')).toBeInTheDocument();
  292. });
  293. it('should correctly show an app as empty', async () => {
  294. jest.mocked(getMeasuresWithPeriodAndMetrics).mockResolvedValueOnce({
  295. component: { key: '', name: '', qualifier: ComponentQualifier.Application, measures: [] },
  296. metrics: [],
  297. period: mockPeriod(),
  298. });
  299. renderBranchOverview({ component });
  300. expect(await screen.findByText('portfolio.app.empty')).toBeInTheDocument();
  301. });
  302. });
  303. it.each([
  304. ['no analysis', [], true],
  305. ['1 analysis, no CI data', [mockAnalysis()], false],
  306. ['1 analysis, no CI detected', [mockAnalysis({ detectedCI: NO_CI_DETECTED })], false],
  307. ['1 analysis, CI detected', [mockAnalysis({ detectedCI: 'Cirrus CI' })], true],
  308. ])(
  309. "should correctly flag a project that wasn't analyzed using a CI (%s)",
  310. async (_, analyses, expected) => {
  311. (getProjectActivity as jest.Mock).mockResolvedValueOnce({ analyses });
  312. renderBranchOverview();
  313. // wait for loading
  314. await screen.findByText('overview.quality_gate');
  315. expect(screen.queryByText('overview.project.next_steps.set_up_ci') === null).toBe(expected);
  316. }
  317. );
  318. it('should correctly handle graph type storage', async () => {
  319. renderBranchOverview();
  320. expect(getActivityGraph).toHaveBeenCalledWith(BRANCH_OVERVIEW_ACTIVITY_GRAPH, 'foo');
  321. const select = await screen.findByLabelText('project_activity.graphs.choose_type');
  322. await selectEvent.select(select, `project_activity.graphs.${GraphType.issues}`);
  323. expect(saveActivityGraph).toHaveBeenCalledWith(
  324. BRANCH_OVERVIEW_ACTIVITY_GRAPH,
  325. 'foo',
  326. GraphType.issues
  327. );
  328. });
  329. function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
  330. renderComponent(
  331. <CurrentUserContextProvider currentUser={mockLoggedInUser()}>
  332. <BranchOverview
  333. branch={mockMainBranch()}
  334. component={mockComponent({
  335. breadcrumbs: [mockComponent({ key: 'foo' })],
  336. key: 'foo',
  337. name: 'Foo',
  338. })}
  339. {...props}
  340. />
  341. </CurrentUserContextProvider>
  342. );
  343. }