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.

ComponentsServiceMock.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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 { cloneDeep, pick } from 'lodash';
  21. import { DEFAULT_METRICS } from '../../helpers/mocks/metrics';
  22. import { HttpStatus, RequestData } from '../../helpers/request';
  23. import { mockMetric } from '../../helpers/testMocks';
  24. import { isDefined } from '../../helpers/types';
  25. import { BranchParameters } from '../../types/branch-like';
  26. import { TreeComponent, Visibility } from '../../types/component';
  27. import {
  28. Component,
  29. ComponentMeasure,
  30. Dict,
  31. DuplicatedFile,
  32. Duplication,
  33. Metric,
  34. Paging,
  35. } from '../../types/types';
  36. import {
  37. ComponentRaw,
  38. GetTreeParams,
  39. changeKey,
  40. getBreadcrumbs,
  41. getChildren,
  42. getComponentData,
  43. getComponentForSourceViewer,
  44. getComponentLeaves,
  45. getComponentTree,
  46. getDuplications,
  47. getSources,
  48. getTree,
  49. searchProjects,
  50. setApplicationTags,
  51. setProjectTags,
  52. } from '../components';
  53. import {
  54. ComponentTree,
  55. SourceFile,
  56. mockFullComponentTree,
  57. mockFullSourceViewerFileList,
  58. } from './data/components';
  59. import { mockIssuesList } from './data/issues';
  60. import { MeasureRecords, mockFullMeasureData } from './data/measures';
  61. import { mockProjects } from './data/projects';
  62. import { listAllComponent, listChildComponent, listLeavesComponent } from './data/utils';
  63. jest.mock('../components');
  64. export default class ComponentsServiceMock {
  65. failLoadingComponentStatus: HttpStatus | undefined = undefined;
  66. defaultComponents: ComponentTree[];
  67. defaultProjects: ComponentRaw[];
  68. components: ComponentTree[];
  69. defaultSourceFiles: SourceFile[];
  70. sourceFiles: SourceFile[];
  71. defaultMeasures: MeasureRecords;
  72. measures: MeasureRecords;
  73. projects: ComponentRaw[];
  74. constructor(components?: ComponentTree[], sourceFiles?: SourceFile[], measures?: MeasureRecords) {
  75. this.defaultComponents = components || [mockFullComponentTree()];
  76. this.defaultSourceFiles = sourceFiles || mockFullSourceViewerFileList();
  77. const issueList = mockIssuesList();
  78. this.defaultMeasures =
  79. measures ||
  80. this.defaultComponents.reduce(
  81. (acc, tree) => ({ ...acc, ...mockFullMeasureData(tree, issueList) }),
  82. {},
  83. );
  84. this.defaultProjects = mockProjects();
  85. this.components = cloneDeep(this.defaultComponents);
  86. this.sourceFiles = cloneDeep(this.defaultSourceFiles);
  87. this.measures = cloneDeep(this.defaultMeasures);
  88. this.projects = cloneDeep(this.defaultProjects);
  89. jest.mocked(getComponentTree).mockImplementation(this.handleGetComponentTree);
  90. jest.mocked(getChildren).mockImplementation(this.handleGetChildren);
  91. jest.mocked(getTree).mockImplementation(this.handleGetTree);
  92. jest.mocked(getComponentData).mockImplementation(this.handleGetComponentData);
  93. jest
  94. .mocked(getComponentForSourceViewer)
  95. .mockImplementation(this.handleGetComponentForSourceViewer);
  96. jest.mocked(getDuplications).mockImplementation(this.handleGetDuplications);
  97. jest.mocked(getSources).mockImplementation(this.handleGetSources);
  98. jest.mocked(changeKey).mockImplementation(this.handleChangeKey);
  99. jest.mocked(getComponentLeaves).mockImplementation(this.handleGetComponentLeaves);
  100. jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
  101. jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
  102. jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags);
  103. jest.mocked(searchProjects).mockImplementation(this.handleSearchProjects);
  104. }
  105. handleSearchProjects: typeof searchProjects = (data) => {
  106. const pageIndex = data.p ?? 1;
  107. const pageSize = data.ps ?? 100;
  108. const components = this.projects
  109. .filter((c) => {
  110. if (data.filter && data.filter.startsWith('query')) {
  111. const query = data.filter.split('query=')[1];
  112. return c.key.includes(query) || c.name.includes(query);
  113. }
  114. })
  115. .map((c) => c);
  116. return this.reply({
  117. components: components.slice((pageIndex - 1) * pageSize, pageIndex * pageSize),
  118. facets: [],
  119. paging: {
  120. pageSize,
  121. pageIndex,
  122. total: components.length,
  123. },
  124. });
  125. };
  126. findComponentTree = (key: string, from?: ComponentTree) => {
  127. let tree: ComponentTree | undefined;
  128. const recurse = (node: ComponentTree): boolean => {
  129. if (node.component.key === key) {
  130. tree = node;
  131. return true;
  132. }
  133. return node.children.some((child) => recurse(child));
  134. };
  135. if (from !== undefined) {
  136. recurse(from);
  137. return tree;
  138. }
  139. for (let i = 0, len = this.components.length; i < len; i++) {
  140. if (recurse(this.components[i])) {
  141. return tree;
  142. }
  143. }
  144. throw new Error(`Couldn't find component tree for key ${key}`);
  145. };
  146. findSourceFile = (key: string): SourceFile => {
  147. const sourceFile = this.sourceFiles.find((s) => s.component.key === key);
  148. if (sourceFile) {
  149. return sourceFile;
  150. }
  151. throw new Error(`Couldn't find source file for key ${key}`);
  152. };
  153. registerComponent = (component: Component, ancestors: Component[] = []) => {
  154. this.components.push({ component, ancestors, children: [] });
  155. };
  156. registerComponentTree = (componentTree: ComponentTree, replace = true) => {
  157. if (replace) {
  158. this.components = [];
  159. }
  160. this.components.push(componentTree);
  161. };
  162. registerComponentMeasures = (measures: MeasureRecords) => {
  163. this.measures = measures;
  164. };
  165. setFailLoadingComponentStatus = (status: HttpStatus.Forbidden | HttpStatus.NotFound) => {
  166. this.failLoadingComponentStatus = status;
  167. };
  168. getHugeFileKey = () => {
  169. const { sourceFile } = this.sourceFiles.reduce(
  170. (acc, sourceFile) => {
  171. if (sourceFile.lines.length > acc.size) {
  172. return {
  173. sourceFile,
  174. size: sourceFile.lines.length,
  175. };
  176. }
  177. return acc;
  178. },
  179. { sourceFile: undefined, size: -Infinity },
  180. );
  181. if (sourceFile) {
  182. return sourceFile.component.key;
  183. }
  184. throw new Error('Could not find a large source file');
  185. };
  186. getEmptyFileKey = () => {
  187. const sourceFile = this.sourceFiles.find((sourceFile) => {
  188. if (sourceFile.lines.length === 0) {
  189. return sourceFile;
  190. }
  191. });
  192. if (sourceFile) {
  193. return sourceFile.component.key;
  194. }
  195. throw new Error('Could not find an empty source file');
  196. };
  197. getNonEmptyFileKey = (preferredKey = 'foo:test1.js') => {
  198. let sourceFile = this.sourceFiles.find((sourceFile) => {
  199. if (sourceFile.component.key === preferredKey) {
  200. return sourceFile;
  201. }
  202. });
  203. if (!sourceFile) {
  204. sourceFile = this.sourceFiles.find((sourceFile) => {
  205. if (sourceFile.lines.length > 0) {
  206. return sourceFile;
  207. }
  208. });
  209. }
  210. if (sourceFile) {
  211. return sourceFile.component.key;
  212. }
  213. throw new Error('Could not find a non-empty source file');
  214. };
  215. reset = () => {
  216. this.components = cloneDeep(this.defaultComponents);
  217. this.sourceFiles = cloneDeep(this.defaultSourceFiles);
  218. this.measures = cloneDeep(this.defaultMeasures);
  219. };
  220. handleGetChildren = (
  221. component: string,
  222. metrics: string[] = [],
  223. data: RequestData = {},
  224. ): Promise<{
  225. baseComponent: ComponentMeasure;
  226. components: ComponentMeasure[];
  227. metrics: Metric[];
  228. paging: Paging;
  229. }> => {
  230. return this.handleGetComponentTree('children', component, metrics, data);
  231. };
  232. handleGetComponentTree = (
  233. strategy: string,
  234. key: string,
  235. metricKeys: string[] = [],
  236. { p = 1, ps = 100 }: RequestData = {},
  237. ): Promise<{
  238. baseComponent: ComponentMeasure;
  239. components: ComponentMeasure[];
  240. metrics: Metric[];
  241. paging: Paging;
  242. }> => {
  243. const base = this.findComponentTree(key);
  244. let components: Component[] = [];
  245. if (base === undefined) {
  246. return Promise.reject({
  247. errors: [{ msg: `No component has been found for id ${key}` }],
  248. });
  249. }
  250. if (strategy === 'all' || strategy === '') {
  251. components = listAllComponent(base);
  252. } else if (strategy === 'children') {
  253. components = listChildComponent(base);
  254. } else if (strategy === 'leaves') {
  255. components = listLeavesComponent(base);
  256. }
  257. const componentsMeasures: ComponentMeasure[] = components.map((c) => {
  258. return {
  259. measures: metricKeys
  260. .map((metric) => this.measures[c.key] && this.measures[c.key][metric])
  261. .filter(isDefined),
  262. ...pick(c, ['analysisDate', 'key', 'name', 'qualifier']),
  263. };
  264. });
  265. return this.reply({
  266. baseComponent: base.component,
  267. components: componentsMeasures.slice(ps * (p - 1), ps * (p - 1) + ps),
  268. metrics: metricKeys.map((metric) => DEFAULT_METRICS[metric] ?? mockMetric({ key: metric })),
  269. paging: {
  270. pageSize: ps,
  271. pageIndex: p,
  272. total: componentsMeasures.length,
  273. },
  274. });
  275. };
  276. handleGetTree = ({
  277. component: key,
  278. q = '',
  279. qualifiers,
  280. }: GetTreeParams & { qualifiers?: string }): Promise<{
  281. baseComponent: TreeComponent;
  282. components: TreeComponent[];
  283. paging: Paging;
  284. }> => {
  285. const base = this.findComponentTree(key);
  286. if (base === undefined) {
  287. return Promise.reject({
  288. errors: [{ msg: `No component has been found for key ${key}` }],
  289. });
  290. }
  291. const components: TreeComponent[] = listAllComponent(base)
  292. .filter(({ name, key }) => name.includes(q) || key.includes(q))
  293. .filter(({ qualifier }) => (qualifiers?.length ? qualifiers.includes(qualifier) : true))
  294. .map((c) => ({ ...c, visibility: Visibility.Public }));
  295. return this.reply({
  296. baseComponent: { ...base.component, visibility: Visibility.Public },
  297. components,
  298. paging: {
  299. pageIndex: 1,
  300. pageSize: 100,
  301. total: components.length,
  302. },
  303. });
  304. };
  305. handleGetComponentData = (data: { component: string } & BranchParameters) => {
  306. if (this.failLoadingComponentStatus !== undefined) {
  307. return Promise.reject({ status: this.failLoadingComponentStatus });
  308. }
  309. const tree = this.findComponentTree(data.component);
  310. if (tree) {
  311. const { component, ancestors } = tree;
  312. return this.reply({ component, ancestors } as {
  313. component: ComponentRaw;
  314. ancestors: ComponentRaw[];
  315. });
  316. }
  317. throw new Error(`Couldn't find component with key ${data.component}`);
  318. };
  319. handleGetComponentForSourceViewer = ({ component }: { component: string } & BranchParameters) => {
  320. const sourceFile = this.findSourceFile(component);
  321. return this.reply(sourceFile.component);
  322. };
  323. handleGetDuplications = ({
  324. key,
  325. }: { key: string } & BranchParameters): Promise<{
  326. duplications: Duplication[];
  327. files: Dict<DuplicatedFile>;
  328. }> => {
  329. const { duplication } = this.findSourceFile(key);
  330. if (duplication) {
  331. return this.reply(duplication);
  332. }
  333. return this.reply({ duplications: [], files: {} });
  334. };
  335. handleGetSources = (data: { key: string; from?: number; to?: number } & BranchParameters) => {
  336. const { lines } = this.findSourceFile(data.key);
  337. const from = data.from || 1;
  338. const to = data.to || lines.length;
  339. return this.reply(lines.slice(from - 1, to));
  340. };
  341. handleChangeKey = (data: { from: string; to: string }) => {
  342. const treeItem = this.components.find(({ component }) => component.key === data.from);
  343. if (treeItem) {
  344. treeItem.component.key = data.to;
  345. return this.reply(undefined);
  346. }
  347. return Promise.reject({ status: 404, message: 'Component not found' });
  348. };
  349. handleGetComponentLeaves = (
  350. component: string,
  351. metrics: string[] = [],
  352. data: RequestData = {},
  353. ): Promise<{
  354. baseComponent: ComponentMeasure;
  355. components: ComponentMeasure[];
  356. metrics: Metric[];
  357. paging: Paging;
  358. }> => {
  359. return this.handleGetComponentTree('leaves', component, metrics, data);
  360. };
  361. handleGetBreadcrumbs = ({ component: key }: { component: string } & BranchParameters) => {
  362. const base = this.findComponentTree(key);
  363. if (base === undefined) {
  364. return Promise.reject({
  365. errors: [{ msg: `No component has been found for id ${key}` }],
  366. });
  367. }
  368. return this.reply([...(base.ancestors as ComponentRaw[]), base.component as ComponentRaw]);
  369. };
  370. handleSetProjectTags: typeof setProjectTags = ({ project, tags }) => {
  371. const base = this.findComponentTree(project);
  372. if (base !== undefined) {
  373. base.component.tags = tags.split(',');
  374. }
  375. return this.reply();
  376. };
  377. handleSetApplicationTags: typeof setApplicationTags = ({ application, tags }) => {
  378. const base = this.findComponentTree(application);
  379. if (base !== undefined) {
  380. base.component.tags = tags.split(',');
  381. }
  382. return this.reply();
  383. };
  384. reply<T>(): Promise<void>;
  385. reply<T>(response: T): Promise<T>;
  386. reply<T>(response?: T): Promise<T | void> {
  387. return Promise.resolve(response ? cloneDeep(response) : undefined);
  388. }
  389. }