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.

IssuesServiceMock.ts 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  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 { cloneDeep, uniqueId } from 'lodash';
  21. import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
  22. import {
  23. ISSUE_TYPES,
  24. RESOLUTIONS,
  25. SEVERITIES,
  26. SOURCE_SCOPES,
  27. STATUSES,
  28. } from '../../helpers/constants';
  29. import { mockIssueAuthors, mockIssueChangelog } from '../../helpers/mocks/issues';
  30. import { RequestData } from '../../helpers/request';
  31. import { getStandards } from '../../helpers/security-standard';
  32. import { mockLoggedInUser, mockPaging, mockRuleDetails } from '../../helpers/testMocks';
  33. import { SearchRulesResponse } from '../../types/coding-rules';
  34. import {
  35. ASSIGNEE_ME,
  36. CleanCodeAttributeCategory,
  37. IssueResolution,
  38. IssueStatus,
  39. IssueTransition,
  40. IssueType,
  41. ListIssuesResponse,
  42. RawFacet,
  43. RawIssue,
  44. RawIssuesResponse,
  45. ReferencedComponent,
  46. SoftwareImpactSeverity,
  47. SoftwareQuality,
  48. } from '../../types/issues';
  49. import { SearchRulesQuery } from '../../types/rules';
  50. import { Standards } from '../../types/security';
  51. import { Dict, Rule, RuleActivation, RuleDetails, SnippetsByComponent } from '../../types/types';
  52. import { LoggedInUser, NoticeType, RestUser } from '../../types/users';
  53. import {
  54. addIssueComment,
  55. bulkChangeIssues,
  56. deleteIssueComment,
  57. editIssueComment,
  58. getIssueChangelog,
  59. getIssueFlowSnippets,
  60. listIssues,
  61. searchIssueAuthors,
  62. searchIssueTags,
  63. searchIssues,
  64. setIssueAssignee,
  65. setIssueSeverity,
  66. setIssueTags,
  67. setIssueTransition,
  68. setIssueType,
  69. } from '../issues';
  70. import { getRuleDetails, searchRules } from '../rules';
  71. import { dismissNotice, getCurrentUser, getUsers } from '../users';
  72. import { IssueData, mockIssuesList } from './data/issues';
  73. import { mockRuleList } from './data/rules';
  74. jest.mock('../../api/issues');
  75. // The following 2 mocks are needed, because IssuesServiceMock mocks more than it should.
  76. // This should be removed once IssuesServiceMock is cleaned up.
  77. jest.mock('../../api/rules');
  78. jest.mock('../../api/users');
  79. function mockReferenceComponent(override?: Partial<ReferencedComponent>) {
  80. return {
  81. key: 'component1',
  82. name: 'Component1',
  83. uuid: 'id1',
  84. enabled: true,
  85. ...override,
  86. };
  87. }
  88. function generateReferenceComponentsForIssues(issueData: IssueData[]) {
  89. return issueData
  90. .reduce((componentKeys, response) => {
  91. const componentKey = response.issue.component;
  92. if (!componentKeys.includes(componentKey)) {
  93. return [...componentKeys, componentKey];
  94. }
  95. return componentKeys;
  96. }, [] as string[])
  97. .map((key) => mockReferenceComponent({ key, enabled: true }));
  98. }
  99. export default class IssuesServiceMock {
  100. isAdmin = false;
  101. currentUser: LoggedInUser;
  102. standards?: Standards;
  103. defaultList: IssueData[];
  104. rulesList: Rule[];
  105. list: IssueData[];
  106. constructor() {
  107. this.currentUser = mockLoggedInUser();
  108. this.defaultList = mockIssuesList();
  109. this.rulesList = mockRuleList();
  110. this.list = cloneDeep(this.defaultList);
  111. jest.mocked(addIssueComment).mockImplementation(this.handleAddComment);
  112. jest.mocked(bulkChangeIssues).mockImplementation(this.handleBulkChangeIssues);
  113. jest.mocked(deleteIssueComment).mockImplementation(this.handleDeleteComment);
  114. jest.mocked(dismissNotice).mockImplementation(this.handleDismissNotification);
  115. jest.mocked(editIssueComment).mockImplementation(this.handleEditComment);
  116. jest.mocked(getCurrentUser).mockImplementation(this.handleGetCurrentUser);
  117. jest.mocked(getIssueChangelog).mockImplementation(this.handleGetIssueChangelog);
  118. jest.mocked(getIssueFlowSnippets).mockImplementation(this.handleGetIssueFlowSnippets);
  119. jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails);
  120. jest.mocked(listIssues).mockImplementation(this.handleListIssues);
  121. jest.mocked(searchIssueAuthors).mockImplementation(this.handleSearchIssueAuthors);
  122. jest.mocked(searchIssues).mockImplementation(this.handleSearchIssues);
  123. jest.mocked(searchIssueTags).mockImplementation(this.handleSearchIssueTags);
  124. jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
  125. jest.mocked(getUsers).mockImplementation(this.handleGetUsers);
  126. jest.mocked(setIssueAssignee).mockImplementation(this.handleSetIssueAssignee);
  127. jest.mocked(setIssueSeverity).mockImplementation(this.handleSetIssueSeverity);
  128. jest.mocked(setIssueTags).mockImplementation(this.handleSetIssueTags);
  129. jest.mocked(setIssueTransition).mockImplementation(this.handleSetIssueTransition);
  130. jest.mocked(setIssueType).mockImplementation(this.handleSetIssueType);
  131. }
  132. reset = () => {
  133. this.list = cloneDeep(this.defaultList);
  134. this.currentUser = mockLoggedInUser();
  135. };
  136. setCurrentUser = (user: LoggedInUser) => {
  137. this.currentUser = user;
  138. };
  139. setIssueList = (list: IssueData[]) => {
  140. this.list = list;
  141. };
  142. async getStandards(): Promise<Standards> {
  143. if (this.standards) {
  144. return this.standards;
  145. }
  146. this.standards = await getStandards();
  147. return this.standards;
  148. }
  149. owasp2021FacetList(): RawFacet {
  150. return {
  151. property: 'owaspTop10-2021',
  152. values: [{ val: 'a1', count: 0 }],
  153. };
  154. }
  155. setIsAdmin(isAdmin: boolean) {
  156. this.isAdmin = isAdmin;
  157. }
  158. handleBulkChangeIssues = (issueKeys: string[], query: RequestData) => {
  159. // For now we only check for issue type and status change.
  160. this.list
  161. .filter((i) => issueKeys.includes(i.issue.key))
  162. .forEach((data) => {
  163. data.issue.type = query.set_type ?? data.issue.type;
  164. data.issue.status = query.do_transition ?? data.issue.status;
  165. });
  166. return this.reply(undefined);
  167. };
  168. handleGetIssueFlowSnippets = (issueKey: string): Promise<Dict<SnippetsByComponent>> => {
  169. const issue = this.list.find((i) => i.issue.key === issueKey);
  170. if (issue === undefined) {
  171. return Promise.reject({ errors: [{ msg: `No issue has been found for id ${issueKey}` }] });
  172. }
  173. return this.reply(issue.snippets);
  174. };
  175. handleSearchRules = (req: SearchRulesQuery): Promise<SearchRulesResponse> => {
  176. const rules = this.rulesList.filter((rule) => {
  177. const query = req.q?.toLowerCase() || '';
  178. const nameMatches = rule.name.toLowerCase().includes(query);
  179. const keyMatches = rule.key.toLowerCase().includes(query);
  180. const isTypeRight = req.types?.includes(rule.type);
  181. return isTypeRight && (nameMatches || keyMatches);
  182. });
  183. return this.reply({
  184. rules,
  185. paging: mockPaging({
  186. total: rules.length,
  187. pageIndex: 1,
  188. pageSize: 30,
  189. }),
  190. });
  191. };
  192. handleGetRuleDetails = (parameters: {
  193. actives?: boolean;
  194. key: string;
  195. }): Promise<{ actives?: RuleActivation[]; rule: RuleDetails }> => {
  196. if (parameters.key === 'advancedRuleId') {
  197. return this.reply({
  198. rule: mockRuleDetails({
  199. key: parameters.key,
  200. name: 'Advanced rule',
  201. htmlNote: '<h1>Extended Description</h1>',
  202. educationPrinciples: ['defense_in_depth'],
  203. descriptionSections: [
  204. { key: RuleDescriptionSections.INTRODUCTION, content: '<h1>Into</h1>' },
  205. { key: RuleDescriptionSections.ROOT_CAUSE, content: '<h1>Because</h1>' },
  206. { key: RuleDescriptionSections.HOW_TO_FIX, content: '<h1>Fix with</h1>' },
  207. {
  208. content: '<p> Context 1 content<p>',
  209. key: RuleDescriptionSections.HOW_TO_FIX,
  210. context: {
  211. key: 'spring',
  212. displayName: 'Spring',
  213. },
  214. },
  215. {
  216. content: '<p> Context 2 content<p>',
  217. key: RuleDescriptionSections.HOW_TO_FIX,
  218. context: {
  219. key: 'context_2',
  220. displayName: 'Context 2',
  221. },
  222. },
  223. {
  224. content: '<p> Context 3 content<p>',
  225. key: RuleDescriptionSections.HOW_TO_FIX,
  226. context: {
  227. key: 'context_3',
  228. displayName: 'Context 3',
  229. },
  230. },
  231. { key: RuleDescriptionSections.RESOURCES, content: '<h1>Link</h1>' },
  232. ],
  233. }),
  234. });
  235. }
  236. return this.reply({
  237. rule: mockRuleDetails({
  238. key: parameters.key,
  239. name: 'Simple rule',
  240. htmlNote: '<h1>Note</h1>',
  241. descriptionSections: [
  242. {
  243. key: RuleDescriptionSections.DEFAULT,
  244. content: '<h1>Default</h1> Default description',
  245. },
  246. ],
  247. }),
  248. });
  249. };
  250. mockFacetDetailResponse = (query: RequestData): RawFacet[] => {
  251. const facets = (query.facets ?? '').split(',');
  252. const cleanCodeCategories: CleanCodeAttributeCategory[] = (
  253. query.cleanCodeAttributeCategories ?? Object.values(CleanCodeAttributeCategory).join(',')
  254. ).split(',');
  255. return facets.map((name: string): RawFacet => {
  256. if (name === 'owaspTop10-2021') {
  257. return this.owasp2021FacetList();
  258. }
  259. if (name === 'codeVariants') {
  260. return {
  261. property: 'codeVariants',
  262. values: this.list.reduce((acc, { issue }) => {
  263. if (issue.codeVariants?.length) {
  264. issue.codeVariants.forEach((codeVariant) => {
  265. const item = acc.find(({ val }) => val === codeVariant);
  266. if (item) {
  267. item.count++;
  268. } else {
  269. acc.push({
  270. val: codeVariant,
  271. count: 1,
  272. });
  273. }
  274. });
  275. }
  276. return acc;
  277. }, [] as RawFacet['values']),
  278. };
  279. }
  280. if (name === 'languages') {
  281. const counters = {
  282. [CleanCodeAttributeCategory.Intentional]: { java: 4100, ts: 500 },
  283. [CleanCodeAttributeCategory.Consistent]: { java: 100, ts: 200 },
  284. [CleanCodeAttributeCategory.Adaptable]: { java: 21000, ts: 2000 },
  285. [CleanCodeAttributeCategory.Responsible]: { java: 111, ts: 674 },
  286. };
  287. return {
  288. property: name,
  289. values: [
  290. {
  291. val: 'java',
  292. count: cleanCodeCategories.reduce<number>(
  293. (acc, category) => acc + counters[category].java,
  294. 0
  295. ),
  296. },
  297. {
  298. val: 'ts',
  299. count: cleanCodeCategories.reduce<number>(
  300. (acc, category) => acc + counters[category].ts,
  301. 0
  302. ),
  303. },
  304. ],
  305. };
  306. }
  307. return {
  308. property: name,
  309. values: (
  310. {
  311. resolutions: RESOLUTIONS,
  312. severities: SEVERITIES,
  313. statuses: STATUSES,
  314. types: ISSUE_TYPES,
  315. scopes: SOURCE_SCOPES.map(({ scope }) => scope),
  316. projects: ['org.project1', 'org.project2'],
  317. impactSoftwareQualities: Object.values(SoftwareQuality),
  318. impactSeverities: Object.values(SoftwareImpactSeverity),
  319. cleanCodeAttributeCategories: cleanCodeCategories,
  320. tags: ['unused', 'confusing'],
  321. rules: ['simpleRuleId', 'advancedRuleId', 'other'],
  322. assignees: ['email1@sonarsource.com', 'email2@sonarsource.com'],
  323. author: ['email3@sonarsource.com', 'email4@sonarsource.com'],
  324. }[name] ?? []
  325. ).map((val) => ({
  326. val,
  327. count: 1, // if 0, the facet can't be clicked in tests
  328. })),
  329. };
  330. });
  331. };
  332. handleListIssues = (query: RequestData): Promise<ListIssuesResponse> => {
  333. const filteredList = this.list
  334. .filter((item) => !query.types || query.types.split(',').includes(item.issue.type))
  335. .filter(
  336. (item) =>
  337. !query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10')
  338. );
  339. // Splice list items according to paging using a fixed page size
  340. const pageIndex = query.p || 1;
  341. const pageSize = 7;
  342. const listItems = filteredList.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);
  343. // Generate response
  344. return this.reply({
  345. components: generateReferenceComponentsForIssues(filteredList),
  346. issues: listItems.map((line) => line.issue),
  347. paging: mockPaging({
  348. pageIndex,
  349. pageSize,
  350. total: filteredList.length,
  351. }),
  352. rules: this.rulesList,
  353. });
  354. };
  355. handleSearchIssues = (query: RequestData): Promise<RawIssuesResponse> => {
  356. const facets = this.mockFacetDetailResponse(query);
  357. // Filter list (only supports assignee, type and severity)
  358. const filteredList = this.list
  359. .filter((item) => {
  360. if (!query.cleanCodeAttributeCategories) {
  361. return true;
  362. }
  363. return query.cleanCodeAttributeCategories
  364. .split(',')
  365. .includes(item.issue.cleanCodeAttributeCategory);
  366. })
  367. .filter((item) => {
  368. if (!query.impactSoftwareQualities) {
  369. return true;
  370. }
  371. return item.issue.impacts.some(({ softwareQuality }) =>
  372. query.impactSoftwareQualities.split(',').includes(softwareQuality)
  373. );
  374. })
  375. .filter((item) => {
  376. if (!query.impactSeverities) {
  377. return true;
  378. }
  379. return item.issue.impacts.some(({ severity }) =>
  380. query.impactSeverities.split(',').includes(severity)
  381. );
  382. })
  383. .filter((item) => {
  384. if (!query.assignees) {
  385. return true;
  386. }
  387. if (query.assignees === ASSIGNEE_ME) {
  388. return item.issue.assignee === mockLoggedInUser().login;
  389. }
  390. return query.assignees.split(',').includes(item.issue.assignee);
  391. })
  392. .filter((item) => {
  393. if (!query.tags) {
  394. return true;
  395. }
  396. if (!item.issue.tags) {
  397. return false;
  398. }
  399. return item.issue.tags.some((tag) => query.tags?.split(',').includes(tag));
  400. })
  401. .filter(
  402. (item) =>
  403. !query.createdBefore || new Date(item.issue.creationDate) <= new Date(query.createdBefore)
  404. )
  405. .filter(
  406. (item) =>
  407. !query.createdAfter || new Date(item.issue.creationDate) >= new Date(query.createdAfter)
  408. )
  409. .filter((item) => !query.types || query.types.split(',').includes(item.issue.type))
  410. .filter(
  411. (item) => !query.severities || query.severities.split(',').includes(item.issue.severity)
  412. )
  413. .filter((item) => !query.scopes || query.scopes.split(',').includes(item.issue.scope))
  414. .filter((item) => !query.statuses || query.statuses.split(',').includes(item.issue.status))
  415. .filter((item) => !query.projects || query.projects.split(',').includes(item.issue.project))
  416. .filter((item) => !query.rules || query.rules.split(',').includes(item.issue.rule))
  417. .filter(
  418. (item) => !query.resolutions || query.resolutions.split(',').includes(item.issue.resolution)
  419. )
  420. .filter(
  421. (item) =>
  422. !query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10')
  423. )
  424. .filter((item) => {
  425. if (!query.codeVariants) {
  426. return true;
  427. }
  428. if (!item.issue.codeVariants) {
  429. return false;
  430. }
  431. return item.issue.codeVariants.some((codeVariant) =>
  432. query.codeVariants?.split(',').includes(codeVariant)
  433. );
  434. });
  435. // Splice list items according to paging using a fixed page size
  436. const pageIndex = query.p || 1;
  437. const pageSize = 7;
  438. const listItems = filteredList.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);
  439. // Generate response
  440. return this.reply({
  441. components: generateReferenceComponentsForIssues(filteredList),
  442. effortTotal: 199629,
  443. facets,
  444. issues: listItems.map((line) => line.issue),
  445. languages: [{ name: 'java' }, { name: 'python' }, { name: 'ts' }],
  446. paging: mockPaging({
  447. pageIndex,
  448. pageSize,
  449. total: filteredList.length,
  450. }),
  451. rules: this.rulesList,
  452. users: [
  453. { login: 'login0' },
  454. { login: 'login1', name: 'Login 1' },
  455. { login: 'login2', name: 'Login 2' },
  456. ],
  457. });
  458. };
  459. handleGetCurrentUser = () => {
  460. return this.reply(this.currentUser);
  461. };
  462. handleDismissNotification = (noticeType: NoticeType) => {
  463. if ([NoticeType.EDUCATION_PRINCIPLES, NoticeType.ISSUE_GUIDE].includes(noticeType)) {
  464. return this.reply(true);
  465. }
  466. return Promise.reject();
  467. };
  468. handleSetIssueType = (data: { issue: string; type: IssueType }) => {
  469. return this.getActionsResponse({ type: data.type }, data.issue);
  470. };
  471. handleSetIssueSeverity = (data: { issue: string; severity: string }) => {
  472. return this.getActionsResponse({ severity: data.severity }, data.issue);
  473. };
  474. handleSetIssueAssignee = (data: { issue: string; assignee?: string }) => {
  475. return this.getActionsResponse(
  476. { assignee: data.assignee === '_me' ? this.currentUser.login : data.assignee },
  477. data.issue
  478. );
  479. };
  480. handleSetIssueTransition = (data: { issue: string; transition: string }) => {
  481. const statusMap: { [key: string]: IssueStatus } = {
  482. [IssueTransition.Confirm]: IssueStatus.Confirmed,
  483. [IssueTransition.UnConfirm]: IssueStatus.Reopened,
  484. [IssueTransition.Resolve]: IssueStatus.Resolved,
  485. [IssueTransition.WontFix]: IssueStatus.Resolved,
  486. [IssueTransition.FalsePositive]: IssueStatus.Resolved,
  487. };
  488. const transitionMap: Dict<IssueTransition[]> = {
  489. [IssueStatus.Reopened]: [
  490. IssueTransition.Confirm,
  491. IssueTransition.Resolve,
  492. IssueTransition.FalsePositive,
  493. IssueTransition.WontFix,
  494. ],
  495. [IssueStatus.Open]: [
  496. IssueTransition.Confirm,
  497. IssueTransition.Resolve,
  498. IssueTransition.FalsePositive,
  499. IssueTransition.WontFix,
  500. ],
  501. [IssueStatus.Confirmed]: [
  502. IssueTransition.Resolve,
  503. IssueTransition.UnConfirm,
  504. IssueTransition.FalsePositive,
  505. IssueTransition.WontFix,
  506. ],
  507. [IssueStatus.Resolved]: [IssueTransition.Reopen],
  508. };
  509. const resolutionMap: Dict<string> = {
  510. [IssueTransition.WontFix]: IssueResolution.WontFix,
  511. [IssueTransition.FalsePositive]: IssueResolution.FalsePositive,
  512. };
  513. return this.getActionsResponse(
  514. {
  515. status: statusMap[data.transition],
  516. transitions: transitionMap[statusMap[data.transition]],
  517. resolution: resolutionMap[data.transition],
  518. },
  519. data.issue
  520. );
  521. };
  522. handleSetIssueTags = (data: { issue: string; tags: string }) => {
  523. const tagsArr = data.tags.split(',');
  524. return this.getActionsResponse({ tags: tagsArr }, data.issue);
  525. };
  526. handleAddComment = (data: { issue: string; text: string }) => {
  527. return this.getActionsResponse(
  528. {
  529. comments: [
  530. {
  531. createdAt: '2022-07-28T11:30:04+0200',
  532. htmlText: data.text,
  533. key: uniqueId(),
  534. login: 'admin',
  535. markdown: data.text,
  536. updatable: true,
  537. },
  538. ],
  539. },
  540. data.issue
  541. );
  542. };
  543. handleEditComment = (data: { comment: string; text: string }) => {
  544. const issueKey = this.list.find((i) => i.issue.comments?.some((c) => c.key === data.comment))
  545. ?.issue.key;
  546. if (!issueKey) {
  547. throw new Error(`Couldn't find issue for comment ${data.comment}`);
  548. }
  549. return this.getActionsResponse(
  550. {
  551. comments: [
  552. {
  553. createdAt: '2022-07-28T11:30:04+0200',
  554. htmlText: data.text,
  555. key: data.comment,
  556. login: 'admin',
  557. markdown: data.text,
  558. updatable: true,
  559. },
  560. ],
  561. },
  562. issueKey
  563. );
  564. };
  565. handleDeleteComment = (data: { comment: string }) => {
  566. const issue = this.list.find((i) =>
  567. i.issue.comments?.some((c) => c.key === data.comment)
  568. )?.issue;
  569. if (!issue) {
  570. throw new Error(`Couldn't find issue for comment ${data.comment}`);
  571. }
  572. return this.getActionsResponse(
  573. {
  574. comments: issue.comments?.filter((c) => c.key !== data.comment),
  575. },
  576. issue.key
  577. );
  578. };
  579. handleGetUsers = () => {
  580. return this.reply({
  581. page: mockPaging(),
  582. users: [mockLoggedInUser() as unknown as RestUser],
  583. });
  584. };
  585. handleSearchIssueAuthors = () => {
  586. return this.reply(mockIssueAuthors());
  587. };
  588. handleSearchIssueTags = () => {
  589. return this.reply(['accessibility', 'android', 'unused']);
  590. };
  591. handleGetIssueChangelog = (_issue: string) => {
  592. return this.reply({
  593. changelog: [
  594. mockIssueChangelog({
  595. creationDate: '2018-09-01',
  596. diffs: [
  597. {
  598. key: 'status',
  599. newValue: IssueStatus.Reopened,
  600. oldValue: IssueStatus.Confirmed,
  601. },
  602. ],
  603. }),
  604. mockIssueChangelog({
  605. creationDate: '2018-10-01',
  606. diffs: [
  607. {
  608. key: 'assign',
  609. newValue: 'darth.vader',
  610. oldValue: 'luke.skywalker',
  611. },
  612. ],
  613. }),
  614. ],
  615. });
  616. };
  617. getActionsResponse = (overrides: Partial<RawIssue>, issueKey: string) => {
  618. const issueDataSelected = this.list.find((l) => l.issue.key === issueKey);
  619. if (!issueDataSelected) {
  620. throw new Error(`Coulnd't find issue for key ${issueKey}`);
  621. }
  622. issueDataSelected.issue = {
  623. ...issueDataSelected.issue,
  624. ...overrides,
  625. };
  626. return this.reply({
  627. issue: issueDataSelected.issue,
  628. });
  629. };
  630. reply<T>(response: T): Promise<T> {
  631. return Promise.resolve(cloneDeep(response));
  632. }
  633. }