github-issue-tower-defence-management 1.51.0 → 1.52.1
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/.github/workflows/umino-project.yml +2 -2
- package/CHANGELOG.md +20 -0
- package/README.md +41 -2
- package/bin/adapter/entry-points/cli/index.js +2 -2
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +31 -9
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js +1 -1
- package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +48 -26
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -2
- package/src/adapter/entry-points/cli/index.ts +2 -2
- package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +251 -0
- package/src/adapter/repositories/GraphqlProjectRepository.ts +54 -25
- package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +144 -10
- package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +1 -1
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +160 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +91 -29
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts +2 -0
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const mockPost = jest.fn();
|
|
2
|
+
|
|
3
|
+
jest.mock('ky', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
post: mockPost,
|
|
6
|
+
get: jest.fn(),
|
|
7
|
+
put: jest.fn(),
|
|
8
|
+
patch: jest.fn(),
|
|
9
|
+
delete: jest.fn(),
|
|
10
|
+
extend: jest.fn(),
|
|
11
|
+
create: jest.fn(),
|
|
12
|
+
stop: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
__esModule: true,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { GraphqlProjectRepository } from './GraphqlProjectRepository';
|
|
18
|
+
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
19
|
+
|
|
20
|
+
const mockJsonResponse = <T>(data: T) => ({
|
|
21
|
+
json: jest.fn().mockResolvedValue(data),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('GraphqlProjectRepository.fetchProjectId', () => {
|
|
25
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
26
|
+
let repository: GraphqlProjectRepository;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.useFakeTimers();
|
|
30
|
+
mockPost.mockReset();
|
|
31
|
+
repository = new GraphqlProjectRepository(localStorageRepository, '');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.useRealTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('user-owned project', () => {
|
|
39
|
+
it('should resolve project ID from user owner when organization returns null', async () => {
|
|
40
|
+
mockPost.mockReturnValueOnce(
|
|
41
|
+
mockJsonResponse({
|
|
42
|
+
data: {
|
|
43
|
+
organization: null,
|
|
44
|
+
user: {
|
|
45
|
+
projectV2: {
|
|
46
|
+
id: 'PVT_user123',
|
|
47
|
+
databaseId: 999,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = await repository.fetchProjectId('some-user', 1);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe('PVT_user123');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should resolve project ID from organization owner when user returns null', async () => {
|
|
60
|
+
mockPost.mockReturnValueOnce(
|
|
61
|
+
mockJsonResponse({
|
|
62
|
+
data: {
|
|
63
|
+
organization: {
|
|
64
|
+
projectV2: {
|
|
65
|
+
id: 'PVT_org456',
|
|
66
|
+
databaseId: 111,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
user: null,
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const result = await repository.fetchProjectId('some-org', 2);
|
|
75
|
+
|
|
76
|
+
expect(result).toBe('PVT_org456');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('memoization', () => {
|
|
81
|
+
it('should return cached project ID on subsequent calls without re-fetching', async () => {
|
|
82
|
+
mockPost.mockReturnValueOnce(
|
|
83
|
+
mockJsonResponse({
|
|
84
|
+
data: {
|
|
85
|
+
organization: null,
|
|
86
|
+
user: {
|
|
87
|
+
projectV2: {
|
|
88
|
+
id: 'PVT_cached',
|
|
89
|
+
databaseId: 777,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const first = await repository.fetchProjectId('owner', 10);
|
|
97
|
+
const second = await repository.fetchProjectId('owner', 10);
|
|
98
|
+
|
|
99
|
+
expect(first).toBe('PVT_cached');
|
|
100
|
+
expect(second).toBe('PVT_cached');
|
|
101
|
+
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should use separate cache entries for different owner+projectNumber combinations', async () => {
|
|
105
|
+
mockPost
|
|
106
|
+
.mockReturnValueOnce(
|
|
107
|
+
mockJsonResponse({
|
|
108
|
+
data: {
|
|
109
|
+
organization: null,
|
|
110
|
+
user: { projectV2: { id: 'PVT_A', databaseId: 1 } },
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
.mockReturnValueOnce(
|
|
115
|
+
mockJsonResponse({
|
|
116
|
+
data: {
|
|
117
|
+
organization: null,
|
|
118
|
+
user: { projectV2: { id: 'PVT_B', databaseId: 2 } },
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const resultA = await repository.fetchProjectId('ownerA', 1);
|
|
124
|
+
const resultB = await repository.fetchProjectId('ownerB', 1);
|
|
125
|
+
|
|
126
|
+
expect(resultA).toBe('PVT_A');
|
|
127
|
+
expect(resultB).toBe('PVT_B');
|
|
128
|
+
expect(mockPost).toHaveBeenCalledTimes(2);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('errors-only response hardening', () => {
|
|
133
|
+
it('should throw a clear error when response has no data field', async () => {
|
|
134
|
+
mockPost.mockReturnValueOnce(
|
|
135
|
+
mockJsonResponse({
|
|
136
|
+
errors: [{ message: 'Could not resolve to a User' }],
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await expect(repository.fetchProjectId('bad-owner', 1)).rejects.toThrow(
|
|
141
|
+
'Could not resolve to a User',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should throw a clear error when data is null', async () => {
|
|
146
|
+
mockPost.mockReturnValueOnce(
|
|
147
|
+
mockJsonResponse({
|
|
148
|
+
data: null,
|
|
149
|
+
errors: [{ message: 'secondary rate limit' }],
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await expect(
|
|
154
|
+
repository.fetchProjectId('rate-limited', 1),
|
|
155
|
+
).rejects.toThrow('secondary rate limit');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should throw a clear error when data has no project in either org or user', async () => {
|
|
159
|
+
mockPost.mockReturnValueOnce(
|
|
160
|
+
mockJsonResponse({
|
|
161
|
+
data: {
|
|
162
|
+
organization: null,
|
|
163
|
+
user: null,
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
repository.fetchProjectId('no-project-owner', 99),
|
|
170
|
+
).rejects.toThrow('project not found');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw a clear error when network call fails', async () => {
|
|
174
|
+
mockPost.mockReturnValueOnce({
|
|
175
|
+
json: jest.fn().mockRejectedValue(new Error('network failure')),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await expect(repository.fetchProjectId('owner', 1)).rejects.toThrow(
|
|
179
|
+
'network failure',
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('backoff after failure', () => {
|
|
185
|
+
it('should not re-call GraphQL within 1 hour after a failure', async () => {
|
|
186
|
+
mockPost.mockReturnValueOnce(
|
|
187
|
+
mockJsonResponse({
|
|
188
|
+
errors: [{ message: 'auth failure' }],
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
await expect(repository.fetchProjectId('owner', 5)).rejects.toThrow();
|
|
193
|
+
|
|
194
|
+
await expect(repository.fetchProjectId('owner', 5)).rejects.toThrow(
|
|
195
|
+
'backoff',
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should retry after 1 hour backoff has elapsed', async () => {
|
|
202
|
+
mockPost
|
|
203
|
+
.mockReturnValueOnce(
|
|
204
|
+
mockJsonResponse({
|
|
205
|
+
errors: [{ message: 'temporary error' }],
|
|
206
|
+
}),
|
|
207
|
+
)
|
|
208
|
+
.mockReturnValueOnce(
|
|
209
|
+
mockJsonResponse({
|
|
210
|
+
data: {
|
|
211
|
+
organization: null,
|
|
212
|
+
user: { projectV2: { id: 'PVT_recovered', databaseId: 42 } },
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await expect(repository.fetchProjectId('owner', 7)).rejects.toThrow();
|
|
218
|
+
|
|
219
|
+
jest.advanceTimersByTime(60 * 60 * 1000 + 1);
|
|
220
|
+
|
|
221
|
+
const result = await repository.fetchProjectId('owner', 7);
|
|
222
|
+
|
|
223
|
+
expect(result).toBe('PVT_recovered');
|
|
224
|
+
expect(mockPost).toHaveBeenCalledTimes(2);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should apply backoff independently per owner+projectNumber', async () => {
|
|
228
|
+
mockPost
|
|
229
|
+
.mockReturnValueOnce(
|
|
230
|
+
mockJsonResponse({
|
|
231
|
+
errors: [{ message: 'error for owner A' }],
|
|
232
|
+
}),
|
|
233
|
+
)
|
|
234
|
+
.mockReturnValueOnce(
|
|
235
|
+
mockJsonResponse({
|
|
236
|
+
data: {
|
|
237
|
+
organization: null,
|
|
238
|
+
user: { projectV2: { id: 'PVT_B', databaseId: 2 } },
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
await expect(repository.fetchProjectId('ownerA', 1)).rejects.toThrow();
|
|
244
|
+
|
|
245
|
+
const resultB = await repository.fetchProjectId('ownerB', 1);
|
|
246
|
+
|
|
247
|
+
expect(resultB).toBe('PVT_B');
|
|
248
|
+
expect(mockPost).toHaveBeenCalledTimes(2);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -4,6 +4,8 @@ import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/Proj
|
|
|
4
4
|
import { FieldOption, Project } from '../../domain/entities/Project';
|
|
5
5
|
import { normalizeFieldName } from './utils';
|
|
6
6
|
|
|
7
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
8
|
+
|
|
7
9
|
export class GraphqlProjectRepository
|
|
8
10
|
extends BaseGitHubRepository
|
|
9
11
|
implements
|
|
@@ -16,6 +18,9 @@ export class GraphqlProjectRepository
|
|
|
16
18
|
| 'updateStatusList'
|
|
17
19
|
>
|
|
18
20
|
{
|
|
21
|
+
private readonly projectIdCache = new Map<string, string>();
|
|
22
|
+
private readonly fetchProjectIdFailedAt = new Map<string, number>();
|
|
23
|
+
|
|
19
24
|
extractProjectFromUrl = (
|
|
20
25
|
projectUrl: string,
|
|
21
26
|
): {
|
|
@@ -32,6 +37,17 @@ export class GraphqlProjectRepository
|
|
|
32
37
|
login: string,
|
|
33
38
|
projectNumber: number,
|
|
34
39
|
): Promise<string> => {
|
|
40
|
+
const cacheKey = `${login}:${projectNumber}`;
|
|
41
|
+
const cached = this.projectIdCache.get(cacheKey);
|
|
42
|
+
if (cached) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
const failedAt = this.fetchProjectIdFailedAt.get(cacheKey);
|
|
46
|
+
if (failedAt !== undefined && Date.now() - failedAt < ONE_HOUR_MS) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`fetchProjectId for ${login}/${projectNumber} is in backoff after a recent failure`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
35
51
|
const graphqlQuery = {
|
|
36
52
|
query: `query GetProjectID($login: String!, $number: Int!) {
|
|
37
53
|
organization(login: $login) {
|
|
@@ -53,32 +69,41 @@ export class GraphqlProjectRepository
|
|
|
53
69
|
},
|
|
54
70
|
};
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
let response: {
|
|
73
|
+
data?: {
|
|
74
|
+
organization?: {
|
|
75
|
+
projectV2?: {
|
|
76
|
+
id: string;
|
|
77
|
+
databaseId: number;
|
|
78
|
+
} | null;
|
|
79
|
+
} | null;
|
|
80
|
+
user?: {
|
|
81
|
+
projectV2?: {
|
|
82
|
+
id: string;
|
|
83
|
+
databaseId: number;
|
|
84
|
+
} | null;
|
|
85
|
+
} | null;
|
|
86
|
+
} | null;
|
|
87
|
+
errors?: { message: string }[];
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
response = await ky
|
|
91
|
+
.post('https://api.github.com/graphql', {
|
|
92
|
+
json: graphqlQuery,
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.json();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
|
|
100
|
+
throw new Error(
|
|
101
|
+
`fetchProjectId network error for ${login}/${projectNumber}: ${String(error)}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
80
104
|
|
|
81
105
|
if (!response.data) {
|
|
106
|
+
this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
|
|
82
107
|
const errorMessages = response.errors
|
|
83
108
|
? response.errors.map((e) => e.message).join('; ')
|
|
84
109
|
: 'no data field in response';
|
|
@@ -90,8 +115,12 @@ export class GraphqlProjectRepository
|
|
|
90
115
|
response.data.organization?.projectV2?.id ||
|
|
91
116
|
response.data.user?.projectV2?.id;
|
|
92
117
|
if (!projectId) {
|
|
93
|
-
|
|
118
|
+
this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
|
|
119
|
+
throw new Error(
|
|
120
|
+
`fetchProjectId: project not found for ${login}/${projectNumber}`,
|
|
121
|
+
);
|
|
94
122
|
}
|
|
123
|
+
this.projectIdCache.set(cacheKey, projectId);
|
|
95
124
|
return projectId;
|
|
96
125
|
};
|
|
97
126
|
findProjectIdByUrl = async (
|
|
@@ -129,18 +129,146 @@ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
|
|
|
129
129
|
expectedGetIssueByUrlCalls: [],
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
|
-
name: 'should
|
|
132
|
+
name: 'should process story issues even when cache is used',
|
|
133
133
|
input: {
|
|
134
134
|
project: basicProject,
|
|
135
|
-
issues: [basicStoryIssue1],
|
|
135
|
+
issues: [basicStoryIssue1, basicStoryIssue2],
|
|
136
136
|
cacheUsed: true,
|
|
137
137
|
urlOfStoryView: 'https://example.com',
|
|
138
138
|
storyObjectMap: basicStoryObjectMap,
|
|
139
139
|
},
|
|
140
|
-
expectedCreateNewIssueCalls: [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
expectedCreateNewIssueCalls: [
|
|
141
|
+
[
|
|
142
|
+
'org',
|
|
143
|
+
'repo',
|
|
144
|
+
'Task 1',
|
|
145
|
+
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
146
|
+
[],
|
|
147
|
+
[],
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
'org',
|
|
151
|
+
'repo',
|
|
152
|
+
'Task 2',
|
|
153
|
+
'- Parent issue: https://github.com/org/repo/issues/123',
|
|
154
|
+
[],
|
|
155
|
+
[],
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
'org',
|
|
159
|
+
'repo',
|
|
160
|
+
'Task 3',
|
|
161
|
+
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
162
|
+
[],
|
|
163
|
+
[],
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
'org',
|
|
167
|
+
'repo',
|
|
168
|
+
'Task 4',
|
|
169
|
+
'- Parent issue: https://github.com/org/repo/issues/456',
|
|
170
|
+
[],
|
|
171
|
+
[],
|
|
172
|
+
],
|
|
173
|
+
],
|
|
174
|
+
expectedUpdateIssueCalls: [
|
|
175
|
+
[
|
|
176
|
+
{
|
|
177
|
+
...basicStoryIssue1,
|
|
178
|
+
body: `https://example.com?sliceBy%5Bvalue%5D=Story%201
|
|
179
|
+
|
|
180
|
+
- [ ] Task 1
|
|
181
|
+
- [ ] Task 2`,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
{
|
|
186
|
+
...basicStoryIssue1,
|
|
187
|
+
body: `https://example.com?sliceBy%5Bvalue%5D=Story%201
|
|
188
|
+
|
|
189
|
+
- [ ] https://github.com/org/repo/issues/1
|
|
190
|
+
- [ ] Task 2`,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
...basicStoryIssue1,
|
|
196
|
+
body: `https://example.com?sliceBy%5Bvalue%5D=Story%201
|
|
197
|
+
|
|
198
|
+
- [ ] https://github.com/org/repo/issues/1
|
|
199
|
+
- [ ] https://github.com/org/repo/issues/2`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
{
|
|
204
|
+
...basicStoryIssue2,
|
|
205
|
+
body: `https://example.com?sliceBy%5Bvalue%5D=Story%202
|
|
206
|
+
|
|
207
|
+
- [ ] Task 3
|
|
208
|
+
- [ ] Task 4`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
{
|
|
213
|
+
...basicStoryIssue2,
|
|
214
|
+
body: `https://example.com?sliceBy%5Bvalue%5D=Story%202
|
|
215
|
+
|
|
216
|
+
- [ ] https://github.com/org/repo/issues/3
|
|
217
|
+
- [ ] Task 4`,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
...basicStoryIssue2,
|
|
223
|
+
body: `https://example.com?sliceBy%5Bvalue%5D=Story%202
|
|
224
|
+
|
|
225
|
+
- [ ] https://github.com/org/repo/issues/3
|
|
226
|
+
- [ ] https://github.com/org/repo/issues/4`,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
],
|
|
230
|
+
expectedUpdateStoryCalls: [
|
|
231
|
+
[
|
|
232
|
+
basicProject,
|
|
233
|
+
{
|
|
234
|
+
...mock<Issue>(),
|
|
235
|
+
url: 'https://github.com/org/repo/issues/1',
|
|
236
|
+
},
|
|
237
|
+
'story1',
|
|
238
|
+
],
|
|
239
|
+
[
|
|
240
|
+
basicProject,
|
|
241
|
+
{
|
|
242
|
+
...mock<Issue>(),
|
|
243
|
+
url: 'https://github.com/org/repo/issues/2',
|
|
244
|
+
},
|
|
245
|
+
'story1',
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
basicProject,
|
|
249
|
+
{
|
|
250
|
+
...mock<Issue>(),
|
|
251
|
+
url: 'https://github.com/org/repo/issues/3',
|
|
252
|
+
},
|
|
253
|
+
'story2',
|
|
254
|
+
],
|
|
255
|
+
[
|
|
256
|
+
basicProject,
|
|
257
|
+
{
|
|
258
|
+
...mock<Issue>(),
|
|
259
|
+
url: 'https://github.com/org/repo/issues/4',
|
|
260
|
+
},
|
|
261
|
+
'story2',
|
|
262
|
+
],
|
|
263
|
+
],
|
|
264
|
+
expectedGetIssueByUrlCalls: [
|
|
265
|
+
['https://github.com/org/repo/issues/123'],
|
|
266
|
+
['https://github.com/org/repo/issues/1'],
|
|
267
|
+
['https://github.com/org/repo/issues/2'],
|
|
268
|
+
['https://github.com/org/repo/issues/456'],
|
|
269
|
+
['https://github.com/org/repo/issues/3'],
|
|
270
|
+
['https://github.com/org/repo/issues/4'],
|
|
271
|
+
],
|
|
144
272
|
},
|
|
145
273
|
{
|
|
146
274
|
name: 'should skip regular stories',
|
|
@@ -767,10 +895,16 @@ Some description without checkboxes`,
|
|
|
767
895
|
const storyIssuesByUrl = new Map(
|
|
768
896
|
input.issues.map((issue) => [issue.url, issue]),
|
|
769
897
|
);
|
|
770
|
-
mockIssueRepository.getIssueByUrl.mockImplementation(
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
898
|
+
mockIssueRepository.getIssueByUrl.mockImplementation(async (url) => {
|
|
899
|
+
const storyIssue = storyIssuesByUrl.get(url);
|
|
900
|
+
if (storyIssue) {
|
|
901
|
+
return storyIssue;
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
...mock<Issue>(),
|
|
905
|
+
url,
|
|
906
|
+
};
|
|
907
|
+
});
|
|
774
908
|
|
|
775
909
|
const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
|
|
776
910
|
mockIssueRepository,
|