Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

IssuesServiceMock.ts 22KB

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