github-issue-tower-defence-management 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.github/workflows/commit-lint.yml +0 -2
  2. package/.github/workflows/empty-format-test-job.yml +28 -0
  3. package/.github/workflows/test.yml +1 -0
  4. package/CHANGELOG.md +37 -38
  5. package/bin/adapter/entry-points/cli/index.js +2 -1
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +140 -17
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/repositories/BaseGitHubRepository.js +3 -9
  10. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  11. package/bin/adapter/repositories/GraphqlProjectRepository.js +30 -0
  12. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  13. package/bin/adapter/repositories/LocalStorageRepository.js +6 -0
  14. package/bin/adapter/repositories/LocalStorageRepository.js.map +1 -1
  15. package/bin/adapter/repositories/SystemDateRepository.js +15 -3
  16. package/bin/adapter/repositories/SystemDateRepository.js.map +1 -1
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +69 -9
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  19. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +28 -2
  20. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +1 -1
  21. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +7 -0
  22. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  23. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +19 -5
  24. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +1 -1
  25. package/bin/adapter/repositories/issue/RestIssueRepository.js +1 -0
  26. package/bin/adapter/repositories/issue/RestIssueRepository.js.map +1 -1
  27. package/bin/adapter/repositories/utils.js +1 -6
  28. package/bin/adapter/repositories/utils.js.map +1 -1
  29. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js +98 -65
  30. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js.map +1 -1
  31. package/bin/domain/usecases/AnalyzeStoriesUseCase.js +173 -0
  32. package/bin/domain/usecases/AnalyzeStoriesUseCase.js.map +1 -0
  33. package/bin/domain/usecases/ChangeStatusLongInReviewIssueUseCase.js +37 -0
  34. package/bin/domain/usecases/ChangeStatusLongInReviewIssueUseCase.js.map +1 -0
  35. package/bin/domain/usecases/ClearDependedIssueURLUseCase.js +66 -0
  36. package/bin/domain/usecases/ClearDependedIssueURLUseCase.js.map +1 -0
  37. package/bin/domain/usecases/ClearNextActionHourUseCase.js +8 -2
  38. package/bin/domain/usecases/ClearNextActionHourUseCase.js.map +1 -1
  39. package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js +63 -0
  40. package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js.map +1 -0
  41. package/bin/domain/usecases/CreateEstimationIssueUseCase.js +100 -0
  42. package/bin/domain/usecases/CreateEstimationIssueUseCase.js.map +1 -0
  43. package/bin/domain/usecases/HandleScheduledEventUseCase.js +136 -2
  44. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  45. package/bin/domain/usecases/utils.js +24 -0
  46. package/bin/domain/usecases/utils.js.map +1 -0
  47. package/package.json +2 -2
  48. package/src/adapter/entry-points/cli/index.test.ts +1 -0
  49. package/src/adapter/entry-points/cli/index.ts +3 -1
  50. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +92 -7
  51. package/src/adapter/repositories/AxiosSlackRepository.test.ts +3 -0
  52. package/src/adapter/repositories/BaseGitHubRepository.test.ts +3 -1
  53. package/src/adapter/repositories/BaseGitHubRepository.ts +3 -12
  54. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +30 -1
  55. package/src/adapter/repositories/GraphqlProjectRepository.ts +41 -0
  56. package/src/adapter/repositories/LocalStorageCacheRepository.test.ts +1 -0
  57. package/src/adapter/repositories/LocalStorageRepository.ts +6 -0
  58. package/src/adapter/repositories/SystemDateRepository.ts +17 -3
  59. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +8 -0
  60. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +123 -16
  61. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +3 -0
  62. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +10 -0
  63. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +37 -1
  64. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +7 -1
  65. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +15 -0
  66. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +7 -1
  67. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +22 -5
  68. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
  69. package/src/adapter/repositories/issue/RestIssueRepository.ts +5 -2
  70. package/src/adapter/repositories/utils.test.ts +16 -1
  71. package/src/adapter/repositories/utils.ts +1 -6
  72. package/src/domain/entities/Issue.ts +4 -0
  73. package/src/domain/entities/Project.ts +21 -4
  74. package/src/domain/usecases/AnalyzeProblemByIssueUseCase.ts +151 -115
  75. package/src/domain/usecases/AnalyzeStoriesUseCase.ts +296 -0
  76. package/src/domain/usecases/CONTRIBUTING_FOR_TEST.md +254 -0
  77. package/src/domain/usecases/ChangeStatusLongInReviewIssueUseCase.test.ts +204 -0
  78. package/src/domain/usecases/ChangeStatusLongInReviewIssueUseCase.ts +57 -0
  79. package/src/domain/usecases/ClearDependedIssueURLUseCase.test.ts +840 -0
  80. package/src/domain/usecases/ClearDependedIssueURLUseCase.ts +107 -0
  81. package/src/domain/usecases/ClearNextActionHourUseCase.ts +12 -2
  82. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +490 -0
  83. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +98 -0
  84. package/src/domain/usecases/CreateEstimationIssueUseCase.ts +157 -0
  85. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.test.ts +8 -0
  86. package/src/domain/usecases/HandleScheduledEventUseCase.ts +184 -2
  87. package/src/domain/usecases/adapter-interfaces/DateRepository.ts +2 -0
  88. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +13 -1
  89. package/src/domain/usecases/utils.ts +28 -0
  90. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts +1 -1
  91. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  92. package/types/adapter/repositories/BaseGitHubRepository.d.ts +3 -1
  93. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  94. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  95. package/types/adapter/repositories/LocalStorageRepository.d.ts +1 -0
  96. package/types/adapter/repositories/LocalStorageRepository.d.ts.map +1 -1
  97. package/types/adapter/repositories/SystemDateRepository.d.ts +2 -0
  98. package/types/adapter/repositories/SystemDateRepository.d.ts.map +1 -1
  99. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +15 -5
  100. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  101. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +7 -1
  102. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +1 -1
  103. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +3 -0
  104. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  105. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +1 -1
  106. package/types/adapter/repositories/issue/RestIssueRepository.d.ts +1 -1
  107. package/types/adapter/repositories/issue/RestIssueRepository.d.ts.map +1 -1
  108. package/types/adapter/repositories/utils.d.ts.map +1 -1
  109. package/types/domain/entities/Issue.d.ts +4 -0
  110. package/types/domain/entities/Issue.d.ts.map +1 -1
  111. package/types/domain/entities/Project.d.ts +21 -4
  112. package/types/domain/entities/Project.d.ts.map +1 -1
  113. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts +13 -8
  114. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts.map +1 -1
  115. package/types/domain/usecases/AnalyzeStoriesUseCase.d.ts +45 -0
  116. package/types/domain/usecases/AnalyzeStoriesUseCase.d.ts.map +1 -0
  117. package/types/domain/usecases/ChangeStatusLongInReviewIssueUseCase.d.ts +17 -0
  118. package/types/domain/usecases/ChangeStatusLongInReviewIssueUseCase.d.ts.map +1 -0
  119. package/types/domain/usecases/ClearDependedIssueURLUseCase.d.ts +13 -0
  120. package/types/domain/usecases/ClearDependedIssueURLUseCase.d.ts.map +1 -0
  121. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts.map +1 -1
  122. package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts +20 -0
  123. package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts.map +1 -0
  124. package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts +33 -0
  125. package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts.map +1 -0
  126. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +28 -2
  127. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  128. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts +2 -0
  129. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts.map +1 -1
  130. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +4 -1
  131. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  132. package/types/domain/usecases/utils.d.ts +5 -0
  133. package/types/domain/usecases/utils.d.ts.map +1 -0
  134. package/types/index.d.ts +1 -1
@@ -0,0 +1,107 @@
1
+ import { Issue } from '../entities/Issue';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { Project } from '../entities/Project';
4
+
5
+ export class ClearDependedIssueURLUseCase {
6
+ constructor(
7
+ readonly issueRepository: Pick<
8
+ IssueRepository,
9
+ 'clearProjectField' | 'createComment' | 'updateProjectTextField'
10
+ >,
11
+ ) {}
12
+
13
+ run = async (input: {
14
+ project: Project;
15
+ issues: Issue[];
16
+ cacheUsed: boolean;
17
+ }): Promise<void> => {
18
+ const dependedIssueUrlSeparatedByComma =
19
+ input.project.dependedIssueUrlSeparatedByComma;
20
+ if (!dependedIssueUrlSeparatedByComma || input.cacheUsed) {
21
+ return;
22
+ }
23
+ for (const issue of input.issues) {
24
+ if (issue.dependedIssueUrls.length <= 0 || issue.isClosed) {
25
+ continue;
26
+ }
27
+ const circularDependedIssueUrls = issue.dependedIssueUrls.filter(
28
+ (dependedIssueUrl) => {
29
+ // get all depended issues circularly
30
+ const circularDependedIssues = new Set<string>();
31
+ const stack = [dependedIssueUrl];
32
+ while (stack.length > 0) {
33
+ const url = stack.pop();
34
+ if (!url) {
35
+ throw new Error('url is undefined');
36
+ }
37
+ if (circularDependedIssues.has(url)) {
38
+ continue;
39
+ }
40
+ circularDependedIssues.add(url);
41
+ const dependedIssue = input.issues.find(
42
+ (issue) => issue.url === url,
43
+ );
44
+ if (!dependedIssue) {
45
+ continue;
46
+ }
47
+ stack.push(...dependedIssue.dependedIssueUrls);
48
+ }
49
+ return circularDependedIssues.has(issue.url);
50
+ },
51
+ );
52
+ if (circularDependedIssueUrls.length > 0) {
53
+ await this.issueRepository.clearProjectField(
54
+ input.project,
55
+ dependedIssueUrlSeparatedByComma.fieldId,
56
+ issue,
57
+ );
58
+ await this.issueRepository.createComment(
59
+ issue,
60
+ `Circular dependency removed:
61
+ ${circularDependedIssueUrls.map((url) => `- ${url}`).join('\n')}`,
62
+ );
63
+ continue;
64
+ }
65
+ const remainingDependedIssueUrls = issue.dependedIssueUrls.filter(
66
+ (dependedIssueUrl) =>
67
+ input.issues.some((issue) => {
68
+ const r = issue.url === dependedIssueUrl && !issue.isClosed;
69
+ return r;
70
+ }),
71
+ );
72
+ const closedDependedIssueUrls = issue.dependedIssueUrls.filter(
73
+ (dependedIssueUrl) =>
74
+ remainingDependedIssueUrls.indexOf(dependedIssueUrl) === -1,
75
+ );
76
+ if (
77
+ remainingDependedIssueUrls.length === issue.dependedIssueUrls.length
78
+ ) {
79
+ continue;
80
+ }
81
+ if (remainingDependedIssueUrls.length === 0) {
82
+ await this.issueRepository.clearProjectField(
83
+ input.project,
84
+ dependedIssueUrlSeparatedByComma.fieldId,
85
+ issue,
86
+ );
87
+ await this.issueRepository.createComment(
88
+ issue,
89
+ `Closed all depended issues:
90
+ ${closedDependedIssueUrls.map((url) => `- ${url}`).join('\n')}`,
91
+ );
92
+ } else {
93
+ await this.issueRepository.updateProjectTextField(
94
+ input.project,
95
+ dependedIssueUrlSeparatedByComma.fieldId,
96
+ issue,
97
+ remainingDependedIssueUrls.join(','),
98
+ );
99
+ await this.issueRepository.createComment(
100
+ issue,
101
+ `Closed depended issues:
102
+ ${closedDependedIssueUrls.map((url) => `- ${url}`).join('\n')}`,
103
+ );
104
+ }
105
+ }
106
+ };
107
+ }
@@ -14,9 +14,10 @@ export class ClearNextActionHourUseCase {
14
14
  cacheUsed: boolean;
15
15
  }): Promise<void> => {
16
16
  const nextActionHour = input.project.nextActionHour;
17
- if (!nextActionHour || input.cacheUsed) {
17
+ if (!nextActionHour) {
18
18
  return;
19
19
  }
20
+ const nextActionDate = input.project.nextActionDate;
20
21
  const targetDates = input.targetDates
21
22
  .filter((targetDate) => targetDate.getMinutes() === 45)
22
23
  .reverse();
@@ -26,7 +27,7 @@ export class ClearNextActionHourUseCase {
26
27
  const targetDate = new Date(
27
28
  targetDates[targetDates.length - 1].getTime() + 5 * 60 * 1000,
28
29
  );
29
- const targetHour = targetDate.getHours();
30
+ const targetHour = targetDate.getHours() + 1;
30
31
  const isTargetIssue = (issue: Issue): boolean => {
31
32
  return (
32
33
  issue.nextActionHour !== null &&
@@ -46,6 +47,15 @@ export class ClearNextActionHourUseCase {
46
47
  issue,
47
48
  );
48
49
  await new Promise((resolve) => setTimeout(resolve, 5000));
50
+ if (!nextActionDate) {
51
+ continue;
52
+ }
53
+ await this.issueRepository.clearProjectField(
54
+ input.project,
55
+ nextActionDate.fieldId,
56
+ issue,
57
+ );
58
+ await new Promise((resolve) => setTimeout(resolve, 5000));
49
59
  }
50
60
  };
51
61
  }
@@ -0,0 +1,490 @@
1
+ import { mock } from 'jest-mock-extended';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { ConvertCheckboxToIssueInStoryIssueUseCase } from './ConvertCheckboxToIssueInStoryIssueUseCase';
4
+ import { Project, StoryOption } from '../entities/Project';
5
+ import { Issue } from '../entities/Issue';
6
+ import { StoryObject, StoryObjectMap } from './HandleScheduledEventUseCase';
7
+
8
+ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
9
+ jest.setTimeout(5 * 60 * 1000);
10
+ const mockIssueRepository = mock<IssueRepository>();
11
+
12
+ describe('run', () => {
13
+ const basicProject = {
14
+ ...mock<Project>(),
15
+ story: {
16
+ name: 'Story Field',
17
+ fieldId: 'storyFieldId',
18
+ stories: [
19
+ { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
20
+ { ...mock<StoryOption>(), id: 'story2', name: 'Story 2' },
21
+ { ...mock<StoryOption>(), id: 'regular3', name: 'regular / Story 3' },
22
+ ],
23
+ workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
24
+ },
25
+ };
26
+
27
+ const basicStoryIssue1 = {
28
+ ...mock<Issue>(),
29
+ title: 'Story 1',
30
+ number: 123,
31
+ body: `- [ ] Task 1
32
+ - [ ] Task 2`,
33
+ url: 'https://github.com/org/repo/issues/123',
34
+ };
35
+
36
+ const basicStoryIssue2 = {
37
+ ...mock<Issue>(),
38
+ title: 'Story 2',
39
+ number: 456,
40
+ body: `- [ ] Task 3
41
+ - [ ] Task 4`,
42
+ url: 'https://github.com/org/repo/issues/456',
43
+ };
44
+
45
+ const basicStoryObject1: StoryObject = {
46
+ story: { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
47
+ storyIssue: basicStoryIssue1,
48
+ issues: [],
49
+ };
50
+ const basicStoryObject2: StoryObject = {
51
+ story: { ...mock<StoryOption>(), id: 'story2', name: 'Story 2' },
52
+ storyIssue: basicStoryIssue2,
53
+ issues: [],
54
+ };
55
+
56
+ const basicStoryObjectMap: StoryObjectMap = new Map([
57
+ ['Story 1', basicStoryObject1],
58
+ ['Story 2', basicStoryObject2],
59
+ ]);
60
+
61
+ const regularStoryProject = {
62
+ ...basicProject,
63
+ story: {
64
+ name: 'Story Field',
65
+ fieldId: 'storyFieldId',
66
+ stories: [
67
+ { ...mock<StoryOption>(), id: 'regular1', name: 'regular / Story 1' },
68
+ ],
69
+ workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
70
+ },
71
+ };
72
+
73
+ const regularStoryObjectMap = new Map([
74
+ [
75
+ 'regular / Story 1',
76
+ {
77
+ story: {
78
+ ...mock<StoryOption>(),
79
+ id: 'regular1',
80
+ name: 'regular / Story 1',
81
+ },
82
+ storyIssue: basicStoryIssue1,
83
+ issues: [],
84
+ },
85
+ ],
86
+ ]);
87
+
88
+ const testCases: {
89
+ name: string;
90
+ input: {
91
+ project: Project;
92
+ issues: Issue[];
93
+ cacheUsed: boolean;
94
+ org: string;
95
+ repo: string;
96
+ urlOfStoryView: string;
97
+ disabledStatus: string;
98
+ storyObjectMap: StoryObjectMap;
99
+ };
100
+ expectedThrowError?: Error;
101
+ expectedCreateNewIssueCalls: [
102
+ string,
103
+ string,
104
+ string,
105
+ string,
106
+ string[],
107
+ string[],
108
+ ][];
109
+ expectedUpdateIssueCalls: [Issue][];
110
+ expectedUpdateStoryCalls: [Project, Issue, string][];
111
+ expectedGetIssueByUrlCalls: [string][];
112
+ }[] = [
113
+ {
114
+ name: 'should not process when story is not set',
115
+ input: {
116
+ project: { ...basicProject, story: null },
117
+ issues: [basicStoryIssue1],
118
+ cacheUsed: false,
119
+ org: 'testOrg',
120
+ repo: 'testRepo',
121
+ urlOfStoryView: 'https://example.com',
122
+ disabledStatus: 'Closed',
123
+ storyObjectMap: basicStoryObjectMap,
124
+ },
125
+ expectedCreateNewIssueCalls: [],
126
+ expectedUpdateIssueCalls: [],
127
+ expectedUpdateStoryCalls: [],
128
+ expectedGetIssueByUrlCalls: [],
129
+ },
130
+ {
131
+ name: 'should not process when cache is used',
132
+ input: {
133
+ project: basicProject,
134
+ issues: [basicStoryIssue1],
135
+ cacheUsed: true,
136
+ org: 'testOrg',
137
+ repo: 'testRepo',
138
+ urlOfStoryView: 'https://example.com',
139
+ disabledStatus: 'Closed',
140
+ storyObjectMap: basicStoryObjectMap,
141
+ },
142
+ expectedCreateNewIssueCalls: [],
143
+ expectedUpdateIssueCalls: [],
144
+ expectedUpdateStoryCalls: [],
145
+ expectedGetIssueByUrlCalls: [],
146
+ },
147
+ {
148
+ name: 'should skip regular stories',
149
+ input: {
150
+ project: regularStoryProject,
151
+ issues: [basicStoryIssue1],
152
+ cacheUsed: false,
153
+ org: 'testOrg',
154
+ repo: 'testRepo',
155
+ urlOfStoryView: 'https://example.com',
156
+ disabledStatus: 'Closed',
157
+ storyObjectMap: regularStoryObjectMap,
158
+ },
159
+ expectedCreateNewIssueCalls: [],
160
+ expectedUpdateIssueCalls: [],
161
+ expectedUpdateStoryCalls: [],
162
+ expectedGetIssueByUrlCalls: [],
163
+ },
164
+ {
165
+ name: 'should throw error when story issue not found',
166
+ input: {
167
+ project: basicProject,
168
+ issues: [],
169
+ cacheUsed: false,
170
+ org: 'testOrg',
171
+ repo: 'testRepo',
172
+ urlOfStoryView: 'https://example.com',
173
+ disabledStatus: 'Closed',
174
+ storyObjectMap: basicStoryObjectMap,
175
+ },
176
+ expectedThrowError: new Error('Story issue not found: Story 1'),
177
+ expectedCreateNewIssueCalls: [],
178
+ expectedUpdateIssueCalls: [],
179
+ expectedUpdateStoryCalls: [],
180
+ expectedGetIssueByUrlCalls: [],
181
+ },
182
+
183
+ {
184
+ name: 'should skip closed story issues or disabled status',
185
+ input: {
186
+ project: basicProject,
187
+ issues: [
188
+ {
189
+ ...basicStoryIssue1,
190
+ state: 'CLOSED',
191
+ isClosed: true,
192
+ },
193
+ {
194
+ ...basicStoryIssue2,
195
+ status: 'Disabled',
196
+ },
197
+ ],
198
+ cacheUsed: false,
199
+ org: 'testOrg',
200
+ repo: 'testRepo',
201
+ urlOfStoryView: 'https://example.com',
202
+ disabledStatus: 'Disabled',
203
+ storyObjectMap: basicStoryObjectMap,
204
+ },
205
+ expectedCreateNewIssueCalls: [],
206
+ expectedUpdateIssueCalls: [],
207
+ expectedUpdateStoryCalls: [],
208
+ expectedGetIssueByUrlCalls: [],
209
+ },
210
+ {
211
+ name: 'should create new issues for checkboxes and update story issue',
212
+ input: {
213
+ project: basicProject,
214
+ issues: [basicStoryIssue1, basicStoryIssue2],
215
+ cacheUsed: false,
216
+ org: 'testOrg',
217
+ repo: 'testRepo',
218
+ urlOfStoryView: 'https://example.com',
219
+ disabledStatus: 'Closed',
220
+ storyObjectMap: basicStoryObjectMap,
221
+ },
222
+ expectedCreateNewIssueCalls: [
223
+ ['testOrg', 'testRepo', 'Task 1', '', [], []],
224
+ ['testOrg', 'testRepo', 'Task 2', '', [], []],
225
+ ['testOrg', 'testRepo', 'Task 3', '', [], []],
226
+ ['testOrg', 'testRepo', 'Task 4', '', [], []],
227
+ ],
228
+ expectedUpdateIssueCalls: [
229
+ [
230
+ {
231
+ ...basicStoryIssue1,
232
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
233
+ - [ ] Task 2`,
234
+ },
235
+ ],
236
+ [
237
+ {
238
+ ...basicStoryIssue1,
239
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
240
+ - [ ] https://github.com/testOrg/testRepo/issues/2`,
241
+ },
242
+ ],
243
+ [
244
+ {
245
+ ...basicStoryIssue2,
246
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
247
+ - [ ] Task 4`,
248
+ },
249
+ ],
250
+ [
251
+ {
252
+ ...basicStoryIssue2,
253
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
254
+ - [ ] https://github.com/testOrg/testRepo/issues/4`,
255
+ },
256
+ ],
257
+ ],
258
+ expectedUpdateStoryCalls: [
259
+ [
260
+ basicProject,
261
+ {
262
+ ...mock<Issue>(),
263
+ url: 'https://github.com/testOrg/testRepo/issues/1',
264
+ },
265
+ 'story1',
266
+ ],
267
+ [
268
+ basicProject,
269
+ {
270
+ ...mock<Issue>(),
271
+ url: 'https://github.com/testOrg/testRepo/issues/2',
272
+ },
273
+ 'story1',
274
+ ],
275
+ [
276
+ basicProject,
277
+ {
278
+ ...mock<Issue>(),
279
+ url: 'https://github.com/testOrg/testRepo/issues/3',
280
+ },
281
+ 'story2',
282
+ ],
283
+ [
284
+ basicProject,
285
+ {
286
+ ...mock<Issue>(),
287
+ url: 'https://github.com/testOrg/testRepo/issues/4',
288
+ },
289
+ 'story2',
290
+ ],
291
+ ],
292
+ expectedGetIssueByUrlCalls: [
293
+ ['https://github.com/testOrg/testRepo/issues/1'],
294
+ ['https://github.com/testOrg/testRepo/issues/2'],
295
+ ['https://github.com/testOrg/testRepo/issues/3'],
296
+ ['https://github.com/testOrg/testRepo/issues/4'],
297
+ ],
298
+ },
299
+ {
300
+ name: 'should create new issues with replaced STORYNAME for checkboxes and update story issue',
301
+ input: {
302
+ project: basicProject,
303
+ issues: [
304
+ {
305
+ ...basicStoryIssue1,
306
+ body: `- [ ] Task 1
307
+ - [ ] Task 2 for \`STORYNAME\``,
308
+ },
309
+ basicStoryIssue2,
310
+ ],
311
+ cacheUsed: false,
312
+ org: 'testOrg',
313
+ repo: 'testRepo',
314
+ urlOfStoryView: 'https://example.com',
315
+ disabledStatus: 'Closed',
316
+ storyObjectMap: basicStoryObjectMap,
317
+ },
318
+ expectedCreateNewIssueCalls: [
319
+ ['testOrg', 'testRepo', 'Task 1', '', [], []],
320
+ ['testOrg', 'testRepo', 'Task 2 for `Story 1 #123`', '', [], []],
321
+ ['testOrg', 'testRepo', 'Task 3', '', [], []],
322
+ ['testOrg', 'testRepo', 'Task 4', '', [], []],
323
+ ],
324
+ expectedUpdateIssueCalls: [
325
+ [
326
+ {
327
+ ...basicStoryIssue1,
328
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
329
+ - [ ] Task 2 for \`STORYNAME\``,
330
+ },
331
+ ],
332
+ [
333
+ {
334
+ ...basicStoryIssue1,
335
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
336
+ - [ ] https://github.com/testOrg/testRepo/issues/2`,
337
+ },
338
+ ],
339
+ [
340
+ {
341
+ ...basicStoryIssue2,
342
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
343
+ - [ ] Task 4`,
344
+ },
345
+ ],
346
+ [
347
+ {
348
+ ...basicStoryIssue2,
349
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
350
+ - [ ] https://github.com/testOrg/testRepo/issues/4`,
351
+ },
352
+ ],
353
+ ],
354
+ expectedUpdateStoryCalls: [
355
+ [
356
+ basicProject,
357
+ {
358
+ ...mock<Issue>(),
359
+ url: 'https://github.com/testOrg/testRepo/issues/1',
360
+ },
361
+ 'story1',
362
+ ],
363
+ [
364
+ basicProject,
365
+ {
366
+ ...mock<Issue>(),
367
+ url: 'https://github.com/testOrg/testRepo/issues/2',
368
+ },
369
+ 'story1',
370
+ ],
371
+ [
372
+ basicProject,
373
+ {
374
+ ...mock<Issue>(),
375
+ url: 'https://github.com/testOrg/testRepo/issues/3',
376
+ },
377
+ 'story2',
378
+ ],
379
+ [
380
+ basicProject,
381
+ {
382
+ ...mock<Issue>(),
383
+ url: 'https://github.com/testOrg/testRepo/issues/4',
384
+ },
385
+ 'story2',
386
+ ],
387
+ ],
388
+ expectedGetIssueByUrlCalls: [
389
+ ['https://github.com/testOrg/testRepo/issues/1'],
390
+ ['https://github.com/testOrg/testRepo/issues/2'],
391
+ ['https://github.com/testOrg/testRepo/issues/3'],
392
+ ['https://github.com/testOrg/testRepo/issues/4'],
393
+ ],
394
+ },
395
+ ];
396
+
397
+ testCases.forEach(
398
+ ({
399
+ name,
400
+ input,
401
+ expectedThrowError,
402
+ expectedCreateNewIssueCalls,
403
+ expectedUpdateIssueCalls,
404
+ expectedUpdateStoryCalls,
405
+ expectedGetIssueByUrlCalls,
406
+ }) => {
407
+ it(name, async () => {
408
+ jest.clearAllMocks();
409
+
410
+ let issueCounter = 1;
411
+ mockIssueRepository.createNewIssue.mockImplementation(
412
+ async () => issueCounter++,
413
+ );
414
+ mockIssueRepository.getIssueByUrl.mockImplementation(async (url) => ({
415
+ ...mock<Issue>(),
416
+ url,
417
+ }));
418
+
419
+ const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
420
+ mockIssueRepository,
421
+ );
422
+ try {
423
+ await useCase.run(input);
424
+ } catch (e) {
425
+ if (expectedThrowError === undefined) {
426
+ throw e;
427
+ }
428
+ if (e === null || typeof e !== 'object' || !('message' in e)) {
429
+ expect(e).toEqual(expectedThrowError);
430
+ }
431
+ }
432
+
433
+ expect(mockIssueRepository.createNewIssue.mock.calls).toEqual(
434
+ expectedCreateNewIssueCalls,
435
+ );
436
+ expect(mockIssueRepository.updateIssue.mock.calls).toEqual(
437
+ expectedUpdateIssueCalls,
438
+ );
439
+ expect(mockIssueRepository.updateStory.mock.calls).toEqual(
440
+ expectedUpdateStoryCalls,
441
+ );
442
+ expect(mockIssueRepository.getIssueByUrl.mock.calls).toEqual(
443
+ expectedGetIssueByUrlCalls,
444
+ );
445
+ });
446
+ },
447
+ );
448
+ });
449
+
450
+ describe('findCheckboxTextsNotCreatedIssue', () => {
451
+ const testCases: {
452
+ name: string;
453
+ input: string;
454
+ expected: string[];
455
+ }[] = [
456
+ {
457
+ name: 'should find checkbox texts not created as issues',
458
+ input: `- [ ] Task 1
459
+ - [ ] Task 2
460
+ - [x] Task 3
461
+ - [ ] #5
462
+ - [ ] #6
463
+ - [ ] https://github.com/org/repo/issues/1
464
+ - [ ] https://github.com/org/repo/issues/7 `,
465
+ expected: ['Task 1', 'Task 2'],
466
+ },
467
+ {
468
+ name: 'should return empty array when no checkboxes found',
469
+ input: 'No checkboxes here',
470
+ expected: [],
471
+ },
472
+ {
473
+ name: 'should return empty array when all checkboxes are issues',
474
+ input: `- [ ] https://github.com/org/repo/issues/1
475
+ - [ ] https://github.com/org/repo/issues/2`,
476
+ expected: [],
477
+ },
478
+ ];
479
+
480
+ testCases.forEach(({ name, input, expected }) => {
481
+ it(name, () => {
482
+ const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
483
+ mockIssueRepository,
484
+ );
485
+ const result = useCase.findCheckboxTextsNotCreatedIssue(input);
486
+ expect(result).toEqual(expected);
487
+ });
488
+ });
489
+ });
490
+ });