github-issue-tower-defence-management 2.5.0 → 2.6.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.
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-C5cOzhyn.js → index-CaxYBHiT.js} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/BrowserGitHubProjectRepository.js +94 -0
- package/bin/adapter/repositories/BrowserGitHubProjectRepository.js.map +1 -0
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +9 -1
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/StatusDefaultRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/StatusDefaultRepository.js.map +1 -0
- package/package.json +2 -1
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +140 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +21 -19
- package/src/adapter/entry-points/console/ui-dist/assets/{index-C5cOzhyn.js → index-CaxYBHiT.js} +1 -1
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -0
- package/src/adapter/repositories/BrowserGitHubProjectRepository.test.ts +108 -0
- package/src/adapter/repositories/BrowserGitHubProjectRepository.ts +118 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +143 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +21 -0
- package/src/domain/usecases/adapter-interfaces/StatusDefaultRepository.ts +5 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/BrowserGitHubProjectRepository.d.ts +10 -0
- package/types/adapter/repositories/BrowserGitHubProjectRepository.d.ts.map +1 -0
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts +3 -1
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/StatusDefaultRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/StatusDefaultRepository.d.ts.map +1 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CaxYBHiT.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CLHvrafO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -79,6 +79,7 @@ import { AgentDesignationLabelAdoptUseCase } from '../../../domain/usecases/Agen
|
|
|
79
79
|
import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
|
|
80
80
|
import { ProjectRequiredFieldCreateUseCase } from '../../../domain/usecases/ProjectRequiredFieldCreateUseCase';
|
|
81
81
|
import { SetupTowerDefenceProjectUseCase } from '../../../domain/usecases/SetupTowerDefenceProjectUseCase';
|
|
82
|
+
import { BrowserGitHubProjectRepository } from '../../repositories/BrowserGitHubProjectRepository';
|
|
82
83
|
import { DailySecurityScanUseCase } from '../../../domain/usecases/DailySecurityScanUseCase';
|
|
83
84
|
import { QualityCheckAdvanceUseCase } from '../../../domain/usecases/QualityCheckAdvanceUseCase';
|
|
84
85
|
import { ReopenedDoneIssueRevertUseCase } from '../../../domain/usecases/ReopenedDoneIssueRevertUseCase';
|
|
@@ -434,9 +435,15 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
434
435
|
);
|
|
435
436
|
const projectRequiredFieldCreateUseCase =
|
|
436
437
|
new ProjectRequiredFieldCreateUseCase(projectRepository);
|
|
438
|
+
const browserGitHubProjectRepository = new BrowserGitHubProjectRepository(
|
|
439
|
+
process.env.GITHUB_USERNAME,
|
|
440
|
+
process.env.GITHUB_PASSWORD,
|
|
441
|
+
process.env.GITHUB_TOTP_SECRET,
|
|
442
|
+
);
|
|
437
443
|
const setupTowerDefenceProjectUseCase = new SetupTowerDefenceProjectUseCase(
|
|
438
444
|
projectRepository,
|
|
439
445
|
issueRepository,
|
|
446
|
+
browserGitHubProjectRepository,
|
|
440
447
|
);
|
|
441
448
|
const actionAnnouncement = new ActionAnnouncementUseCase(issueRepository);
|
|
442
449
|
const setWorkflowManagementIssueToStoryUseCase =
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { BrowserGitHubProjectRepository } from './BrowserGitHubProjectRepository';
|
|
2
|
+
import { Project } from '../../domain/entities/Project';
|
|
3
|
+
|
|
4
|
+
jest.mock('playwright', () => ({
|
|
5
|
+
chromium: {
|
|
6
|
+
launch: jest.fn(),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const buildProject = (url: string): Project => ({
|
|
11
|
+
id: 'project-1',
|
|
12
|
+
url,
|
|
13
|
+
databaseId: 1,
|
|
14
|
+
name: 'test-project',
|
|
15
|
+
status: {
|
|
16
|
+
name: 'Status',
|
|
17
|
+
fieldId: 'status-field-1',
|
|
18
|
+
statuses: [
|
|
19
|
+
{
|
|
20
|
+
id: 'opt-aw',
|
|
21
|
+
name: 'Awaiting Workspace',
|
|
22
|
+
color: 'BLUE',
|
|
23
|
+
description: '',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
nextActionDate: null,
|
|
28
|
+
nextActionHour: null,
|
|
29
|
+
story: null,
|
|
30
|
+
remainingEstimationMinutes: null,
|
|
31
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
32
|
+
completionDate50PercentConfidence: null,
|
|
33
|
+
agent: null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('BrowserGitHubProjectRepository', () => {
|
|
37
|
+
it('should call console.warn and return without throwing when username is undefined', async () => {
|
|
38
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
39
|
+
const repo = new BrowserGitHubProjectRepository(
|
|
40
|
+
undefined,
|
|
41
|
+
'password',
|
|
42
|
+
undefined,
|
|
43
|
+
);
|
|
44
|
+
const project = buildProject(
|
|
45
|
+
'https://github.com/users/testuser/projects/1',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
await expect(
|
|
49
|
+
repo.setStatusFieldDefault(project, 'opt-aw'),
|
|
50
|
+
).resolves.toBeUndefined();
|
|
51
|
+
|
|
52
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
53
|
+
expect.stringContaining('GITHUB_USERNAME or GITHUB_PASSWORD is unset'),
|
|
54
|
+
);
|
|
55
|
+
warnSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should call console.warn and return without throwing when password is undefined', async () => {
|
|
59
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
60
|
+
const repo = new BrowserGitHubProjectRepository(
|
|
61
|
+
'username',
|
|
62
|
+
undefined,
|
|
63
|
+
undefined,
|
|
64
|
+
);
|
|
65
|
+
const project = buildProject(
|
|
66
|
+
'https://github.com/users/testuser/projects/1',
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
repo.setStatusFieldDefault(project, 'opt-aw'),
|
|
71
|
+
).resolves.toBeUndefined();
|
|
72
|
+
|
|
73
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
74
|
+
expect.stringContaining('GITHUB_USERNAME or GITHUB_PASSWORD is unset'),
|
|
75
|
+
);
|
|
76
|
+
warnSpy.mockRestore();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should throw when projectLocationFromUrl returns null for an invalid URL', async () => {
|
|
80
|
+
const repo = new BrowserGitHubProjectRepository(
|
|
81
|
+
'username',
|
|
82
|
+
'password',
|
|
83
|
+
undefined,
|
|
84
|
+
);
|
|
85
|
+
const project = buildProject('https://example.com/invalid/url');
|
|
86
|
+
|
|
87
|
+
await expect(repo.setStatusFieldDefault(project, 'opt-aw')).rejects.toThrow(
|
|
88
|
+
'cannot parse project URL',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw when optionId is not found in project status list', async () => {
|
|
93
|
+
const repo = new BrowserGitHubProjectRepository(
|
|
94
|
+
'username',
|
|
95
|
+
'password',
|
|
96
|
+
undefined,
|
|
97
|
+
);
|
|
98
|
+
const project = buildProject(
|
|
99
|
+
'https://github.com/users/testuser/projects/1',
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await expect(
|
|
103
|
+
repo.setStatusFieldDefault(project, 'nonexistent-id'),
|
|
104
|
+
).rejects.toThrow(
|
|
105
|
+
'option with id "nonexistent-id" not found in project status list',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHmac } from 'crypto';
|
|
2
|
+
import { chromium } from 'playwright';
|
|
3
|
+
import { Project } from '../../domain/entities/Project';
|
|
4
|
+
import { StatusDefaultRepository } from '../../domain/usecases/adapter-interfaces/StatusDefaultRepository';
|
|
5
|
+
import { projectLocationFromUrl } from './RestProjectRepository';
|
|
6
|
+
|
|
7
|
+
const generateTotp = (secret: string): string => {
|
|
8
|
+
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
9
|
+
const secretUpper = secret.toUpperCase().replace(/=+$/, '');
|
|
10
|
+
let bytes = 0n;
|
|
11
|
+
let bitsLeft = 0;
|
|
12
|
+
const output: number[] = [];
|
|
13
|
+
for (const char of secretUpper) {
|
|
14
|
+
const val = base32Chars.indexOf(char);
|
|
15
|
+
if (val === -1) continue;
|
|
16
|
+
bytes = (bytes << 5n) | BigInt(val);
|
|
17
|
+
bitsLeft += 5;
|
|
18
|
+
if (bitsLeft >= 8) {
|
|
19
|
+
bitsLeft -= 8;
|
|
20
|
+
output.push(Number((bytes >> BigInt(bitsLeft)) & 0xffn));
|
|
21
|
+
bytes &= (1n << BigInt(bitsLeft)) - 1n;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const key = Buffer.from(output);
|
|
25
|
+
const counter = BigInt(Math.floor(Date.now() / 1000 / 30));
|
|
26
|
+
const counterBuffer = Buffer.alloc(8);
|
|
27
|
+
counterBuffer.writeBigUInt64BE(counter);
|
|
28
|
+
const hmac = createHmac('sha1', key).update(counterBuffer).digest();
|
|
29
|
+
const offset = hmac[hmac.length - 1] & 0x0f;
|
|
30
|
+
const code =
|
|
31
|
+
((hmac[offset] & 0x7f) << 24) |
|
|
32
|
+
((hmac[offset + 1] & 0xff) << 16) |
|
|
33
|
+
((hmac[offset + 2] & 0xff) << 8) |
|
|
34
|
+
(hmac[offset + 3] & 0xff);
|
|
35
|
+
return String(code % 1_000_000).padStart(6, '0');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class BrowserGitHubProjectRepository implements Pick<
|
|
39
|
+
StatusDefaultRepository,
|
|
40
|
+
'setStatusFieldDefault'
|
|
41
|
+
> {
|
|
42
|
+
constructor(
|
|
43
|
+
private readonly username: string | undefined,
|
|
44
|
+
private readonly password: string | undefined,
|
|
45
|
+
private readonly totpSecret: string | undefined,
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
setStatusFieldDefault = async (
|
|
49
|
+
project: Project,
|
|
50
|
+
optionId: string,
|
|
51
|
+
): Promise<void> => {
|
|
52
|
+
const location = projectLocationFromUrl(project.url);
|
|
53
|
+
if (!location) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`BrowserGitHubProjectRepository: cannot parse project URL: ${project.url}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const settingsUrl = `https://github.com/${location.ownerType}/${location.owner}/projects/${location.projectNumber}/settings/fields/Status`;
|
|
60
|
+
|
|
61
|
+
if (!this.username || !this.password) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`BrowserGitHubProjectRepository: GITHUB_USERNAME or GITHUB_PASSWORD is unset; skipping setStatusFieldDefault. Settings URL: ${settingsUrl}`,
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const optionName = project.status.statuses.find(
|
|
69
|
+
(s) => s.id === optionId,
|
|
70
|
+
)?.name;
|
|
71
|
+
if (!optionName) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`BrowserGitHubProjectRepository: option with id "${optionId}" not found in project status list`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const browser = await chromium.launch({ headless: true });
|
|
78
|
+
try {
|
|
79
|
+
const page = await browser.newPage();
|
|
80
|
+
|
|
81
|
+
await page.goto('https://github.com/login');
|
|
82
|
+
await page.fill('#login_field', this.username);
|
|
83
|
+
await page.fill('#password', this.password);
|
|
84
|
+
await page.click('[name="commit"]');
|
|
85
|
+
|
|
86
|
+
const otpInput = page.locator('#app_totp');
|
|
87
|
+
const isOtpVisible = await otpInput.isVisible().catch(() => false);
|
|
88
|
+
if (isOtpVisible && this.totpSecret) {
|
|
89
|
+
const totp = generateTotp(this.totpSecret);
|
|
90
|
+
await otpInput.fill(totp);
|
|
91
|
+
await page.click('[type="submit"]');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await page.goto(settingsUrl);
|
|
95
|
+
|
|
96
|
+
const defaultSelect = page
|
|
97
|
+
.locator('select[aria-label="Default value"]')
|
|
98
|
+
.first();
|
|
99
|
+
await defaultSelect.waitFor({ state: 'visible' });
|
|
100
|
+
const options = await defaultSelect.locator('option').allTextContents();
|
|
101
|
+
const matchingOption = options.find((o) => o.trim() === optionName);
|
|
102
|
+
if (!matchingOption) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`BrowserGitHubProjectRepository: option "${optionName}" not found in Default dropdown. Available: ${options.join(', ')}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
await defaultSelect.selectOption({ label: optionName });
|
|
108
|
+
|
|
109
|
+
const saveButton = page
|
|
110
|
+
.locator('button[type="submit"]')
|
|
111
|
+
.filter({ hasText: /save/i })
|
|
112
|
+
.first();
|
|
113
|
+
await saveButton.click();
|
|
114
|
+
} finally {
|
|
115
|
+
await browser.close();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -2,6 +2,7 @@ import { mock } from 'jest-mock-extended';
|
|
|
2
2
|
import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
|
|
3
3
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
4
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
5
|
+
import { StatusDefaultRepository } from './adapter-interfaces/StatusDefaultRepository';
|
|
5
6
|
import { FieldOption, Project } from '../entities/Project';
|
|
6
7
|
import { Issue } from '../entities/Issue';
|
|
7
8
|
import {
|
|
@@ -111,9 +112,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
111
112
|
cacheUsed: false,
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
const mockStatusDefaultRepository =
|
|
116
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
114
117
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
115
118
|
mockProjectRepository,
|
|
116
119
|
mockIssueRepository,
|
|
120
|
+
mockStatusDefaultRepository,
|
|
117
121
|
);
|
|
118
122
|
await useCase.run({ projectUrl: project.url });
|
|
119
123
|
|
|
@@ -142,9 +146,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
142
146
|
cacheUsed: false,
|
|
143
147
|
});
|
|
144
148
|
|
|
149
|
+
const mockStatusDefaultRepository =
|
|
150
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
145
151
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
146
152
|
mockProjectRepository,
|
|
147
153
|
mockIssueRepository,
|
|
154
|
+
mockStatusDefaultRepository,
|
|
148
155
|
);
|
|
149
156
|
await useCase.run({ projectUrl: project.url });
|
|
150
157
|
|
|
@@ -167,9 +174,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
167
174
|
cacheUsed: false,
|
|
168
175
|
});
|
|
169
176
|
|
|
177
|
+
const mockStatusDefaultRepository =
|
|
178
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
170
179
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
171
180
|
mockProjectRepository,
|
|
172
181
|
mockIssueRepository,
|
|
182
|
+
mockStatusDefaultRepository,
|
|
173
183
|
);
|
|
174
184
|
await useCase.run({ projectUrl: project.url });
|
|
175
185
|
|
|
@@ -210,9 +220,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
210
220
|
cacheUsed: false,
|
|
211
221
|
});
|
|
212
222
|
|
|
223
|
+
const mockStatusDefaultRepository =
|
|
224
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
213
225
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
214
226
|
mockProjectRepository,
|
|
215
227
|
mockIssueRepository,
|
|
228
|
+
mockStatusDefaultRepository,
|
|
216
229
|
);
|
|
217
230
|
await useCase.run({ projectUrl: project.url });
|
|
218
231
|
|
|
@@ -312,9 +325,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
312
325
|
cacheUsed: false,
|
|
313
326
|
});
|
|
314
327
|
|
|
328
|
+
const mockStatusDefaultRepository =
|
|
329
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
315
330
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
316
331
|
mockProjectRepository,
|
|
317
332
|
mockIssueRepository,
|
|
333
|
+
mockStatusDefaultRepository,
|
|
318
334
|
);
|
|
319
335
|
await useCase.run({ projectUrl: project.url });
|
|
320
336
|
|
|
@@ -350,9 +366,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
350
366
|
cacheUsed: false,
|
|
351
367
|
});
|
|
352
368
|
|
|
369
|
+
const mockStatusDefaultRepository =
|
|
370
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
353
371
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
354
372
|
mockProjectRepository,
|
|
355
373
|
mockIssueRepository,
|
|
374
|
+
mockStatusDefaultRepository,
|
|
356
375
|
);
|
|
357
376
|
await useCase.run({ projectUrl: project.url });
|
|
358
377
|
|
|
@@ -395,9 +414,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
395
414
|
cacheUsed: false,
|
|
396
415
|
});
|
|
397
416
|
|
|
417
|
+
const mockStatusDefaultRepository =
|
|
418
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
398
419
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
399
420
|
mockProjectRepository,
|
|
400
421
|
mockIssueRepository,
|
|
422
|
+
mockStatusDefaultRepository,
|
|
401
423
|
);
|
|
402
424
|
await useCase.run({ projectUrl: project.url });
|
|
403
425
|
|
|
@@ -437,9 +459,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
437
459
|
cacheUsed: false,
|
|
438
460
|
});
|
|
439
461
|
|
|
462
|
+
const mockStatusDefaultRepository =
|
|
463
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
440
464
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
441
465
|
mockProjectRepository,
|
|
442
466
|
mockIssueRepository,
|
|
467
|
+
mockStatusDefaultRepository,
|
|
443
468
|
);
|
|
444
469
|
await useCase.run({ projectUrl: project.url });
|
|
445
470
|
|
|
@@ -470,9 +495,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
470
495
|
cacheUsed: false,
|
|
471
496
|
});
|
|
472
497
|
|
|
498
|
+
const mockStatusDefaultRepository =
|
|
499
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
473
500
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
474
501
|
mockProjectRepository,
|
|
475
502
|
mockIssueRepository,
|
|
503
|
+
mockStatusDefaultRepository,
|
|
476
504
|
);
|
|
477
505
|
await useCase.run({ projectUrl: project.url });
|
|
478
506
|
|
|
@@ -513,9 +541,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
513
541
|
cacheUsed: false,
|
|
514
542
|
});
|
|
515
543
|
|
|
544
|
+
const mockStatusDefaultRepository =
|
|
545
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
516
546
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
517
547
|
mockProjectRepository,
|
|
518
548
|
mockIssueRepository,
|
|
549
|
+
mockStatusDefaultRepository,
|
|
519
550
|
);
|
|
520
551
|
await useCase.run({ projectUrl: project.url });
|
|
521
552
|
|
|
@@ -548,9 +579,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
548
579
|
cacheUsed: false,
|
|
549
580
|
});
|
|
550
581
|
|
|
582
|
+
const mockStatusDefaultRepository =
|
|
583
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
551
584
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
552
585
|
mockProjectRepository,
|
|
553
586
|
mockIssueRepository,
|
|
587
|
+
mockStatusDefaultRepository,
|
|
554
588
|
);
|
|
555
589
|
await useCase.run({ projectUrl: project.url });
|
|
556
590
|
|
|
@@ -591,9 +625,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
591
625
|
cacheUsed: false,
|
|
592
626
|
});
|
|
593
627
|
|
|
628
|
+
const mockStatusDefaultRepository =
|
|
629
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
594
630
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
595
631
|
mockProjectRepository,
|
|
596
632
|
mockIssueRepository,
|
|
633
|
+
mockStatusDefaultRepository,
|
|
597
634
|
);
|
|
598
635
|
await useCase.run({ projectUrl: project.url });
|
|
599
636
|
|
|
@@ -632,9 +669,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
632
669
|
cacheUsed: false,
|
|
633
670
|
});
|
|
634
671
|
|
|
672
|
+
const mockStatusDefaultRepository =
|
|
673
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
635
674
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
636
675
|
mockProjectRepository,
|
|
637
676
|
mockIssueRepository,
|
|
677
|
+
mockStatusDefaultRepository,
|
|
638
678
|
);
|
|
639
679
|
await useCase.run({ projectUrl: project.url });
|
|
640
680
|
|
|
@@ -698,9 +738,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
698
738
|
cacheUsed: false,
|
|
699
739
|
});
|
|
700
740
|
|
|
741
|
+
const mockStatusDefaultRepository =
|
|
742
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
701
743
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
702
744
|
mockProjectRepository,
|
|
703
745
|
mockIssueRepository,
|
|
746
|
+
mockStatusDefaultRepository,
|
|
704
747
|
);
|
|
705
748
|
await useCase.run({ projectUrl: project.url });
|
|
706
749
|
|
|
@@ -812,9 +855,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
812
855
|
});
|
|
813
856
|
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
814
857
|
|
|
858
|
+
const mockStatusDefaultRepository =
|
|
859
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
815
860
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
816
861
|
mockProjectRepository,
|
|
817
862
|
mockIssueRepository,
|
|
863
|
+
mockStatusDefaultRepository,
|
|
818
864
|
);
|
|
819
865
|
await useCase.run({ projectUrl: project.url });
|
|
820
866
|
|
|
@@ -865,9 +911,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
865
911
|
cacheUsed: false,
|
|
866
912
|
});
|
|
867
913
|
|
|
914
|
+
const mockStatusDefaultRepository =
|
|
915
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
868
916
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
869
917
|
mockProjectRepository,
|
|
870
918
|
mockIssueRepository,
|
|
919
|
+
mockStatusDefaultRepository,
|
|
871
920
|
);
|
|
872
921
|
await useCase.run({ projectUrl: project.url });
|
|
873
922
|
|
|
@@ -899,9 +948,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
899
948
|
});
|
|
900
949
|
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
901
950
|
|
|
951
|
+
const mockStatusDefaultRepository =
|
|
952
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
902
953
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
903
954
|
mockProjectRepository,
|
|
904
955
|
mockIssueRepository,
|
|
956
|
+
mockStatusDefaultRepository,
|
|
905
957
|
);
|
|
906
958
|
await useCase.run({ projectUrl: project.url });
|
|
907
959
|
|
|
@@ -976,9 +1028,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
976
1028
|
});
|
|
977
1029
|
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
978
1030
|
|
|
1031
|
+
const mockStatusDefaultRepository =
|
|
1032
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
979
1033
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
980
1034
|
mockProjectRepository,
|
|
981
1035
|
mockIssueRepository,
|
|
1036
|
+
mockStatusDefaultRepository,
|
|
982
1037
|
);
|
|
983
1038
|
await useCase.run({ projectUrl: project.url });
|
|
984
1039
|
|
|
@@ -1037,9 +1092,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1037
1092
|
});
|
|
1038
1093
|
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
1039
1094
|
|
|
1095
|
+
const mockStatusDefaultRepository =
|
|
1096
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1040
1097
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1041
1098
|
mockProjectRepository,
|
|
1042
1099
|
mockIssueRepository,
|
|
1100
|
+
mockStatusDefaultRepository,
|
|
1043
1101
|
);
|
|
1044
1102
|
await useCase.run({ projectUrl: project.url });
|
|
1045
1103
|
|
|
@@ -1075,9 +1133,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1075
1133
|
});
|
|
1076
1134
|
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
1077
1135
|
|
|
1136
|
+
const mockStatusDefaultRepository =
|
|
1137
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1078
1138
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1079
1139
|
mockProjectRepository,
|
|
1080
1140
|
mockIssueRepository,
|
|
1141
|
+
mockStatusDefaultRepository,
|
|
1081
1142
|
);
|
|
1082
1143
|
await useCase.run({ projectUrl: project.url });
|
|
1083
1144
|
|
|
@@ -1111,9 +1172,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1111
1172
|
cacheUsed: false,
|
|
1112
1173
|
});
|
|
1113
1174
|
|
|
1175
|
+
const mockStatusDefaultRepository =
|
|
1176
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1114
1177
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1115
1178
|
mockProjectRepository,
|
|
1116
1179
|
mockIssueRepository,
|
|
1180
|
+
mockStatusDefaultRepository,
|
|
1117
1181
|
);
|
|
1118
1182
|
await useCase.run({ projectUrl: project.url });
|
|
1119
1183
|
|
|
@@ -1142,9 +1206,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1142
1206
|
cacheUsed: false,
|
|
1143
1207
|
});
|
|
1144
1208
|
|
|
1209
|
+
const mockStatusDefaultRepository =
|
|
1210
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1145
1211
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1146
1212
|
mockProjectRepository,
|
|
1147
1213
|
mockIssueRepository,
|
|
1214
|
+
mockStatusDefaultRepository,
|
|
1148
1215
|
);
|
|
1149
1216
|
await useCase.run({ projectUrl: project.url });
|
|
1150
1217
|
|
|
@@ -1199,9 +1266,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1199
1266
|
});
|
|
1200
1267
|
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
1201
1268
|
|
|
1269
|
+
const mockStatusDefaultRepository =
|
|
1270
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1202
1271
|
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1203
1272
|
mockProjectRepository,
|
|
1204
1273
|
mockIssueRepository,
|
|
1274
|
+
mockStatusDefaultRepository,
|
|
1205
1275
|
);
|
|
1206
1276
|
await useCase.run({ projectUrl: project.url });
|
|
1207
1277
|
|
|
@@ -1217,4 +1287,77 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1217
1287
|
awaitingWorkspaceId,
|
|
1218
1288
|
);
|
|
1219
1289
|
});
|
|
1290
|
+
|
|
1291
|
+
it('should call setStatusFieldDefault with Awaiting Workspace option id when statuses are already in canonical order', async () => {
|
|
1292
|
+
const mockProjectRepository =
|
|
1293
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
1294
|
+
const mockIssueRepository =
|
|
1295
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
1296
|
+
const canonicalStatuses = buildCanonicalStatuses();
|
|
1297
|
+
const project = buildProject(canonicalStatuses);
|
|
1298
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
1299
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1300
|
+
project: mock<Project>(),
|
|
1301
|
+
issues: [],
|
|
1302
|
+
cacheUsed: false,
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
const mockStatusDefaultRepository =
|
|
1306
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1307
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1308
|
+
mockProjectRepository,
|
|
1309
|
+
mockIssueRepository,
|
|
1310
|
+
mockStatusDefaultRepository,
|
|
1311
|
+
);
|
|
1312
|
+
await useCase.run({ projectUrl: project.url });
|
|
1313
|
+
|
|
1314
|
+
expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
|
|
1315
|
+
expect(
|
|
1316
|
+
mockStatusDefaultRepository.setStatusFieldDefault,
|
|
1317
|
+
).toHaveBeenCalledTimes(1);
|
|
1318
|
+
expect(
|
|
1319
|
+
mockStatusDefaultRepository.setStatusFieldDefault,
|
|
1320
|
+
).toHaveBeenCalledWith(project, canonicalStatuses[0].id);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('should call setStatusFieldDefault with Awaiting Workspace option id after updateStatusList', async () => {
|
|
1324
|
+
const mockProjectRepository =
|
|
1325
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
1326
|
+
const mockIssueRepository =
|
|
1327
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
1328
|
+
const awaitingWorkspaceId = 'existing-aws-id';
|
|
1329
|
+
const statuses: FieldOption[] = [
|
|
1330
|
+
{
|
|
1331
|
+
id: awaitingWorkspaceId,
|
|
1332
|
+
name: AWAITING_WORKSPACE_STATUS_NAME,
|
|
1333
|
+
color: 'BLUE',
|
|
1334
|
+
description: 'stale description',
|
|
1335
|
+
},
|
|
1336
|
+
];
|
|
1337
|
+
const project = buildProject(statuses);
|
|
1338
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
1339
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
1340
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1341
|
+
project: mock<Project>(),
|
|
1342
|
+
issues: [],
|
|
1343
|
+
cacheUsed: false,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
const mockStatusDefaultRepository =
|
|
1347
|
+
mock<Pick<StatusDefaultRepository, 'setStatusFieldDefault'>>();
|
|
1348
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1349
|
+
mockProjectRepository,
|
|
1350
|
+
mockIssueRepository,
|
|
1351
|
+
mockStatusDefaultRepository,
|
|
1352
|
+
);
|
|
1353
|
+
await useCase.run({ projectUrl: project.url });
|
|
1354
|
+
|
|
1355
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
1356
|
+
expect(
|
|
1357
|
+
mockStatusDefaultRepository.setStatusFieldDefault,
|
|
1358
|
+
).toHaveBeenCalledTimes(1);
|
|
1359
|
+
expect(
|
|
1360
|
+
mockStatusDefaultRepository.setStatusFieldDefault,
|
|
1361
|
+
).toHaveBeenCalledWith(project, awaitingWorkspaceId);
|
|
1362
|
+
});
|
|
1220
1363
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FieldOption } from '../entities/Project';
|
|
2
2
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
3
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
|
+
import { StatusDefaultRepository } from './adapter-interfaces/StatusDefaultRepository';
|
|
4
5
|
import {
|
|
5
6
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
6
7
|
DONE_STATUS_NAME,
|
|
@@ -24,6 +25,10 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
24
25
|
IssueRepository,
|
|
25
26
|
'getAllIssues' | 'updateStatus'
|
|
26
27
|
>,
|
|
28
|
+
private readonly statusDefaultRepository: Pick<
|
|
29
|
+
StatusDefaultRepository,
|
|
30
|
+
'setStatusFieldDefault'
|
|
31
|
+
>,
|
|
27
32
|
) {}
|
|
28
33
|
|
|
29
34
|
private static readonly LEGACY_STATUS_NAMES: Readonly<
|
|
@@ -117,6 +122,9 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
const awaitingWorkspaceOptionId: string | undefined =
|
|
126
|
+
awaitingWorkspaceStatus?.id;
|
|
127
|
+
|
|
120
128
|
const hasMigratedFromName = existing.some((s) =>
|
|
121
129
|
SetupTowerDefenceProjectUseCase.MIGRATED_FROM_NAMES.has(s.name),
|
|
122
130
|
);
|
|
@@ -126,6 +134,12 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
126
134
|
existing,
|
|
127
135
|
)
|
|
128
136
|
) {
|
|
137
|
+
if (awaitingWorkspaceOptionId !== undefined) {
|
|
138
|
+
await this.statusDefaultRepository.setStatusFieldDefault(
|
|
139
|
+
project,
|
|
140
|
+
awaitingWorkspaceOptionId,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
129
143
|
return;
|
|
130
144
|
}
|
|
131
145
|
|
|
@@ -172,6 +186,13 @@ export class SetupTowerDefenceProjectUseCase {
|
|
|
172
186
|
];
|
|
173
187
|
|
|
174
188
|
await this.projectRepository.updateStatusList(project, newStatusList);
|
|
189
|
+
|
|
190
|
+
if (awaitingWorkspaceOptionId !== undefined) {
|
|
191
|
+
await this.statusDefaultRepository.setStatusFieldDefault(
|
|
192
|
+
project,
|
|
193
|
+
awaitingWorkspaceOptionId,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
175
196
|
};
|
|
176
197
|
|
|
177
198
|
private static hasRequiredStatusesInCanonicalOrder = (
|