github-issue-tower-defence-management 2.3.0 → 2.5.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 (38) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/adapter/entry-points/cli/fleetConfig.js +19 -1
  3. package/bin/adapter/entry-points/cli/fleetConfig.js.map +1 -1
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +17 -4
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.js +27 -0
  7. package/bin/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.js.map +1 -0
  8. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -11
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  10. package/bin/domain/usecases/SubscriptionDisabledIssueUseCase.js +42 -0
  11. package/bin/domain/usecases/SubscriptionDisabledIssueUseCase.js.map +1 -0
  12. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +9 -4
  13. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
  14. package/package.json +1 -1
  15. package/src/adapter/entry-points/cli/fleetConfig.test.ts +86 -0
  16. package/src/adapter/entry-points/cli/fleetConfig.ts +38 -0
  17. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +24 -8
  18. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +27 -5
  19. package/src/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.ts +41 -0
  20. package/src/adapter/repositories/githubGraphqlClient.test.ts +2 -2
  21. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +0 -56
  22. package/src/domain/usecases/HandleScheduledEventUseCase.ts +0 -13
  23. package/src/domain/usecases/SubscriptionDisabledIssueUseCase.test.ts +197 -0
  24. package/src/domain/usecases/SubscriptionDisabledIssueUseCase.ts +74 -0
  25. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +83 -0
  26. package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +12 -4
  27. package/types/adapter/entry-points/cli/fleetConfig.d.ts +7 -0
  28. package/types/adapter/entry-points/cli/fleetConfig.d.ts.map +1 -1
  29. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  30. package/types/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.d.ts +9 -0
  31. package/types/adapter/entry-points/handlers/subscriptionDisabledTokenHandler.d.ts.map +1 -0
  32. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -4
  33. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  34. package/types/domain/usecases/SubscriptionDisabledIssueUseCase.d.ts +16 -0
  35. package/types/domain/usecases/SubscriptionDisabledIssueUseCase.d.ts.map +1 -0
  36. package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
  37. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +0 -1638
  38. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +0 -192
@@ -1,1638 +0,0 @@
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 '../entities/StoryObjectMap';
7
-
8
- describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
9
- jest.setTimeout(5 * 60 * 1000);
10
- const mockIssueRepository = mock<IssueRepository>();
11
-
12
- describe('run', () => {
13
- const basicStory = {
14
- name: 'Story Field',
15
- databaseId: 1,
16
- fieldId: 'storyFieldId',
17
- stories: [
18
- { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
19
- { ...mock<StoryOption>(), id: 'story2', name: 'Story 2' },
20
- { ...mock<StoryOption>(), id: 'regular3', name: 'regular / Story 3' },
21
- ],
22
- workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
23
- };
24
- const basicProject: Project = {
25
- ...mock<Project>(),
26
- story: basicStory,
27
- };
28
-
29
- const basicStoryIssue1 = {
30
- ...mock<Issue>(),
31
- title: 'Story 1',
32
- number: 123,
33
- body: `- [ ] Task 1
34
- - [ ] Task 2`,
35
- url: 'https://github.com/org/repo/issues/123',
36
- org: 'org',
37
- repo: 'repo',
38
- };
39
-
40
- const basicStoryIssue2 = {
41
- ...mock<Issue>(),
42
- title: 'Story 2',
43
- number: 456,
44
- body: `- [ ] Task 3
45
- - [ ] Task 4`,
46
- url: 'https://github.com/org/repo/issues/456',
47
- org: 'org',
48
- repo: 'repo',
49
- };
50
-
51
- const storyIssue1WithStoryNamePlaceholder = {
52
- ...basicStoryIssue1,
53
- body: `- [ ] Task 1
54
- - [ ] Task 2 for \`STORYNAME\``,
55
- };
56
-
57
- const basicStoryObject1: StoryObject = {
58
- story: { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
59
- storyIssue: basicStoryIssue1,
60
- issues: [],
61
- };
62
- const basicStoryObject2: StoryObject = {
63
- story: { ...mock<StoryOption>(), id: 'story2', name: 'Story 2' },
64
- storyIssue: basicStoryIssue2,
65
- issues: [],
66
- };
67
-
68
- const basicStoryObjectMap: StoryObjectMap = new Map([
69
- ['Story 1', basicStoryObject1],
70
- ['Story 2', basicStoryObject2],
71
- ]);
72
-
73
- const singleStoryProject: Project = {
74
- ...basicProject,
75
- story: {
76
- name: 'Story Field',
77
- databaseId: 1,
78
- fieldId: 'storyFieldId',
79
- stories: [{ ...mock<StoryOption>(), id: 'story1', name: 'Story 1' }],
80
- workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
81
- },
82
- };
83
-
84
- const singleStoryObjectMap: StoryObjectMap = new Map([
85
- ['Story 1', basicStoryObject1],
86
- ]);
87
-
88
- const regularStoryProject = {
89
- ...basicProject,
90
- story: {
91
- name: 'Story Field',
92
- fieldId: 'storyFieldId',
93
- databaseId: 1,
94
- stories: [
95
- { ...mock<StoryOption>(), id: 'regular1', name: 'regular / Story 1' },
96
- ],
97
- workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
98
- },
99
- };
100
-
101
- const regularStoryObjectMap = new Map([
102
- [
103
- 'regular / Story 1',
104
- {
105
- story: {
106
- ...mock<StoryOption>(),
107
- id: 'regular1',
108
- name: 'regular / Story 1',
109
- },
110
- storyIssue: basicStoryIssue1,
111
- issues: [],
112
- },
113
- ],
114
- ]);
115
-
116
- const testCases: {
117
- name: string;
118
- input: {
119
- project: Project;
120
- issues: Issue[];
121
- cacheUsed: boolean;
122
- urlOfStoryView: string;
123
- storyObjectMap: StoryObjectMap;
124
- manager: string;
125
- createTaskFromStoryBodyCheckboxEnabled: boolean;
126
- };
127
- expectedThrowError?: Error;
128
- expectedCreateNewIssueCalls: [
129
- string,
130
- string,
131
- string,
132
- string,
133
- string[],
134
- string[],
135
- ][];
136
- expectedUpdateIssueBodyCalls: [Issue, string][];
137
- expectedUpdateStoryCalls: [Project, Issue, string][];
138
- expectedIssueUrlReads: [string][];
139
- expectedAddIssueToProjectCalls: [Project, string][];
140
- }[] = [
141
- {
142
- name: 'should not process when story is not set',
143
- input: {
144
- project: { ...basicProject, story: null },
145
- issues: [basicStoryIssue1],
146
- cacheUsed: false,
147
- urlOfStoryView: 'https://example.com',
148
- storyObjectMap: basicStoryObjectMap,
149
- manager: 'manager',
150
- createTaskFromStoryBodyCheckboxEnabled: true,
151
- },
152
- expectedCreateNewIssueCalls: [],
153
- expectedUpdateIssueBodyCalls: [],
154
- expectedUpdateStoryCalls: [],
155
- expectedIssueUrlReads: [],
156
- expectedAddIssueToProjectCalls: [],
157
- },
158
- {
159
- name: 'should process story issues even when cache is used',
160
- input: {
161
- project: basicProject,
162
- issues: [basicStoryIssue1, basicStoryIssue2],
163
- cacheUsed: true,
164
- urlOfStoryView: 'https://example.com',
165
- storyObjectMap: basicStoryObjectMap,
166
- manager: 'manager',
167
- createTaskFromStoryBodyCheckboxEnabled: true,
168
- },
169
- expectedCreateNewIssueCalls: [
170
- [
171
- 'org',
172
- 'repo',
173
- 'Task 1',
174
- '- Parent issue: https://github.com/org/repo/issues/123',
175
- ['manager'],
176
- [],
177
- ],
178
- [
179
- 'org',
180
- 'repo',
181
- 'Task 2',
182
- '- Parent issue: https://github.com/org/repo/issues/123',
183
- ['manager'],
184
- [],
185
- ],
186
- [
187
- 'org',
188
- 'repo',
189
- 'Task 3',
190
- '- Parent issue: https://github.com/org/repo/issues/456',
191
- ['manager'],
192
- [],
193
- ],
194
- [
195
- 'org',
196
- 'repo',
197
- 'Task 4',
198
- '- Parent issue: https://github.com/org/repo/issues/456',
199
- ['manager'],
200
- [],
201
- ],
202
- ],
203
- expectedUpdateIssueBodyCalls: [
204
- [
205
- basicStoryIssue1,
206
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
207
-
208
- - [ ] Task 1
209
- - [ ] Task 2`,
210
- ],
211
- [
212
- basicStoryIssue1,
213
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
214
-
215
- - [ ] https://github.com/org/repo/issues/1
216
- - [ ] Task 2`,
217
- ],
218
- [
219
- basicStoryIssue1,
220
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
221
-
222
- - [ ] https://github.com/org/repo/issues/1
223
- - [ ] https://github.com/org/repo/issues/2`,
224
- ],
225
- [
226
- basicStoryIssue2,
227
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
228
-
229
- - [ ] Task 3
230
- - [ ] Task 4`,
231
- ],
232
- [
233
- basicStoryIssue2,
234
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
235
-
236
- - [ ] https://github.com/org/repo/issues/3
237
- - [ ] Task 4`,
238
- ],
239
- [
240
- basicStoryIssue2,
241
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
242
-
243
- - [ ] https://github.com/org/repo/issues/3
244
- - [ ] https://github.com/org/repo/issues/4`,
245
- ],
246
- ],
247
- expectedUpdateStoryCalls: [
248
- [
249
- basicProject,
250
- {
251
- ...mock<Issue>(),
252
- url: 'https://github.com/org/repo/issues/1',
253
- },
254
- 'story1',
255
- ],
256
- [
257
- basicProject,
258
- {
259
- ...mock<Issue>(),
260
- url: 'https://github.com/org/repo/issues/2',
261
- },
262
- 'story1',
263
- ],
264
- [
265
- basicProject,
266
- {
267
- ...mock<Issue>(),
268
- url: 'https://github.com/org/repo/issues/3',
269
- },
270
- 'story2',
271
- ],
272
- [
273
- basicProject,
274
- {
275
- ...mock<Issue>(),
276
- url: 'https://github.com/org/repo/issues/4',
277
- },
278
- 'story2',
279
- ],
280
- ],
281
- expectedIssueUrlReads: [
282
- ['https://github.com/org/repo/issues/123'],
283
- ['https://github.com/org/repo/issues/1'],
284
- ['https://github.com/org/repo/issues/2'],
285
- ['https://github.com/org/repo/issues/456'],
286
- ['https://github.com/org/repo/issues/3'],
287
- ['https://github.com/org/repo/issues/4'],
288
- ],
289
- expectedAddIssueToProjectCalls: [
290
- [basicProject, 'https://github.com/org/repo/issues/1'],
291
- [basicProject, 'https://github.com/org/repo/issues/2'],
292
- [basicProject, 'https://github.com/org/repo/issues/3'],
293
- [basicProject, 'https://github.com/org/repo/issues/4'],
294
- ],
295
- },
296
- {
297
- name: 'should skip regular stories',
298
- input: {
299
- project: regularStoryProject,
300
- issues: [basicStoryIssue1],
301
- cacheUsed: false,
302
- urlOfStoryView: 'https://example.com',
303
- storyObjectMap: regularStoryObjectMap,
304
- manager: 'manager',
305
- createTaskFromStoryBodyCheckboxEnabled: true,
306
- },
307
- expectedCreateNewIssueCalls: [],
308
- expectedUpdateIssueBodyCalls: [],
309
- expectedUpdateStoryCalls: [],
310
- expectedIssueUrlReads: [],
311
- expectedAddIssueToProjectCalls: [],
312
- },
313
- {
314
- name: 'should throw error when story issue not found',
315
- input: {
316
- project: basicProject,
317
- issues: [],
318
- cacheUsed: false,
319
- urlOfStoryView: 'https://example.com',
320
- storyObjectMap: basicStoryObjectMap,
321
- manager: 'manager',
322
- createTaskFromStoryBodyCheckboxEnabled: true,
323
- },
324
- expectedThrowError: new Error('Story issue not found: Story 1'),
325
- expectedCreateNewIssueCalls: [],
326
- expectedUpdateIssueBodyCalls: [],
327
- expectedUpdateStoryCalls: [],
328
- expectedIssueUrlReads: [],
329
- expectedAddIssueToProjectCalls: [],
330
- },
331
-
332
- {
333
- name: 'should skip closed story issues and only place the story view link on icebox story issues',
334
- input: {
335
- project: basicProject,
336
- issues: [
337
- {
338
- ...basicStoryIssue1,
339
- state: 'CLOSED',
340
- isClosed: true,
341
- },
342
- {
343
- ...basicStoryIssue2,
344
- status: 'Icebox',
345
- },
346
- ],
347
- cacheUsed: false,
348
- urlOfStoryView: 'https://example.com',
349
- storyObjectMap: basicStoryObjectMap,
350
- manager: 'manager',
351
- createTaskFromStoryBodyCheckboxEnabled: true,
352
- },
353
- expectedCreateNewIssueCalls: [],
354
- expectedUpdateIssueBodyCalls: [
355
- [
356
- {
357
- ...basicStoryIssue2,
358
- status: 'Icebox',
359
- },
360
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
361
-
362
- - [ ] Task 3
363
- - [ ] Task 4`,
364
- ],
365
- ],
366
- expectedUpdateStoryCalls: [],
367
- expectedIssueUrlReads: [['https://github.com/org/repo/issues/456']],
368
- expectedAddIssueToProjectCalls: [],
369
- },
370
- {
371
- name: 'should choose the longest prefix-matching title when several issues prefix-match one story option',
372
- input: {
373
- project: singleStoryProject,
374
- issues: [
375
- {
376
- ...basicStoryIssue1,
377
- title: 'Story',
378
- number: 900,
379
- url: 'https://github.com/org/repo/issues/900',
380
- },
381
- {
382
- ...basicStoryIssue1,
383
- title: 'Story 1',
384
- number: 901,
385
- url: 'https://github.com/org/repo/issues/901',
386
- },
387
- ],
388
- cacheUsed: false,
389
- urlOfStoryView: 'https://example.com',
390
- storyObjectMap: singleStoryObjectMap,
391
- manager: 'manager',
392
- createTaskFromStoryBodyCheckboxEnabled: false,
393
- },
394
- expectedCreateNewIssueCalls: [],
395
- expectedUpdateIssueBodyCalls: [
396
- [
397
- {
398
- ...basicStoryIssue1,
399
- title: 'Story 1',
400
- number: 901,
401
- url: 'https://github.com/org/repo/issues/901',
402
- },
403
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
404
-
405
- - [ ] Task 1
406
- - [ ] Task 2`,
407
- ],
408
- ],
409
- expectedUpdateStoryCalls: [],
410
- expectedIssueUrlReads: [['https://github.com/org/repo/issues/901']],
411
- expectedAddIssueToProjectCalls: [],
412
- },
413
- {
414
- name: 'should choose the longest prefix-matching title regardless of the order the issues arrive in',
415
- input: {
416
- project: singleStoryProject,
417
- issues: [
418
- {
419
- ...basicStoryIssue1,
420
- title: 'Story 1',
421
- number: 901,
422
- url: 'https://github.com/org/repo/issues/901',
423
- },
424
- {
425
- ...basicStoryIssue1,
426
- title: 'Story',
427
- number: 900,
428
- url: 'https://github.com/org/repo/issues/900',
429
- },
430
- ],
431
- cacheUsed: false,
432
- urlOfStoryView: 'https://example.com',
433
- storyObjectMap: singleStoryObjectMap,
434
- manager: 'manager',
435
- createTaskFromStoryBodyCheckboxEnabled: false,
436
- },
437
- expectedCreateNewIssueCalls: [],
438
- expectedUpdateIssueBodyCalls: [
439
- [
440
- {
441
- ...basicStoryIssue1,
442
- title: 'Story 1',
443
- number: 901,
444
- url: 'https://github.com/org/repo/issues/901',
445
- },
446
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
447
-
448
- - [ ] Task 1
449
- - [ ] Task 2`,
450
- ],
451
- ],
452
- expectedUpdateStoryCalls: [],
453
- expectedIssueUrlReads: [['https://github.com/org/repo/issues/901']],
454
- expectedAddIssueToProjectCalls: [],
455
- },
456
- {
457
- name: 'should consolidate a stale story view link on an icebox story issue without reading it when the link is already correct',
458
- input: {
459
- project: singleStoryProject,
460
- issues: [
461
- {
462
- ...basicStoryIssue1,
463
- status: 'Icebox',
464
- body: `https://example.com?sliceBy%5Bvalue%5D=Story%201
465
-
466
- - [ ] Task 1
467
- - [ ] Task 2`,
468
- },
469
- ],
470
- cacheUsed: false,
471
- urlOfStoryView: 'https://example.com',
472
- storyObjectMap: singleStoryObjectMap,
473
- manager: 'manager',
474
- createTaskFromStoryBodyCheckboxEnabled: true,
475
- },
476
- expectedCreateNewIssueCalls: [],
477
- expectedUpdateIssueBodyCalls: [],
478
- expectedUpdateStoryCalls: [],
479
- expectedIssueUrlReads: [],
480
- expectedAddIssueToProjectCalls: [],
481
- },
482
- {
483
- name: 'should add story view link and create new issues for checkboxes',
484
- input: {
485
- project: basicProject,
486
- issues: [basicStoryIssue1, basicStoryIssue2],
487
- cacheUsed: false,
488
- urlOfStoryView: 'https://example.com',
489
- storyObjectMap: basicStoryObjectMap,
490
- manager: 'manager',
491
- createTaskFromStoryBodyCheckboxEnabled: true,
492
- },
493
- expectedCreateNewIssueCalls: [
494
- [
495
- 'org',
496
- 'repo',
497
- 'Task 1',
498
- '- Parent issue: https://github.com/org/repo/issues/123',
499
- ['manager'],
500
- [],
501
- ],
502
- [
503
- 'org',
504
- 'repo',
505
- 'Task 2',
506
- '- Parent issue: https://github.com/org/repo/issues/123',
507
- ['manager'],
508
- [],
509
- ],
510
- [
511
- 'org',
512
- 'repo',
513
- 'Task 3',
514
- '- Parent issue: https://github.com/org/repo/issues/456',
515
- ['manager'],
516
- [],
517
- ],
518
- [
519
- 'org',
520
- 'repo',
521
- 'Task 4',
522
- '- Parent issue: https://github.com/org/repo/issues/456',
523
- ['manager'],
524
- [],
525
- ],
526
- ],
527
- expectedUpdateIssueBodyCalls: [
528
- [
529
- basicStoryIssue1,
530
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
531
-
532
- - [ ] Task 1
533
- - [ ] Task 2`,
534
- ],
535
- [
536
- basicStoryIssue1,
537
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
538
-
539
- - [ ] https://github.com/org/repo/issues/1
540
- - [ ] Task 2`,
541
- ],
542
- [
543
- basicStoryIssue1,
544
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
545
-
546
- - [ ] https://github.com/org/repo/issues/1
547
- - [ ] https://github.com/org/repo/issues/2`,
548
- ],
549
- [
550
- basicStoryIssue2,
551
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
552
-
553
- - [ ] Task 3
554
- - [ ] Task 4`,
555
- ],
556
- [
557
- basicStoryIssue2,
558
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
559
-
560
- - [ ] https://github.com/org/repo/issues/3
561
- - [ ] Task 4`,
562
- ],
563
- [
564
- basicStoryIssue2,
565
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
566
-
567
- - [ ] https://github.com/org/repo/issues/3
568
- - [ ] https://github.com/org/repo/issues/4`,
569
- ],
570
- ],
571
- expectedUpdateStoryCalls: [
572
- [
573
- basicProject,
574
- {
575
- ...mock<Issue>(),
576
- url: 'https://github.com/org/repo/issues/1',
577
- },
578
- 'story1',
579
- ],
580
- [
581
- basicProject,
582
- {
583
- ...mock<Issue>(),
584
- url: 'https://github.com/org/repo/issues/2',
585
- },
586
- 'story1',
587
- ],
588
- [
589
- basicProject,
590
- {
591
- ...mock<Issue>(),
592
- url: 'https://github.com/org/repo/issues/3',
593
- },
594
- 'story2',
595
- ],
596
- [
597
- basicProject,
598
- {
599
- ...mock<Issue>(),
600
- url: 'https://github.com/org/repo/issues/4',
601
- },
602
- 'story2',
603
- ],
604
- ],
605
- expectedIssueUrlReads: [
606
- ['https://github.com/org/repo/issues/123'],
607
- ['https://github.com/org/repo/issues/1'],
608
- ['https://github.com/org/repo/issues/2'],
609
- ['https://github.com/org/repo/issues/456'],
610
- ['https://github.com/org/repo/issues/3'],
611
- ['https://github.com/org/repo/issues/4'],
612
- ],
613
- expectedAddIssueToProjectCalls: [
614
- [basicProject, 'https://github.com/org/repo/issues/1'],
615
- [basicProject, 'https://github.com/org/repo/issues/2'],
616
- [basicProject, 'https://github.com/org/repo/issues/3'],
617
- [basicProject, 'https://github.com/org/repo/issues/4'],
618
- ],
619
- },
620
- {
621
- name: 'should not add story view link when it already exists',
622
- input: {
623
- project: {
624
- ...basicProject,
625
- story: {
626
- ...basicStory,
627
- stories: [
628
- { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
629
- ],
630
- },
631
- },
632
- issues: [
633
- {
634
- ...basicStoryIssue1,
635
- body: `https://example.com?sliceBy%5Bvalue%5D=Story%201
636
-
637
- - [ ] Task 1`,
638
- },
639
- ],
640
- cacheUsed: false,
641
- urlOfStoryView: 'https://example.com',
642
- storyObjectMap: new Map([['Story 1', basicStoryObject1]]),
643
- manager: 'manager',
644
- createTaskFromStoryBodyCheckboxEnabled: true,
645
- },
646
- expectedCreateNewIssueCalls: [
647
- [
648
- 'org',
649
- 'repo',
650
- 'Task 1',
651
- '- Parent issue: https://github.com/org/repo/issues/123',
652
- ['manager'],
653
- [],
654
- ],
655
- ],
656
- expectedUpdateIssueBodyCalls: [
657
- [
658
- {
659
- ...basicStoryIssue1,
660
- body: `https://example.com?sliceBy%5Bvalue%5D=Story%201
661
-
662
- - [ ] Task 1`,
663
- },
664
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
665
-
666
- - [ ] https://github.com/org/repo/issues/1`,
667
- ],
668
- ],
669
- expectedUpdateStoryCalls: [
670
- [
671
- {
672
- ...basicProject,
673
- story: {
674
- ...basicStory,
675
- stories: [
676
- { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
677
- ],
678
- },
679
- },
680
- {
681
- ...mock<Issue>(),
682
- url: 'https://github.com/org/repo/issues/1',
683
- },
684
- 'story1',
685
- ],
686
- ],
687
- expectedIssueUrlReads: [
688
- ['https://github.com/org/repo/issues/123'],
689
- ['https://github.com/org/repo/issues/1'],
690
- ],
691
- expectedAddIssueToProjectCalls: [
692
- [
693
- {
694
- ...basicProject,
695
- story: {
696
- ...basicStory,
697
- stories: [
698
- { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
699
- ],
700
- },
701
- },
702
- 'https://github.com/org/repo/issues/1',
703
- ],
704
- ],
705
- },
706
- {
707
- name: 'should add story view link even when no checkboxes exist',
708
- input: {
709
- project: {
710
- ...basicProject,
711
- story: {
712
- ...basicStory,
713
- stories: [
714
- { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
715
- ],
716
- },
717
- },
718
- issues: [
719
- {
720
- ...basicStoryIssue1,
721
- body: 'Some description without checkboxes',
722
- },
723
- ],
724
- cacheUsed: false,
725
- urlOfStoryView: 'https://example.com',
726
- storyObjectMap: new Map([
727
- [
728
- 'Story 1',
729
- {
730
- ...basicStoryObject1,
731
- storyIssue: {
732
- ...basicStoryIssue1,
733
- body: 'Some description without checkboxes',
734
- },
735
- },
736
- ],
737
- ]),
738
- manager: 'manager',
739
- createTaskFromStoryBodyCheckboxEnabled: true,
740
- },
741
- expectedCreateNewIssueCalls: [],
742
- expectedUpdateIssueBodyCalls: [
743
- [
744
- {
745
- ...basicStoryIssue1,
746
- body: 'Some description without checkboxes',
747
- },
748
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
749
-
750
- Some description without checkboxes`,
751
- ],
752
- ],
753
- expectedUpdateStoryCalls: [],
754
- expectedIssueUrlReads: [['https://github.com/org/repo/issues/123']],
755
- expectedAddIssueToProjectCalls: [],
756
- },
757
- {
758
- name: 'should create new issues with replaced STORYNAME for checkboxes and update story issue',
759
- input: {
760
- project: basicProject,
761
- issues: [storyIssue1WithStoryNamePlaceholder, basicStoryIssue2],
762
- cacheUsed: false,
763
- urlOfStoryView: 'https://example.com',
764
- storyObjectMap: basicStoryObjectMap,
765
- manager: 'manager',
766
- createTaskFromStoryBodyCheckboxEnabled: true,
767
- },
768
- expectedCreateNewIssueCalls: [
769
- [
770
- 'org',
771
- 'repo',
772
- 'Task 1',
773
- '- Parent issue: https://github.com/org/repo/issues/123',
774
- ['manager'],
775
- [],
776
- ],
777
- [
778
- 'org',
779
- 'repo',
780
- 'Task 2 for `Story 1 #123`',
781
- '- Parent issue: https://github.com/org/repo/issues/123',
782
- ['manager'],
783
- [],
784
- ],
785
- [
786
- 'org',
787
- 'repo',
788
- 'Task 3',
789
- '- Parent issue: https://github.com/org/repo/issues/456',
790
- ['manager'],
791
- [],
792
- ],
793
- [
794
- 'org',
795
- 'repo',
796
- 'Task 4',
797
- '- Parent issue: https://github.com/org/repo/issues/456',
798
- ['manager'],
799
- [],
800
- ],
801
- ],
802
- expectedUpdateIssueBodyCalls: [
803
- [
804
- storyIssue1WithStoryNamePlaceholder,
805
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
806
-
807
- - [ ] Task 1
808
- - [ ] Task 2 for \`STORYNAME\``,
809
- ],
810
- [
811
- storyIssue1WithStoryNamePlaceholder,
812
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
813
-
814
- - [ ] https://github.com/org/repo/issues/1
815
- - [ ] Task 2 for \`STORYNAME\``,
816
- ],
817
- [
818
- storyIssue1WithStoryNamePlaceholder,
819
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
820
-
821
- - [ ] https://github.com/org/repo/issues/1
822
- - [ ] https://github.com/org/repo/issues/2`,
823
- ],
824
- [
825
- basicStoryIssue2,
826
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
827
-
828
- - [ ] Task 3
829
- - [ ] Task 4`,
830
- ],
831
- [
832
- basicStoryIssue2,
833
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
834
-
835
- - [ ] https://github.com/org/repo/issues/3
836
- - [ ] Task 4`,
837
- ],
838
- [
839
- basicStoryIssue2,
840
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
841
-
842
- - [ ] https://github.com/org/repo/issues/3
843
- - [ ] https://github.com/org/repo/issues/4`,
844
- ],
845
- ],
846
- expectedUpdateStoryCalls: [
847
- [
848
- basicProject,
849
- {
850
- ...mock<Issue>(),
851
- url: 'https://github.com/org/repo/issues/1',
852
- },
853
- 'story1',
854
- ],
855
- [
856
- basicProject,
857
- {
858
- ...mock<Issue>(),
859
- url: 'https://github.com/org/repo/issues/2',
860
- },
861
- 'story1',
862
- ],
863
- [
864
- basicProject,
865
- {
866
- ...mock<Issue>(),
867
- url: 'https://github.com/org/repo/issues/3',
868
- },
869
- 'story2',
870
- ],
871
- [
872
- basicProject,
873
- {
874
- ...mock<Issue>(),
875
- url: 'https://github.com/org/repo/issues/4',
876
- },
877
- 'story2',
878
- ],
879
- ],
880
- expectedIssueUrlReads: [
881
- ['https://github.com/org/repo/issues/123'],
882
- ['https://github.com/org/repo/issues/1'],
883
- ['https://github.com/org/repo/issues/2'],
884
- ['https://github.com/org/repo/issues/456'],
885
- ['https://github.com/org/repo/issues/3'],
886
- ['https://github.com/org/repo/issues/4'],
887
- ],
888
- expectedAddIssueToProjectCalls: [
889
- [basicProject, 'https://github.com/org/repo/issues/1'],
890
- [basicProject, 'https://github.com/org/repo/issues/2'],
891
- [basicProject, 'https://github.com/org/repo/issues/3'],
892
- [basicProject, 'https://github.com/org/repo/issues/4'],
893
- ],
894
- },
895
- {
896
- name: 'should create new issues in same repo as parent story issue when different stories have different repos',
897
- input: {
898
- project: basicProject,
899
- issues: [
900
- {
901
- ...basicStoryIssue1,
902
- org: 'orgA',
903
- repo: 'repoA',
904
- body: `- [ ] Task 1`,
905
- },
906
- {
907
- ...basicStoryIssue2,
908
- org: 'orgB',
909
- repo: 'repoB',
910
- body: `- [ ] Task 2`,
911
- },
912
- ],
913
- cacheUsed: false,
914
- urlOfStoryView: 'https://example.com',
915
- storyObjectMap: new Map([
916
- [
917
- 'Story 1',
918
- {
919
- story: {
920
- ...mock<StoryOption>(),
921
- id: 'story1',
922
- name: 'Story 1',
923
- },
924
- storyIssue: {
925
- ...basicStoryIssue1,
926
- org: 'orgA',
927
- repo: 'repoA',
928
- body: `- [ ] Task 1`,
929
- },
930
- issues: [],
931
- },
932
- ],
933
- [
934
- 'Story 2',
935
- {
936
- story: {
937
- ...mock<StoryOption>(),
938
- id: 'story2',
939
- name: 'Story 2',
940
- },
941
- storyIssue: {
942
- ...basicStoryIssue2,
943
- org: 'orgB',
944
- repo: 'repoB',
945
- body: `- [ ] Task 2`,
946
- },
947
- issues: [],
948
- },
949
- ],
950
- ]),
951
- manager: 'manager',
952
- createTaskFromStoryBodyCheckboxEnabled: true,
953
- },
954
- expectedCreateNewIssueCalls: [
955
- [
956
- 'orgA',
957
- 'repoA',
958
- 'Task 1',
959
- '- Parent issue: https://github.com/org/repo/issues/123',
960
- ['manager'],
961
- [],
962
- ],
963
- [
964
- 'orgB',
965
- 'repoB',
966
- 'Task 2',
967
- '- Parent issue: https://github.com/org/repo/issues/456',
968
- ['manager'],
969
- [],
970
- ],
971
- ],
972
- expectedUpdateIssueBodyCalls: [
973
- [
974
- {
975
- ...basicStoryIssue1,
976
- org: 'orgA',
977
- repo: 'repoA',
978
- body: `- [ ] Task 1`,
979
- },
980
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
981
-
982
- - [ ] Task 1`,
983
- ],
984
- [
985
- {
986
- ...basicStoryIssue1,
987
- org: 'orgA',
988
- repo: 'repoA',
989
- body: `- [ ] Task 1`,
990
- },
991
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
992
-
993
- - [ ] https://github.com/orgA/repoA/issues/1`,
994
- ],
995
- [
996
- {
997
- ...basicStoryIssue2,
998
- org: 'orgB',
999
- repo: 'repoB',
1000
- body: `- [ ] Task 2`,
1001
- },
1002
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
1003
-
1004
- - [ ] Task 2`,
1005
- ],
1006
- [
1007
- {
1008
- ...basicStoryIssue2,
1009
- org: 'orgB',
1010
- repo: 'repoB',
1011
- body: `- [ ] Task 2`,
1012
- },
1013
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
1014
-
1015
- - [ ] https://github.com/orgB/repoB/issues/2`,
1016
- ],
1017
- ],
1018
- expectedUpdateStoryCalls: [
1019
- [
1020
- basicProject,
1021
- {
1022
- ...mock<Issue>(),
1023
- url: 'https://github.com/orgA/repoA/issues/1',
1024
- },
1025
- 'story1',
1026
- ],
1027
- [
1028
- basicProject,
1029
- {
1030
- ...mock<Issue>(),
1031
- url: 'https://github.com/orgB/repoB/issues/2',
1032
- },
1033
- 'story2',
1034
- ],
1035
- ],
1036
- expectedIssueUrlReads: [
1037
- ['https://github.com/org/repo/issues/123'],
1038
- ['https://github.com/orgA/repoA/issues/1'],
1039
- ['https://github.com/org/repo/issues/456'],
1040
- ['https://github.com/orgB/repoB/issues/2'],
1041
- ],
1042
- expectedAddIssueToProjectCalls: [
1043
- [basicProject, 'https://github.com/orgA/repoA/issues/1'],
1044
- [basicProject, 'https://github.com/orgB/repoB/issues/2'],
1045
- ],
1046
- },
1047
- {
1048
- name: 'should not create task issues but still inject story view link when createTaskFromStoryBodyCheckboxEnabled is false',
1049
- input: {
1050
- project: basicProject,
1051
- issues: [basicStoryIssue1, basicStoryIssue2],
1052
- cacheUsed: true,
1053
- urlOfStoryView: 'https://example.com',
1054
- storyObjectMap: basicStoryObjectMap,
1055
- manager: 'manager',
1056
- createTaskFromStoryBodyCheckboxEnabled: false,
1057
- },
1058
- expectedCreateNewIssueCalls: [],
1059
- expectedUpdateIssueBodyCalls: [
1060
- [
1061
- basicStoryIssue1,
1062
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
1063
-
1064
- - [ ] Task 1
1065
- - [ ] Task 2`,
1066
- ],
1067
- [
1068
- basicStoryIssue2,
1069
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
1070
-
1071
- - [ ] Task 3
1072
- - [ ] Task 4`,
1073
- ],
1074
- ],
1075
- expectedUpdateStoryCalls: [],
1076
- expectedIssueUrlReads: [
1077
- ['https://github.com/org/repo/issues/123'],
1078
- ['https://github.com/org/repo/issues/456'],
1079
- ],
1080
- expectedAddIssueToProjectCalls: [],
1081
- },
1082
- {
1083
- name: 'should create task issues from checkboxes when createTaskFromStoryBodyCheckboxEnabled is true',
1084
- input: {
1085
- project: singleStoryProject,
1086
- issues: [basicStoryIssue1],
1087
- cacheUsed: false,
1088
- urlOfStoryView: 'https://example.com',
1089
- storyObjectMap: singleStoryObjectMap,
1090
- manager: 'manager',
1091
- createTaskFromStoryBodyCheckboxEnabled: true,
1092
- },
1093
- expectedCreateNewIssueCalls: [
1094
- [
1095
- 'org',
1096
- 'repo',
1097
- 'Task 1',
1098
- '- Parent issue: https://github.com/org/repo/issues/123',
1099
- ['manager'],
1100
- [],
1101
- ],
1102
- [
1103
- 'org',
1104
- 'repo',
1105
- 'Task 2',
1106
- '- Parent issue: https://github.com/org/repo/issues/123',
1107
- ['manager'],
1108
- [],
1109
- ],
1110
- ],
1111
- expectedUpdateIssueBodyCalls: [
1112
- [
1113
- basicStoryIssue1,
1114
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
1115
-
1116
- - [ ] Task 1
1117
- - [ ] Task 2`,
1118
- ],
1119
- [
1120
- basicStoryIssue1,
1121
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
1122
-
1123
- - [ ] https://github.com/org/repo/issues/1
1124
- - [ ] Task 2`,
1125
- ],
1126
- [
1127
- basicStoryIssue1,
1128
- `https://example.com?sliceBy%5Bvalue%5D=Story%201
1129
-
1130
- - [ ] https://github.com/org/repo/issues/1
1131
- - [ ] https://github.com/org/repo/issues/2`,
1132
- ],
1133
- ],
1134
- expectedUpdateStoryCalls: [
1135
- [
1136
- singleStoryProject,
1137
- {
1138
- ...mock<Issue>(),
1139
- url: 'https://github.com/org/repo/issues/1',
1140
- },
1141
- 'story1',
1142
- ],
1143
- [
1144
- singleStoryProject,
1145
- {
1146
- ...mock<Issue>(),
1147
- url: 'https://github.com/org/repo/issues/2',
1148
- },
1149
- 'story1',
1150
- ],
1151
- ],
1152
- expectedIssueUrlReads: [
1153
- ['https://github.com/org/repo/issues/123'],
1154
- ['https://github.com/org/repo/issues/1'],
1155
- ['https://github.com/org/repo/issues/2'],
1156
- ],
1157
- expectedAddIssueToProjectCalls: [
1158
- [singleStoryProject, 'https://github.com/org/repo/issues/1'],
1159
- [singleStoryProject, 'https://github.com/org/repo/issues/2'],
1160
- ],
1161
- },
1162
- ];
1163
-
1164
- testCases.forEach(
1165
- ({
1166
- name,
1167
- input,
1168
- expectedThrowError,
1169
- expectedCreateNewIssueCalls,
1170
- expectedUpdateIssueBodyCalls,
1171
- expectedUpdateStoryCalls,
1172
- expectedIssueUrlReads,
1173
- expectedAddIssueToProjectCalls,
1174
- }) => {
1175
- it(name, async () => {
1176
- jest.clearAllMocks();
1177
-
1178
- let issueCounter = 1;
1179
- mockIssueRepository.createNewIssue.mockImplementation(
1180
- async () => issueCounter++,
1181
- );
1182
- const storyIssuesByUrl = new Map(
1183
- input.issues.map((issue) => [issue.url, issue]),
1184
- );
1185
- mockIssueRepository.getIssueByUrl.mockImplementation(async (url) => {
1186
- const storyIssue = storyIssuesByUrl.get(url);
1187
- if (storyIssue) {
1188
- return storyIssue;
1189
- }
1190
- return {
1191
- ...mock<Issue>(),
1192
- url,
1193
- };
1194
- });
1195
- mockIssueRepository.getIssueBodyByUrl.mockImplementation(
1196
- async (url) => storyIssuesByUrl.get(url)?.body ?? null,
1197
- );
1198
-
1199
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1200
- mockIssueRepository,
1201
- );
1202
- try {
1203
- await useCase.run(input);
1204
- } catch (e) {
1205
- if (expectedThrowError === undefined) {
1206
- throw e;
1207
- }
1208
- if (e === null || typeof e !== 'object' || !('message' in e)) {
1209
- expect(e).toEqual(expectedThrowError);
1210
- }
1211
- }
1212
-
1213
- expect(mockIssueRepository.createNewIssue.mock.calls).toEqual(
1214
- expectedCreateNewIssueCalls,
1215
- );
1216
- expect(mockIssueRepository.updateIssue).not.toHaveBeenCalled();
1217
- expect(mockIssueRepository.updateIssueBody.mock.calls).toEqual(
1218
- expectedUpdateIssueBodyCalls,
1219
- );
1220
- expect(mockIssueRepository.updateStory.mock.calls).toEqual(
1221
- expectedUpdateStoryCalls,
1222
- );
1223
- expect(mockIssueRepository.getIssueBodyByUrl.mock.calls).toEqual(
1224
- expectedIssueUrlReads.filter(([url]) => storyIssuesByUrl.has(url)),
1225
- );
1226
- expect(mockIssueRepository.getIssueByUrl.mock.calls).toEqual(
1227
- expectedIssueUrlReads.filter(([url]) => !storyIssuesByUrl.has(url)),
1228
- );
1229
- expect(mockIssueRepository.addIssueToProject.mock.calls).toEqual(
1230
- expectedAddIssueToProjectCalls,
1231
- );
1232
- });
1233
- },
1234
- );
1235
-
1236
- it('skips a story whose story issue was deleted on GitHub and continues with the remaining stories', async () => {
1237
- jest.clearAllMocks();
1238
- const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
1239
- mockIssueRepository.getIssueBodyByUrl.mockImplementation(async (url) => {
1240
- if (url === basicStoryIssue1.url) {
1241
- return null;
1242
- }
1243
- return basicStoryIssue2.body;
1244
- });
1245
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1246
- mockIssueRepository,
1247
- );
1248
-
1249
- await useCase.run({
1250
- project: basicProject,
1251
- issues: [basicStoryIssue1, basicStoryIssue2],
1252
- cacheUsed: false,
1253
- urlOfStoryView: 'https://example.com',
1254
- storyObjectMap: basicStoryObjectMap,
1255
- manager: 'ManagerName',
1256
- createTaskFromStoryBodyCheckboxEnabled: false,
1257
- });
1258
-
1259
- expect(warnSpy).toHaveBeenCalledWith(
1260
- expect.stringContaining(basicStoryIssue1.url),
1261
- );
1262
- expect(mockIssueRepository.getIssueBodyByUrl.mock.calls).toEqual([
1263
- [basicStoryIssue1.url],
1264
- [basicStoryIssue2.url],
1265
- ]);
1266
- expect(mockIssueRepository.updateIssueBody.mock.calls).toEqual([
1267
- [
1268
- basicStoryIssue2,
1269
- `https://example.com?sliceBy%5Bvalue%5D=Story%202
1270
-
1271
- - [ ] Task 3
1272
- - [ ] Task 4`,
1273
- ],
1274
- ]);
1275
- warnSpy.mockRestore();
1276
- });
1277
-
1278
- it('propagates errors thrown while fetching the story issue body by URL', async () => {
1279
- jest.clearAllMocks();
1280
- const fetchError = new Error('Failed to fetch body: 502 Bad Gateway');
1281
- mockIssueRepository.getIssueBodyByUrl.mockRejectedValue(fetchError);
1282
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1283
- mockIssueRepository,
1284
- );
1285
-
1286
- await expect(
1287
- useCase.run({
1288
- project: basicProject,
1289
- issues: [basicStoryIssue1, basicStoryIssue2],
1290
- cacheUsed: false,
1291
- urlOfStoryView: 'https://example.com',
1292
- storyObjectMap: basicStoryObjectMap,
1293
- manager: 'ManagerName',
1294
- createTaskFromStoryBodyCheckboxEnabled: false,
1295
- }),
1296
- ).rejects.toThrow('502 Bad Gateway');
1297
- });
1298
-
1299
- describe('story view link position', () => {
1300
- const storyViewLink = 'https://example.com?sliceBy%5Bvalue%5D=Story%201';
1301
-
1302
- const runWithBody = async (body: string): Promise<void> => {
1303
- jest.clearAllMocks();
1304
- mockIssueRepository.getIssueBodyByUrl.mockResolvedValue(body);
1305
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1306
- mockIssueRepository,
1307
- );
1308
- await useCase.run({
1309
- project: singleStoryProject,
1310
- issues: [basicStoryIssue1],
1311
- cacheUsed: false,
1312
- urlOfStoryView: 'https://example.com',
1313
- storyObjectMap: singleStoryObjectMap,
1314
- manager: 'ManagerName',
1315
- createTaskFromStoryBodyCheckboxEnabled: false,
1316
- });
1317
- };
1318
-
1319
- it('moves the story view link back to the first line when it sits in the middle of the body', async () => {
1320
- await runWithBody(`Background text
1321
-
1322
- ${storyViewLink}
1323
-
1324
- - [ ] Task 1`);
1325
-
1326
- expect(mockIssueRepository.updateIssueBody.mock.calls).toHaveLength(1);
1327
- expect(mockIssueRepository.updateIssueBody.mock.calls[0][1])
1328
- .toBe(`${storyViewLink}
1329
-
1330
- Background text
1331
-
1332
- - [ ] Task 1`);
1333
- });
1334
-
1335
- it('moves the story view link back to the first line when it sits at the bottom of the body', async () => {
1336
- await runWithBody(`Background text
1337
-
1338
- - [ ] Task 1
1339
-
1340
- ${storyViewLink}`);
1341
-
1342
- expect(mockIssueRepository.updateIssueBody.mock.calls).toHaveLength(1);
1343
- expect(mockIssueRepository.updateIssueBody.mock.calls[0][1])
1344
- .toBe(`${storyViewLink}
1345
-
1346
- Background text
1347
-
1348
- - [ ] Task 1`);
1349
- });
1350
-
1351
- it('leaves a single story view link on the first line when the body holds more than one copy', async () => {
1352
- await runWithBody(`${storyViewLink}
1353
-
1354
- Background text
1355
-
1356
- ${storyViewLink}`);
1357
-
1358
- expect(mockIssueRepository.updateIssueBody.mock.calls).toHaveLength(1);
1359
- expect(mockIssueRepository.updateIssueBody.mock.calls[0][1])
1360
- .toBe(`${storyViewLink}
1361
-
1362
- Background text`);
1363
- });
1364
-
1365
- it('does not update the issue when the story view link is already the first line', async () => {
1366
- await runWithBody(`${storyViewLink}
1367
-
1368
- - [ ] Task 1`);
1369
-
1370
- expect(mockIssueRepository.updateIssueBody).not.toHaveBeenCalled();
1371
- });
1372
- });
1373
-
1374
- describe('story view link written for an earlier view id', () => {
1375
- const urlOfStoryView =
1376
- 'https://github.com/users/TestUser/projects/48/views/48';
1377
- const currentStoryViewLink = `${urlOfStoryView}?sliceBy%5Bvalue%5D=Story%201`;
1378
- const earlierStoryViewLink =
1379
- 'https://github.com/users/TestUser/projects/48/views/44?sliceBy%5Bvalue%5D=Story%201';
1380
- const otherStoryViewLink = `${urlOfStoryView}?sliceBy%5Bvalue%5D=Story%202`;
1381
-
1382
- const runWithBody = async (body: string): Promise<void> => {
1383
- jest.clearAllMocks();
1384
- mockIssueRepository.getIssueBodyByUrl.mockResolvedValue(body);
1385
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1386
- mockIssueRepository,
1387
- );
1388
- await useCase.run({
1389
- project: singleStoryProject,
1390
- issues: [basicStoryIssue1],
1391
- cacheUsed: false,
1392
- urlOfStoryView,
1393
- storyObjectMap: singleStoryObjectMap,
1394
- manager: 'ManagerName',
1395
- createTaskFromStoryBodyCheckboxEnabled: false,
1396
- });
1397
- };
1398
-
1399
- it('removes a story view link that names an earlier view id of the same project', async () => {
1400
- await runWithBody(`${currentStoryViewLink}
1401
-
1402
- ${earlierStoryViewLink}
1403
-
1404
- - [ ] Task 1`);
1405
-
1406
- expect(mockIssueRepository.updateIssueBody.mock.calls).toHaveLength(1);
1407
- expect(mockIssueRepository.updateIssueBody.mock.calls[0][1])
1408
- .toBe(`${currentStoryViewLink}
1409
-
1410
- - [ ] Task 1`);
1411
- });
1412
-
1413
- it('lifts a story view link that names an earlier view id to the current link on the first line', async () => {
1414
- await runWithBody(`Background text
1415
-
1416
- ${earlierStoryViewLink}`);
1417
-
1418
- expect(mockIssueRepository.updateIssueBody.mock.calls).toHaveLength(1);
1419
- expect(mockIssueRepository.updateIssueBody.mock.calls[0][1])
1420
- .toBe(`${currentStoryViewLink}
1421
-
1422
- Background text`);
1423
- });
1424
-
1425
- it('keeps a story view link that names a different story', async () => {
1426
- await runWithBody(`${currentStoryViewLink}
1427
-
1428
- ${otherStoryViewLink}`);
1429
-
1430
- expect(mockIssueRepository.updateIssueBody).not.toHaveBeenCalled();
1431
- });
1432
- });
1433
-
1434
- describe('story issue refetch', () => {
1435
- const storyViewLink = 'https://example.com?sliceBy%5Bvalue%5D=Story%201';
1436
- const storyIssueWithLinkOnFirstLine: Issue = {
1437
- ...basicStoryIssue1,
1438
- body: `${storyViewLink}
1439
-
1440
- - [ ] Task 1`,
1441
- };
1442
-
1443
- const runWithBoardIssue = async (
1444
- boardIssue: Issue,
1445
- createTaskFromStoryBodyCheckboxEnabled: boolean,
1446
- ): Promise<void> => {
1447
- jest.clearAllMocks();
1448
- mockIssueRepository.getIssueBodyByUrl.mockResolvedValue(
1449
- storyIssueWithLinkOnFirstLine.body,
1450
- );
1451
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1452
- mockIssueRepository,
1453
- );
1454
- await useCase.run({
1455
- project: singleStoryProject,
1456
- issues: [boardIssue],
1457
- cacheUsed: false,
1458
- urlOfStoryView: 'https://example.com',
1459
- storyObjectMap: singleStoryObjectMap,
1460
- manager: 'ManagerName',
1461
- createTaskFromStoryBodyCheckboxEnabled,
1462
- });
1463
- };
1464
-
1465
- it('does not fetch the story issue again when task creation from story body checkboxes is disabled and the story view link is already the first line', async () => {
1466
- await runWithBoardIssue(storyIssueWithLinkOnFirstLine, false);
1467
-
1468
- expect(mockIssueRepository.getIssueBodyByUrl).not.toHaveBeenCalled();
1469
- expect(mockIssueRepository.updateIssueBody).not.toHaveBeenCalled();
1470
- });
1471
-
1472
- it('fetches the story issue when task creation from story body checkboxes is enabled', async () => {
1473
- await runWithBoardIssue(storyIssueWithLinkOnFirstLine, true);
1474
-
1475
- expect(mockIssueRepository.getIssueBodyByUrl.mock.calls[0]).toEqual([
1476
- storyIssueWithLinkOnFirstLine.url,
1477
- ]);
1478
- });
1479
-
1480
- it('fetches the story issue when the story view link is missing from the body', async () => {
1481
- await runWithBoardIssue(basicStoryIssue1, false);
1482
-
1483
- expect(mockIssueRepository.getIssueBodyByUrl.mock.calls).toEqual([
1484
- [basicStoryIssue1.url],
1485
- ]);
1486
- });
1487
- });
1488
-
1489
- describe('story body read and write over REST', () => {
1490
- const storyViewLink = 'https://example.com?sliceBy%5Bvalue%5D=Story%201';
1491
- const bodyWithStoryViewLinkOnFirstLine = `${storyViewLink}
1492
-
1493
- - [ ] Task 1`;
1494
-
1495
- const runSingleStory = async (
1496
- createTaskFromStoryBodyCheckboxEnabled = false,
1497
- ): Promise<void> => {
1498
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1499
- mockIssueRepository,
1500
- );
1501
- await useCase.run({
1502
- project: singleStoryProject,
1503
- issues: [basicStoryIssue1],
1504
- cacheUsed: false,
1505
- urlOfStoryView: 'https://example.com',
1506
- storyObjectMap: singleStoryObjectMap,
1507
- manager: 'ManagerName',
1508
- createTaskFromStoryBodyCheckboxEnabled,
1509
- });
1510
- };
1511
-
1512
- it('reads the story body through the issue body endpoint and issues no project item query for it', async () => {
1513
- jest.clearAllMocks();
1514
- mockIssueRepository.getIssueBodyByUrl.mockResolvedValue(
1515
- bodyWithStoryViewLinkOnFirstLine,
1516
- );
1517
-
1518
- await runSingleStory();
1519
-
1520
- expect(mockIssueRepository.getIssueBodyByUrl.mock.calls).toEqual([
1521
- [basicStoryIssue1.url],
1522
- ]);
1523
- expect(mockIssueRepository.getIssueByUrl).not.toHaveBeenCalled();
1524
- });
1525
-
1526
- it('writes only the body when the story view link has to be placed on the first line', async () => {
1527
- jest.clearAllMocks();
1528
- mockIssueRepository.getIssueBodyByUrl.mockResolvedValue('- [ ] Task 1');
1529
-
1530
- await runSingleStory();
1531
-
1532
- expect(mockIssueRepository.updateIssue).not.toHaveBeenCalled();
1533
- expect(mockIssueRepository.updateIssueBody.mock.calls).toEqual([
1534
- [
1535
- expect.objectContaining({
1536
- org: basicStoryIssue1.org,
1537
- repo: basicStoryIssue1.repo,
1538
- number: basicStoryIssue1.number,
1539
- }),
1540
- `${storyViewLink}
1541
-
1542
- - [ ] Task 1`,
1543
- ],
1544
- ]);
1545
- });
1546
- });
1547
- });
1548
-
1549
- describe('buildStoryViewLink', () => {
1550
- const testCases: {
1551
- name: string;
1552
- urlOfStoryView: string;
1553
- storyName: string;
1554
- expected: string;
1555
- }[] = [
1556
- {
1557
- name: 'should build story view link with encoded story name',
1558
- urlOfStoryView: 'https://github.com/users/TestUser/projects/1/views/1',
1559
- storyName: 'Story 1',
1560
- expected:
1561
- 'https://github.com/users/TestUser/projects/1/views/1?sliceBy%5Bvalue%5D=Story%201',
1562
- },
1563
- {
1564
- name: 'should handle story name with special characters',
1565
- urlOfStoryView: 'https://github.com/users/TestUser/projects/1/views/1',
1566
- storyName: 'planning business trip for next spring',
1567
- expected:
1568
- 'https://github.com/users/TestUser/projects/1/views/1?sliceBy%5Bvalue%5D=planning%20business%20trip%20for%20next%20spring',
1569
- },
1570
- {
1571
- name: 'should handle story name with hash',
1572
- urlOfStoryView: 'https://github.com/users/TestUser/projects/1/views/1',
1573
- storyName: 'Story #1',
1574
- expected:
1575
- 'https://github.com/users/TestUser/projects/1/views/1?sliceBy%5Bvalue%5D=Story%20%231',
1576
- },
1577
- ];
1578
-
1579
- testCases.forEach(({ name, urlOfStoryView, storyName, expected }) => {
1580
- it(name, () => {
1581
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1582
- mockIssueRepository,
1583
- );
1584
- const result = useCase.buildStoryViewLink(urlOfStoryView, storyName);
1585
- expect(result).toEqual(expected);
1586
- });
1587
- });
1588
- });
1589
-
1590
- describe('findCheckboxTextsNotCreatedIssue', () => {
1591
- const testCases: {
1592
- name: string;
1593
- input: string;
1594
- expected: string[];
1595
- }[] = [
1596
- {
1597
- name: 'should find checkbox texts not created as issues',
1598
- input: `- [ ] Task 1
1599
- - [ ] Task 2
1600
- - [x] Task 3
1601
- - [ ] #5
1602
- - [ ] #6
1603
- - [ ] https://github.com/org/repo/issues/1
1604
- - [ ] https://github.com/org/repo/issues/7 `,
1605
- expected: ['Task 1', 'Task 2'],
1606
- },
1607
- {
1608
- name: 'should return empty array when no checkboxes found',
1609
- input: 'No checkboxes here',
1610
- expected: [],
1611
- },
1612
- {
1613
- name: 'should return empty array when all checkboxes are issues',
1614
- input: `- [ ] https://github.com/org/repo/issues/1
1615
- - [ ] https://github.com/org/repo/issues/2`,
1616
- expected: [],
1617
- },
1618
- {
1619
- name: 'should ignore empty checkboxes',
1620
- input: `- [ ] Task 1
1621
- - [ ]
1622
- - [ ] Task 2
1623
- - [ ]`,
1624
- expected: ['Task 1', 'Task 2'],
1625
- },
1626
- ];
1627
-
1628
- testCases.forEach(({ name, input, expected }) => {
1629
- it(name, () => {
1630
- const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
1631
- mockIssueRepository,
1632
- );
1633
- const result = useCase.findCheckboxTextsNotCreatedIssue(input);
1634
- expect(result).toEqual(expected);
1635
- });
1636
- });
1637
- });
1638
- });