@elisra-devops/docgen-data-provider 1.63.13 → 1.67.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/.github/workflows/ci.yml +26 -9
- package/.github/workflows/release.yml +9 -10
- package/bin/helpers/tfs.d.ts +3 -0
- package/bin/helpers/tfs.js +44 -7
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.d.ts +10 -0
- package/bin/modules/GitDataProvider.js +10 -0
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +0 -1
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +63 -24
- package/bin/modules/TicketsDataProvider.js +216 -114
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +279 -0
- package/bin/tests/helpers/helper.test.js.map +1 -0
- package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
- package/bin/tests/helpers/tfs.test.js.map +1 -0
- package/bin/tests/index.test.js +25 -0
- package/bin/tests/index.test.js.map +1 -0
- package/bin/tests/models/tfs-data.test.js +160 -0
- package/bin/tests/models/tfs-data.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
- package/bin/tests/modules/gitDataProvider.test.js +1888 -0
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +13 -1
- package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
- package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/testDataProvider.test.js +717 -0
- package/bin/tests/modules/testDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
- package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
- package/bin/tests/utils/DataProviderUtils.test.js +61 -0
- package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
- package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
- package/bin/tests/utils/testStepParserHelper.test.js +359 -0
- package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
- package/package.json +9 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +298 -141
- package/src/tests/helpers/helper.test.ts +337 -0
- package/src/tests/helpers/tfs.test.ts +1092 -0
- package/src/tests/index.test.ts +28 -0
- package/src/tests/models/tfs-data.test.ts +203 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
- package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
- package/src/tests/modules/gitDataProvider.test.ts +2628 -0
- package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +33 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
- package/src/tests/modules/testDataProvider.test.ts +1046 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
- package/src/tests/utils/DataProviderUtils.test.ts +76 -0
- package/src/tests/utils/testStepParserHelper.test.ts +437 -0
- package/tsconfig.json +1 -0
- package/bin/helpers/test/tfs.test.js.map +0 -1
- package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
- package/bin/modules/test/ResultDataProvider.test.js +0 -444
- package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
- package/bin/modules/test/gitDataProvider.test.js +0 -428
- package/bin/modules/test/gitDataProvider.test.js.map +0 -1
- package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
- package/bin/modules/test/pipelineDataProvider.test.js +0 -237
- package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
- package/bin/modules/test/testDataProvider.test.js +0 -234
- package/bin/modules/test/testDataProvider.test.js.map +0 -1
- package/bin/modules/test/ticketsDataProvider.test.js +0 -348
- package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
- package/src/helpers/test/tfs.test.ts +0 -748
- package/src/modules/test/JfrogDataProvider.test.ts +0 -171
- package/src/modules/test/ResultDataProvider.test.ts +0 -542
- package/src/modules/test/gitDataProvider.test.ts +0 -645
- package/src/modules/test/pipelineDataProvider.test.ts +0 -292
- package/src/modules/test/testDataProvider.test.ts +0 -318
- package/src/modules/test/ticketsDataProvider.test.ts +0 -462
- /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
- /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
- /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
- /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
- /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
|
@@ -0,0 +1,2628 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { TFSServices } from '../../helpers/tfs';
|
|
3
|
+
import GitDataProvider from '../../modules/GitDataProvider';
|
|
4
|
+
import logger from '../../utils/logger';
|
|
5
|
+
|
|
6
|
+
jest.mock('../../helpers/tfs');
|
|
7
|
+
jest.mock('../../utils/logger');
|
|
8
|
+
|
|
9
|
+
describe('GitDataProvider - GetCommitForPipeline', () => {
|
|
10
|
+
let gitDataProvider: GitDataProvider;
|
|
11
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
12
|
+
const mockToken = 'mock-token';
|
|
13
|
+
const mockProjectId = 'project-123';
|
|
14
|
+
const mockBuildId = 456;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return the sourceVersion from build information', async () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
const mockCommitSha = 'abc123def456';
|
|
24
|
+
const mockResponse = {
|
|
25
|
+
id: mockBuildId,
|
|
26
|
+
sourceVersion: mockCommitSha,
|
|
27
|
+
status: 'completed',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
const result = await gitDataProvider.GetCommitForPipeline(mockProjectId, mockBuildId);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
37
|
+
`${mockOrgUrl}${mockProjectId}/_apis/build/builds/${mockBuildId}`,
|
|
38
|
+
mockToken,
|
|
39
|
+
'get'
|
|
40
|
+
);
|
|
41
|
+
expect(result).toBe(mockCommitSha);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should throw an error if the API call fails', async () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const expectedError = new Error('API call failed');
|
|
47
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(expectedError);
|
|
48
|
+
|
|
49
|
+
// Act & Assert
|
|
50
|
+
await expect(gitDataProvider.GetCommitForPipeline(mockProjectId, mockBuildId)).rejects.toThrow(
|
|
51
|
+
'API call failed'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
55
|
+
`${mockOrgUrl}${mockProjectId}/_apis/build/builds/${mockBuildId}`,
|
|
56
|
+
mockToken,
|
|
57
|
+
'get'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return undefined if the response does not contain sourceVersion', async () => {
|
|
62
|
+
// Arrange
|
|
63
|
+
const mockResponse = {
|
|
64
|
+
id: mockBuildId,
|
|
65
|
+
status: 'completed',
|
|
66
|
+
// No sourceVersion property
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
const result = await gitDataProvider.GetCommitForPipeline(mockProjectId, mockBuildId);
|
|
73
|
+
|
|
74
|
+
// Assert
|
|
75
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
76
|
+
`${mockOrgUrl}${mockProjectId}/_apis/build/builds/${mockBuildId}`,
|
|
77
|
+
mockToken,
|
|
78
|
+
'get'
|
|
79
|
+
);
|
|
80
|
+
expect(result).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should correctly construct URL with given project ID and build ID', async () => {
|
|
84
|
+
// Arrange
|
|
85
|
+
const customProjectId = 'custom-project';
|
|
86
|
+
const customBuildId = 789;
|
|
87
|
+
const mockCommitSha = 'xyz789abc';
|
|
88
|
+
const mockResponse = { sourceVersion: mockCommitSha };
|
|
89
|
+
|
|
90
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
await gitDataProvider.GetCommitForPipeline(customProjectId, customBuildId);
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
97
|
+
`${mockOrgUrl}${customProjectId}/_apis/build/builds/${customBuildId}`,
|
|
98
|
+
mockToken,
|
|
99
|
+
'get'
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle different organization URLs correctly', async () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const altOrgUrl = 'https://dev.azure.com/different-org/';
|
|
106
|
+
const altGitDataProvider = new GitDataProvider(altOrgUrl, mockToken);
|
|
107
|
+
const mockResponse = { sourceVersion: 'commit-sha' };
|
|
108
|
+
|
|
109
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
110
|
+
|
|
111
|
+
// Act
|
|
112
|
+
await altGitDataProvider.GetCommitForPipeline(mockProjectId, mockBuildId);
|
|
113
|
+
|
|
114
|
+
// Assert
|
|
115
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
116
|
+
`${altOrgUrl}${mockProjectId}/_apis/build/builds/${mockBuildId}`,
|
|
117
|
+
mockToken,
|
|
118
|
+
'get'
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('GitDataProvider - GetTeamProjectGitReposList', () => {
|
|
123
|
+
let gitDataProvider: GitDataProvider;
|
|
124
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
125
|
+
const mockToken = 'mock-token';
|
|
126
|
+
const mockTeamProject = 'project-123';
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
jest.clearAllMocks();
|
|
130
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return sorted repositories when API call succeeds', async () => {
|
|
134
|
+
// Arrange
|
|
135
|
+
const mockRepos = {
|
|
136
|
+
value: [
|
|
137
|
+
{ id: 'repo2', name: 'ZRepo' },
|
|
138
|
+
{ id: 'repo1', name: 'ARepo' },
|
|
139
|
+
{ id: 'repo3', name: 'MRepo' },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockRepos);
|
|
143
|
+
|
|
144
|
+
// Act
|
|
145
|
+
const result = await gitDataProvider.GetTeamProjectGitReposList(mockTeamProject);
|
|
146
|
+
|
|
147
|
+
// Assert
|
|
148
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
149
|
+
`${mockOrgUrl}/${mockTeamProject}/_apis/git/repositories`,
|
|
150
|
+
mockToken,
|
|
151
|
+
'get'
|
|
152
|
+
);
|
|
153
|
+
expect(result).toHaveLength(3);
|
|
154
|
+
expect(result[0].name).toBe('ARepo');
|
|
155
|
+
expect(result[1].name).toBe('MRepo');
|
|
156
|
+
expect(result[2].name).toBe('ZRepo');
|
|
157
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
158
|
+
expect.stringContaining(`fetching repos list for team project - ${mockTeamProject}`)
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return empty array when no repositories exist', async () => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const mockEmptyRepos = { value: [] };
|
|
165
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockEmptyRepos);
|
|
166
|
+
|
|
167
|
+
// Act
|
|
168
|
+
const result = await gitDataProvider.GetTeamProjectGitReposList(mockTeamProject);
|
|
169
|
+
|
|
170
|
+
// Assert
|
|
171
|
+
expect(result).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle API errors appropriately', async () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
const mockError = new Error('API Error');
|
|
177
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
|
|
178
|
+
|
|
179
|
+
// Act & Assert
|
|
180
|
+
await expect(gitDataProvider.GetTeamProjectGitReposList(mockTeamProject)).rejects.toThrow('API Error');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('GitDataProvider - GetGitRepoFromRepoId', () => {
|
|
185
|
+
let gitDataProvider: GitDataProvider;
|
|
186
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
187
|
+
const mockToken = 'mock-token';
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
jest.clearAllMocks();
|
|
191
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should fetch repository by id', async () => {
|
|
195
|
+
const mockRepoId = 'repo-123';
|
|
196
|
+
const mockResponse = { id: mockRepoId, name: 'Repo' };
|
|
197
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
198
|
+
|
|
199
|
+
const result = await gitDataProvider.GetGitRepoFromRepoId(mockRepoId);
|
|
200
|
+
|
|
201
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
202
|
+
`${mockOrgUrl}_apis/git/repositories/${mockRepoId}`,
|
|
203
|
+
mockToken,
|
|
204
|
+
'get'
|
|
205
|
+
);
|
|
206
|
+
expect(result).toEqual(mockResponse);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('GitDataProvider - GetTag', () => {
|
|
211
|
+
let gitDataProvider: GitDataProvider;
|
|
212
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
213
|
+
const mockToken = 'mock-token';
|
|
214
|
+
const mockGitRepoUrl = 'https://dev.azure.com/orgname/project/_apis/git/repositories/repo-id';
|
|
215
|
+
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
jest.clearAllMocks();
|
|
218
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should return tag info when tag exists', async () => {
|
|
222
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
223
|
+
value: [
|
|
224
|
+
{
|
|
225
|
+
name: 'refs/tags/v1.0.0',
|
|
226
|
+
objectId: 'abc123',
|
|
227
|
+
peeledObjectId: 'def456',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await gitDataProvider.GetTag(mockGitRepoUrl, 'v1.0.0');
|
|
233
|
+
|
|
234
|
+
expect(result).toEqual(
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
name: 'v1.0.0',
|
|
237
|
+
objectId: 'abc123',
|
|
238
|
+
peeledObjectId: 'def456',
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should return null when no matching tag found', async () => {
|
|
244
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
245
|
+
value: [{ name: 'refs/tags/other-tag', objectId: 'abc123' }],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const result = await gitDataProvider.GetTag(mockGitRepoUrl, 'v1.0.0');
|
|
249
|
+
expect(result).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('GitDataProvider - GetFileFromGitRepo', () => {
|
|
254
|
+
let gitDataProvider: GitDataProvider;
|
|
255
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
256
|
+
const mockToken = 'mock-token';
|
|
257
|
+
const mockProjectName = 'project-123';
|
|
258
|
+
const mockRepoId = 'repo-456';
|
|
259
|
+
const mockFileName = 'README.md';
|
|
260
|
+
const mockVersion = { version: 'main', versionType: 'branch' };
|
|
261
|
+
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
jest.clearAllMocks();
|
|
264
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return file content when file exists', async () => {
|
|
268
|
+
// Arrange
|
|
269
|
+
const mockContent = 'This is a test readme file';
|
|
270
|
+
const mockResponse = { content: mockContent };
|
|
271
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
272
|
+
|
|
273
|
+
// Act
|
|
274
|
+
const result = await gitDataProvider.GetFileFromGitRepo(
|
|
275
|
+
mockProjectName,
|
|
276
|
+
mockRepoId,
|
|
277
|
+
mockFileName,
|
|
278
|
+
mockVersion
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Assert
|
|
282
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
283
|
+
expect.stringContaining(`${mockOrgUrl}${mockProjectName}/_apis/git/repositories/${mockRepoId}/items`),
|
|
284
|
+
mockToken,
|
|
285
|
+
'get',
|
|
286
|
+
{},
|
|
287
|
+
{},
|
|
288
|
+
false
|
|
289
|
+
);
|
|
290
|
+
expect(result).toBe(mockContent);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle special characters in version by encoding them', async () => {
|
|
294
|
+
// Arrange
|
|
295
|
+
const specialVersion = { version: 'feature/branch#123', versionType: 'branch' };
|
|
296
|
+
const mockResponse = { content: 'content' };
|
|
297
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
298
|
+
|
|
299
|
+
// Act
|
|
300
|
+
await gitDataProvider.GetFileFromGitRepo(mockProjectName, mockRepoId, mockFileName, specialVersion);
|
|
301
|
+
|
|
302
|
+
// Assert
|
|
303
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
304
|
+
expect.stringContaining('versionDescriptor.version=feature%2Fbranch%23123'),
|
|
305
|
+
expect.anything(),
|
|
306
|
+
expect.anything(),
|
|
307
|
+
expect.anything(),
|
|
308
|
+
expect.anything(),
|
|
309
|
+
expect.anything()
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should use custom gitRepoUrl if provided', async () => {
|
|
314
|
+
// Arrange
|
|
315
|
+
const mockCustomUrl = 'https://custom.git.url';
|
|
316
|
+
const mockResponse = { content: 'content' };
|
|
317
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
318
|
+
|
|
319
|
+
// Act
|
|
320
|
+
await gitDataProvider.GetFileFromGitRepo(
|
|
321
|
+
mockProjectName,
|
|
322
|
+
mockRepoId,
|
|
323
|
+
mockFileName,
|
|
324
|
+
mockVersion,
|
|
325
|
+
mockCustomUrl
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Assert
|
|
329
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
330
|
+
expect.stringContaining(mockCustomUrl),
|
|
331
|
+
expect.anything(),
|
|
332
|
+
expect.anything(),
|
|
333
|
+
expect.anything(),
|
|
334
|
+
expect.anything(),
|
|
335
|
+
expect.anything()
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should return undefined when file does not exist', async () => {
|
|
340
|
+
// Arrange
|
|
341
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
|
|
342
|
+
|
|
343
|
+
// Act
|
|
344
|
+
const result = await gitDataProvider.GetFileFromGitRepo(
|
|
345
|
+
mockProjectName,
|
|
346
|
+
mockRepoId,
|
|
347
|
+
mockFileName,
|
|
348
|
+
mockVersion
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Assert
|
|
352
|
+
expect(result).toBeUndefined();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should log warning and return undefined when error occurs', async () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const mockError = new Error('File not found');
|
|
358
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
|
|
359
|
+
|
|
360
|
+
// Act
|
|
361
|
+
const result = await gitDataProvider.GetFileFromGitRepo(
|
|
362
|
+
mockProjectName,
|
|
363
|
+
mockRepoId,
|
|
364
|
+
mockFileName,
|
|
365
|
+
mockVersion
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Assert
|
|
369
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
370
|
+
expect.stringContaining(`File ${mockFileName} could not be read: ${mockError.message}`)
|
|
371
|
+
);
|
|
372
|
+
expect(result).toBeUndefined();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('GitDataProvider - CheckIfItemExist', () => {
|
|
377
|
+
let gitDataProvider: GitDataProvider;
|
|
378
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
379
|
+
const mockToken = 'mock-token';
|
|
380
|
+
const mockGitApiUrl = 'https://dev.azure.com/orgname/project/_apis/git/repositories/repo-id';
|
|
381
|
+
const mockItemPath = 'path/to/file.txt';
|
|
382
|
+
const mockVersion = { version: 'main', versionType: 'branch' };
|
|
383
|
+
|
|
384
|
+
beforeEach(() => {
|
|
385
|
+
jest.clearAllMocks();
|
|
386
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should return true when item exists', async () => {
|
|
390
|
+
// Arrange
|
|
391
|
+
const mockResponse = { path: mockItemPath, content: 'content' };
|
|
392
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
393
|
+
|
|
394
|
+
// Act
|
|
395
|
+
const result = await gitDataProvider.CheckIfItemExist(mockGitApiUrl, mockItemPath, mockVersion);
|
|
396
|
+
|
|
397
|
+
// Assert
|
|
398
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
399
|
+
expect.stringContaining(`${mockGitApiUrl}/items?path=${mockItemPath}`),
|
|
400
|
+
mockToken,
|
|
401
|
+
'get',
|
|
402
|
+
{},
|
|
403
|
+
{},
|
|
404
|
+
false
|
|
405
|
+
);
|
|
406
|
+
expect(result).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should return false when item does not exist', async () => {
|
|
410
|
+
// Arrange
|
|
411
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('Not found'));
|
|
412
|
+
|
|
413
|
+
// Act
|
|
414
|
+
const result = await gitDataProvider.CheckIfItemExist(mockGitApiUrl, mockItemPath, mockVersion);
|
|
415
|
+
|
|
416
|
+
// Assert
|
|
417
|
+
expect(result).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should return false when API returns null', async () => {
|
|
421
|
+
// Arrange
|
|
422
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(null);
|
|
423
|
+
|
|
424
|
+
// Act
|
|
425
|
+
const result = await gitDataProvider.CheckIfItemExist(mockGitApiUrl, mockItemPath, mockVersion);
|
|
426
|
+
|
|
427
|
+
// Assert
|
|
428
|
+
expect(result).toBe(false);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should handle special characters in version', async () => {
|
|
432
|
+
// Arrange
|
|
433
|
+
const specialVersion = { version: 'feature/branch#123', versionType: 'branch' };
|
|
434
|
+
const mockResponse = { path: mockItemPath };
|
|
435
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
436
|
+
|
|
437
|
+
// Act
|
|
438
|
+
await gitDataProvider.CheckIfItemExist(mockGitApiUrl, mockItemPath, specialVersion);
|
|
439
|
+
|
|
440
|
+
// Assert
|
|
441
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
442
|
+
expect.stringContaining('versionDescriptor.version=feature%2Fbranch%23123'),
|
|
443
|
+
expect.anything(),
|
|
444
|
+
expect.anything(),
|
|
445
|
+
expect.anything(),
|
|
446
|
+
expect.anything(),
|
|
447
|
+
expect.anything()
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('GitDataProvider - GetPullRequestsInCommitRangeWithoutLinkedItems', () => {
|
|
453
|
+
let gitDataProvider: GitDataProvider;
|
|
454
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
455
|
+
const mockToken = 'mock-token';
|
|
456
|
+
const mockProjectId = 'project-123';
|
|
457
|
+
const mockRepoId = 'repo-456';
|
|
458
|
+
|
|
459
|
+
beforeEach(() => {
|
|
460
|
+
jest.clearAllMocks();
|
|
461
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should return filtered pull requests matching commit ids', async () => {
|
|
465
|
+
// Arrange
|
|
466
|
+
const mockCommits = {
|
|
467
|
+
value: [{ commitId: 'commit-1' }, { commitId: 'commit-2' }],
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const mockPullRequests = {
|
|
471
|
+
count: 3,
|
|
472
|
+
value: [
|
|
473
|
+
{
|
|
474
|
+
pullRequestId: 101,
|
|
475
|
+
title: 'PR 1',
|
|
476
|
+
createdBy: { displayName: 'User 1' },
|
|
477
|
+
creationDate: '2023-01-01',
|
|
478
|
+
closedDate: '2023-01-02',
|
|
479
|
+
description: 'Description 1',
|
|
480
|
+
lastMergeCommit: { commitId: 'commit-1' },
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
pullRequestId: 102,
|
|
484
|
+
title: 'PR 2',
|
|
485
|
+
createdBy: { displayName: 'User 2' },
|
|
486
|
+
creationDate: '2023-02-01',
|
|
487
|
+
closedDate: '2023-02-02',
|
|
488
|
+
description: 'Description 2',
|
|
489
|
+
lastMergeCommit: { commitId: 'commit-3' }, // Not in our commit range
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
pullRequestId: 103,
|
|
493
|
+
title: 'PR 3',
|
|
494
|
+
createdBy: { displayName: 'User 3' },
|
|
495
|
+
creationDate: '2023-03-01',
|
|
496
|
+
closedDate: '2023-03-02',
|
|
497
|
+
description: 'Description 3',
|
|
498
|
+
lastMergeCommit: { commitId: 'commit-2' },
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockPullRequests);
|
|
504
|
+
|
|
505
|
+
// Act
|
|
506
|
+
const result = await gitDataProvider.GetPullRequestsInCommitRangeWithoutLinkedItems(
|
|
507
|
+
mockProjectId,
|
|
508
|
+
mockRepoId,
|
|
509
|
+
mockCommits
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Assert
|
|
513
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
514
|
+
expect.stringContaining(
|
|
515
|
+
`${mockOrgUrl}${mockProjectId}/_apis/git/repositories/${mockRepoId}/pullrequests`
|
|
516
|
+
),
|
|
517
|
+
mockToken,
|
|
518
|
+
'get'
|
|
519
|
+
);
|
|
520
|
+
expect(result).toHaveLength(2);
|
|
521
|
+
expect(result[0].pullRequestId).toBe(101);
|
|
522
|
+
expect(result[1].pullRequestId).toBe(103);
|
|
523
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
524
|
+
expect.stringContaining('filtered in commit range 2 pullrequests')
|
|
525
|
+
);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should return empty array when no matching pull requests', async () => {
|
|
529
|
+
// Arrange
|
|
530
|
+
const mockCommits = {
|
|
531
|
+
value: [
|
|
532
|
+
{ commitId: 'commit-999' }, // Not matching any PRs
|
|
533
|
+
],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const mockPullRequests = {
|
|
537
|
+
count: 2,
|
|
538
|
+
value: [
|
|
539
|
+
{
|
|
540
|
+
pullRequestId: 101,
|
|
541
|
+
lastMergeCommit: { commitId: 'commit-1' },
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
pullRequestId: 102,
|
|
545
|
+
lastMergeCommit: { commitId: 'commit-2' },
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockPullRequests);
|
|
551
|
+
|
|
552
|
+
// Act
|
|
553
|
+
const result = await gitDataProvider.GetPullRequestsInCommitRangeWithoutLinkedItems(
|
|
554
|
+
mockProjectId,
|
|
555
|
+
mockRepoId,
|
|
556
|
+
mockCommits
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// Assert
|
|
560
|
+
expect(result).toHaveLength(0);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should handle API errors appropriately', async () => {
|
|
564
|
+
// Arrange
|
|
565
|
+
const mockCommits = { value: [{ commitId: 'commit-1' }] };
|
|
566
|
+
const mockError = new Error('API Error');
|
|
567
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
|
|
568
|
+
|
|
569
|
+
// Act & Assert
|
|
570
|
+
await expect(
|
|
571
|
+
gitDataProvider.GetPullRequestsInCommitRangeWithoutLinkedItems(mockProjectId, mockRepoId, mockCommits)
|
|
572
|
+
).rejects.toThrow('API Error');
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('GitDataProvider - GetBranch', () => {
|
|
577
|
+
let gitDataProvider: GitDataProvider;
|
|
578
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
579
|
+
const mockToken = 'mock-token';
|
|
580
|
+
const mockGitRepoUrl = 'https://dev.azure.com/orgname/project/_apis/git/repositories/repo-id';
|
|
581
|
+
|
|
582
|
+
beforeEach(() => {
|
|
583
|
+
jest.clearAllMocks();
|
|
584
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should return branch info when branch exists', async () => {
|
|
588
|
+
// Arrange
|
|
589
|
+
const branchName = 'main';
|
|
590
|
+
const mockResponse = {
|
|
591
|
+
value: [{ name: 'refs/heads/main', objectId: 'abc123' }],
|
|
592
|
+
};
|
|
593
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
594
|
+
|
|
595
|
+
// Act
|
|
596
|
+
const result = await gitDataProvider.GetBranch(mockGitRepoUrl, branchName);
|
|
597
|
+
|
|
598
|
+
// Assert
|
|
599
|
+
expect(result).toEqual({ name: 'refs/heads/main', objectId: 'abc123' });
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should return null when branch does not exist', async () => {
|
|
603
|
+
// Arrange
|
|
604
|
+
const mockResponse = { value: [] };
|
|
605
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
606
|
+
|
|
607
|
+
// Act
|
|
608
|
+
const result = await gitDataProvider.GetBranch(mockGitRepoUrl, 'nonexistent');
|
|
609
|
+
|
|
610
|
+
// Assert
|
|
611
|
+
expect(result).toBeNull();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('should return null when response is empty', async () => {
|
|
615
|
+
// Arrange
|
|
616
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(null);
|
|
617
|
+
|
|
618
|
+
// Act
|
|
619
|
+
const result = await gitDataProvider.GetBranch(mockGitRepoUrl, 'main');
|
|
620
|
+
|
|
621
|
+
// Assert
|
|
622
|
+
expect(result).toBeNull();
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe('GitDataProvider - GetGitRepoFromPrId', () => {
|
|
627
|
+
let gitDataProvider: GitDataProvider;
|
|
628
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
629
|
+
const mockToken = 'mock-token';
|
|
630
|
+
|
|
631
|
+
beforeEach(() => {
|
|
632
|
+
jest.clearAllMocks();
|
|
633
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should fetch PR by ID', async () => {
|
|
637
|
+
// Arrange
|
|
638
|
+
const prId = 123;
|
|
639
|
+
const mockResponse = { pullRequestId: prId, title: 'Test PR' };
|
|
640
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
641
|
+
|
|
642
|
+
// Act
|
|
643
|
+
const result = await gitDataProvider.GetGitRepoFromPrId(prId);
|
|
644
|
+
|
|
645
|
+
// Assert
|
|
646
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
647
|
+
`${mockOrgUrl}_apis/git/pullrequests/${prId}`,
|
|
648
|
+
mockToken,
|
|
649
|
+
'get'
|
|
650
|
+
);
|
|
651
|
+
expect(result).toEqual(mockResponse);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe('GitDataProvider - GetPullRequestCommits', () => {
|
|
656
|
+
let gitDataProvider: GitDataProvider;
|
|
657
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
658
|
+
const mockToken = 'mock-token';
|
|
659
|
+
|
|
660
|
+
beforeEach(() => {
|
|
661
|
+
jest.clearAllMocks();
|
|
662
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should fetch PR commits', async () => {
|
|
666
|
+
// Arrange
|
|
667
|
+
const repoId = 'repo-123';
|
|
668
|
+
const prId = 456;
|
|
669
|
+
const mockResponse = { value: [{ commitId: 'abc123' }] };
|
|
670
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
671
|
+
|
|
672
|
+
// Act
|
|
673
|
+
const result = await gitDataProvider.GetPullRequestCommits(repoId, prId);
|
|
674
|
+
|
|
675
|
+
// Assert
|
|
676
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
677
|
+
`${mockOrgUrl}_apis/git/repositories/${repoId}/pullRequests/${prId}/commits`,
|
|
678
|
+
mockToken,
|
|
679
|
+
'get'
|
|
680
|
+
);
|
|
681
|
+
expect(result).toEqual(mockResponse);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
describe('GitDataProvider - GetCommitByCommitId', () => {
|
|
686
|
+
let gitDataProvider: GitDataProvider;
|
|
687
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
688
|
+
const mockToken = 'mock-token';
|
|
689
|
+
|
|
690
|
+
beforeEach(() => {
|
|
691
|
+
jest.clearAllMocks();
|
|
692
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('should fetch commit by SHA', async () => {
|
|
696
|
+
// Arrange
|
|
697
|
+
const projectId = 'project-123';
|
|
698
|
+
const repoId = 'repo-456';
|
|
699
|
+
const commitSha = 'abc123def456';
|
|
700
|
+
const mockResponse = { commitId: commitSha, comment: 'Test commit' };
|
|
701
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
702
|
+
|
|
703
|
+
// Act
|
|
704
|
+
const result = await gitDataProvider.GetCommitByCommitId(projectId, repoId, commitSha);
|
|
705
|
+
|
|
706
|
+
// Assert
|
|
707
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
708
|
+
`${mockOrgUrl}${projectId}/_apis/git/repositories/${repoId}/commits/${commitSha}`,
|
|
709
|
+
mockToken,
|
|
710
|
+
'get'
|
|
711
|
+
);
|
|
712
|
+
expect(result).toEqual(mockResponse);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
describe('GitDataProvider - GetItemsForPipelinesRange', () => {
|
|
717
|
+
let gitDataProvider: GitDataProvider;
|
|
718
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
719
|
+
const mockToken = 'mock-token';
|
|
720
|
+
|
|
721
|
+
beforeEach(() => {
|
|
722
|
+
jest.clearAllMocks();
|
|
723
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('should fetch items in pipeline range', async () => {
|
|
727
|
+
// Arrange
|
|
728
|
+
const projectId = 'project-123';
|
|
729
|
+
const fromBuildId = 100;
|
|
730
|
+
const toBuildId = 200;
|
|
731
|
+
const mockWorkItemsResponse = {
|
|
732
|
+
count: 1,
|
|
733
|
+
value: [{ id: 1 }],
|
|
734
|
+
};
|
|
735
|
+
const mockWorkItemResponse = { id: 1, fields: { 'System.Title': 'Test' } };
|
|
736
|
+
|
|
737
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
738
|
+
.mockResolvedValueOnce(mockWorkItemsResponse)
|
|
739
|
+
.mockResolvedValueOnce(mockWorkItemResponse);
|
|
740
|
+
|
|
741
|
+
// Act
|
|
742
|
+
const result = await gitDataProvider.GetItemsForPipelinesRange(projectId, fromBuildId, toBuildId);
|
|
743
|
+
|
|
744
|
+
// Assert
|
|
745
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
746
|
+
expect.stringContaining(`fromBuildId=${fromBuildId}&toBuildId=${toBuildId}`),
|
|
747
|
+
mockToken,
|
|
748
|
+
'get'
|
|
749
|
+
);
|
|
750
|
+
expect(result).toHaveLength(1);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe('GitDataProvider - GetCommitsInDateRange', () => {
|
|
755
|
+
let gitDataProvider: GitDataProvider;
|
|
756
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
757
|
+
const mockToken = 'mock-token';
|
|
758
|
+
|
|
759
|
+
beforeEach(() => {
|
|
760
|
+
jest.clearAllMocks();
|
|
761
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should fetch commits in date range without branch', async () => {
|
|
765
|
+
// Arrange
|
|
766
|
+
const projectId = 'project-123';
|
|
767
|
+
const repoId = 'repo-456';
|
|
768
|
+
const fromDate = '2023-01-01';
|
|
769
|
+
const toDate = '2023-12-31';
|
|
770
|
+
const mockResponse = { value: [{ commitId: 'abc123' }] };
|
|
771
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
772
|
+
|
|
773
|
+
// Act
|
|
774
|
+
const result = await gitDataProvider.GetCommitsInDateRange(projectId, repoId, fromDate, toDate);
|
|
775
|
+
|
|
776
|
+
// Assert
|
|
777
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
778
|
+
expect.stringContaining(`fromDate=${fromDate}`),
|
|
779
|
+
mockToken,
|
|
780
|
+
'get'
|
|
781
|
+
);
|
|
782
|
+
expect(result).toEqual(mockResponse);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('should fetch commits in date range with branch', async () => {
|
|
786
|
+
// Arrange
|
|
787
|
+
const projectId = 'project-123';
|
|
788
|
+
const repoId = 'repo-456';
|
|
789
|
+
const fromDate = '2023-01-01';
|
|
790
|
+
const toDate = '2023-12-31';
|
|
791
|
+
const branchName = 'main';
|
|
792
|
+
const mockResponse = { value: [{ commitId: 'abc123' }] };
|
|
793
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
794
|
+
|
|
795
|
+
// Act
|
|
796
|
+
const result = await gitDataProvider.GetCommitsInDateRange(
|
|
797
|
+
projectId,
|
|
798
|
+
repoId,
|
|
799
|
+
fromDate,
|
|
800
|
+
toDate,
|
|
801
|
+
branchName
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// Assert
|
|
805
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
806
|
+
expect.stringContaining(`itemVersion.version=${branchName}`),
|
|
807
|
+
mockToken,
|
|
808
|
+
'get'
|
|
809
|
+
);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
describe('GitDataProvider - GetCommitsInCommitRange', () => {
|
|
814
|
+
let gitDataProvider: GitDataProvider;
|
|
815
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
816
|
+
const mockToken = 'mock-token';
|
|
817
|
+
|
|
818
|
+
beforeEach(() => {
|
|
819
|
+
jest.clearAllMocks();
|
|
820
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('should fetch commits between two commit SHAs', async () => {
|
|
824
|
+
// Arrange
|
|
825
|
+
const projectId = 'project-123';
|
|
826
|
+
const repoId = 'repo-456';
|
|
827
|
+
const fromSha = 'abc123';
|
|
828
|
+
const toSha = 'def456';
|
|
829
|
+
const mockResponse = { value: [{ commitId: 'xyz789' }] };
|
|
830
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
831
|
+
|
|
832
|
+
// Act
|
|
833
|
+
const result = await gitDataProvider.GetCommitsInCommitRange(projectId, repoId, fromSha, toSha);
|
|
834
|
+
|
|
835
|
+
// Assert
|
|
836
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
837
|
+
expect.stringContaining(`fromCommitId=${fromSha}&searchCriteria.toCommitId=${toSha}`),
|
|
838
|
+
mockToken,
|
|
839
|
+
'get'
|
|
840
|
+
);
|
|
841
|
+
expect(result).toEqual(mockResponse);
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
describe('GitDataProvider - CreatePullRequestComment', () => {
|
|
846
|
+
let gitDataProvider: GitDataProvider;
|
|
847
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
848
|
+
const mockToken = 'mock-token';
|
|
849
|
+
|
|
850
|
+
beforeEach(() => {
|
|
851
|
+
jest.clearAllMocks();
|
|
852
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('should create a PR comment thread', async () => {
|
|
856
|
+
// Arrange
|
|
857
|
+
const projectName = 'project-123';
|
|
858
|
+
const repoId = 'repo-456';
|
|
859
|
+
const prId = 789;
|
|
860
|
+
const threads = { comments: [{ content: 'Test comment' }] };
|
|
861
|
+
const mockResponse = { id: 1, comments: threads.comments };
|
|
862
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
863
|
+
|
|
864
|
+
// Act
|
|
865
|
+
const result = await gitDataProvider.CreatePullRequestComment(projectName, repoId, prId, threads);
|
|
866
|
+
|
|
867
|
+
// Assert
|
|
868
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
869
|
+
expect.stringContaining(`pullRequests/${prId}/threads`),
|
|
870
|
+
mockToken,
|
|
871
|
+
'post',
|
|
872
|
+
threads,
|
|
873
|
+
null
|
|
874
|
+
);
|
|
875
|
+
expect(result).toEqual(mockResponse);
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
describe('GitDataProvider - GetPullRequestComments', () => {
|
|
880
|
+
let gitDataProvider: GitDataProvider;
|
|
881
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
882
|
+
const mockToken = 'mock-token';
|
|
883
|
+
|
|
884
|
+
beforeEach(() => {
|
|
885
|
+
jest.clearAllMocks();
|
|
886
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('should fetch PR comments', async () => {
|
|
890
|
+
// Arrange
|
|
891
|
+
const projectName = 'project-123';
|
|
892
|
+
const repoId = 'repo-456';
|
|
893
|
+
const prId = 789;
|
|
894
|
+
const mockResponse = { value: [{ id: 1, comments: [] }] };
|
|
895
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
896
|
+
|
|
897
|
+
// Act
|
|
898
|
+
const result = await gitDataProvider.GetPullRequestComments(projectName, repoId, prId);
|
|
899
|
+
|
|
900
|
+
// Assert
|
|
901
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
902
|
+
expect.stringContaining(`pullRequests/${prId}/threads`),
|
|
903
|
+
mockToken,
|
|
904
|
+
'get',
|
|
905
|
+
null,
|
|
906
|
+
null
|
|
907
|
+
);
|
|
908
|
+
expect(result).toEqual(mockResponse);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
describe('GitDataProvider - GetCommitsForRepo', () => {
|
|
913
|
+
let gitDataProvider: GitDataProvider;
|
|
914
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
915
|
+
const mockToken = 'mock-token';
|
|
916
|
+
|
|
917
|
+
beforeEach(() => {
|
|
918
|
+
jest.clearAllMocks();
|
|
919
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('should fetch commits for repo with version identifier', async () => {
|
|
923
|
+
// Arrange
|
|
924
|
+
const projectName = 'project-123';
|
|
925
|
+
const repoId = 'repo-456';
|
|
926
|
+
const versionId = 'main';
|
|
927
|
+
const mockResponse = {
|
|
928
|
+
count: 2,
|
|
929
|
+
value: [
|
|
930
|
+
{ commitId: 'abc123', comment: 'First commit', committer: { date: '2023-01-01' } },
|
|
931
|
+
{ commitId: 'def456', comment: 'Second commit', author: { date: '2023-01-02' } },
|
|
932
|
+
],
|
|
933
|
+
};
|
|
934
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
935
|
+
|
|
936
|
+
// Act
|
|
937
|
+
const result = await gitDataProvider.GetCommitsForRepo(projectName, repoId, versionId);
|
|
938
|
+
|
|
939
|
+
// Assert
|
|
940
|
+
expect(result).toHaveLength(2);
|
|
941
|
+
expect(result[0].name).toContain('abc123');
|
|
942
|
+
expect(result[0].value).toBe('abc123');
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('should return empty array when no commits', async () => {
|
|
946
|
+
// Arrange
|
|
947
|
+
const mockResponse = { count: 0, value: [] };
|
|
948
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
949
|
+
|
|
950
|
+
// Act
|
|
951
|
+
const result = await gitDataProvider.GetCommitsForRepo('project', 'repo');
|
|
952
|
+
|
|
953
|
+
// Assert
|
|
954
|
+
expect(result).toEqual([]);
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
describe('GitDataProvider - GetPullRequestsForRepo', () => {
|
|
959
|
+
let gitDataProvider: GitDataProvider;
|
|
960
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
961
|
+
const mockToken = 'mock-token';
|
|
962
|
+
|
|
963
|
+
beforeEach(() => {
|
|
964
|
+
jest.clearAllMocks();
|
|
965
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('should fetch PRs for repo', async () => {
|
|
969
|
+
// Arrange
|
|
970
|
+
const projectName = 'project-123';
|
|
971
|
+
const repoId = 'repo-456';
|
|
972
|
+
const mockResponse = { count: 2, value: [{ pullRequestId: 1 }, { pullRequestId: 2 }] };
|
|
973
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
974
|
+
|
|
975
|
+
// Act
|
|
976
|
+
const result = await gitDataProvider.GetPullRequestsForRepo(projectName, repoId);
|
|
977
|
+
|
|
978
|
+
// Assert
|
|
979
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
980
|
+
expect.stringContaining('pullrequests?status=completed'),
|
|
981
|
+
mockToken,
|
|
982
|
+
'get',
|
|
983
|
+
null,
|
|
984
|
+
null
|
|
985
|
+
);
|
|
986
|
+
expect(result).toEqual(mockResponse);
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
describe('GitDataProvider - GetRepoBranches', () => {
|
|
991
|
+
let gitDataProvider: GitDataProvider;
|
|
992
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
993
|
+
const mockToken = 'mock-token';
|
|
994
|
+
|
|
995
|
+
beforeEach(() => {
|
|
996
|
+
jest.clearAllMocks();
|
|
997
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should fetch repo branches', async () => {
|
|
1001
|
+
// Arrange
|
|
1002
|
+
const projectName = 'project-123';
|
|
1003
|
+
const repoId = 'repo-456';
|
|
1004
|
+
const mockResponse = { value: [{ name: 'refs/heads/main' }, { name: 'refs/heads/develop' }] };
|
|
1005
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
1006
|
+
|
|
1007
|
+
// Act
|
|
1008
|
+
const result = await gitDataProvider.GetRepoBranches(projectName, repoId);
|
|
1009
|
+
|
|
1010
|
+
// Assert
|
|
1011
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
1012
|
+
expect.stringContaining('refs?searchCriteria.$top=1000&filter=heads'),
|
|
1013
|
+
mockToken,
|
|
1014
|
+
'get',
|
|
1015
|
+
null,
|
|
1016
|
+
null
|
|
1017
|
+
);
|
|
1018
|
+
expect(result).toEqual(mockResponse);
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
describe('GitDataProvider - GetPullRequestsLinkedItemsInCommitRange', () => {
|
|
1023
|
+
let gitDataProvider: GitDataProvider;
|
|
1024
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1025
|
+
const mockToken = 'mock-token';
|
|
1026
|
+
|
|
1027
|
+
beforeEach(() => {
|
|
1028
|
+
jest.clearAllMocks();
|
|
1029
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('should fetch and filter PRs with linked work items', async () => {
|
|
1033
|
+
// Arrange
|
|
1034
|
+
const projectId = 'project-123';
|
|
1035
|
+
const repoId = 'repo-456';
|
|
1036
|
+
const commitRange = { value: [{ commitId: 'commit-1' }] };
|
|
1037
|
+
|
|
1038
|
+
const mockPRsResponse = {
|
|
1039
|
+
count: 1,
|
|
1040
|
+
value: [
|
|
1041
|
+
{
|
|
1042
|
+
pullRequestId: 101,
|
|
1043
|
+
lastMergeCommit: { commitId: 'commit-1' },
|
|
1044
|
+
_links: { workItems: { href: 'https://example.com/workitems' } },
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
const mockWorkItemsResponse = {
|
|
1050
|
+
count: 1,
|
|
1051
|
+
value: [{ id: 1 }],
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const mockPopulatedWI = { id: 1, fields: { 'System.Title': 'Test WI' } };
|
|
1055
|
+
|
|
1056
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1057
|
+
.mockResolvedValueOnce(mockPRsResponse)
|
|
1058
|
+
.mockResolvedValueOnce(mockWorkItemsResponse)
|
|
1059
|
+
.mockResolvedValueOnce(mockPopulatedWI);
|
|
1060
|
+
|
|
1061
|
+
// Act
|
|
1062
|
+
const result = await gitDataProvider.GetPullRequestsLinkedItemsInCommitRange(
|
|
1063
|
+
projectId,
|
|
1064
|
+
repoId,
|
|
1065
|
+
commitRange
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
// Assert
|
|
1069
|
+
expect(result).toHaveLength(1);
|
|
1070
|
+
expect(result[0].workItem.id).toBe(1);
|
|
1071
|
+
expect(result[0].pullrequest.pullRequestId).toBe(101);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it('should handle PRs without linked work items', async () => {
|
|
1075
|
+
// Arrange
|
|
1076
|
+
const projectId = 'project-123';
|
|
1077
|
+
const repoId = 'repo-456';
|
|
1078
|
+
const commitRange = { value: [{ commitId: 'commit-1' }] };
|
|
1079
|
+
|
|
1080
|
+
const mockPRsResponse = {
|
|
1081
|
+
count: 1,
|
|
1082
|
+
value: [
|
|
1083
|
+
{
|
|
1084
|
+
pullRequestId: 101,
|
|
1085
|
+
lastMergeCommit: { commitId: 'commit-1' },
|
|
1086
|
+
_links: {},
|
|
1087
|
+
},
|
|
1088
|
+
],
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockPRsResponse);
|
|
1092
|
+
|
|
1093
|
+
// Act
|
|
1094
|
+
const result = await gitDataProvider.GetPullRequestsLinkedItemsInCommitRange(
|
|
1095
|
+
projectId,
|
|
1096
|
+
repoId,
|
|
1097
|
+
commitRange
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
// Assert
|
|
1101
|
+
expect(result).toHaveLength(0);
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
describe('GitDataProvider - GetItemsInCommitRange', () => {
|
|
1106
|
+
let gitDataProvider: GitDataProvider;
|
|
1107
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1108
|
+
const mockToken = 'mock-token';
|
|
1109
|
+
|
|
1110
|
+
beforeEach(() => {
|
|
1111
|
+
jest.clearAllMocks();
|
|
1112
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it('should process commits with work items', async () => {
|
|
1116
|
+
// Arrange
|
|
1117
|
+
const projectId = 'project-123';
|
|
1118
|
+
const repoId = 'repo-456';
|
|
1119
|
+
const commitRange = {
|
|
1120
|
+
value: [
|
|
1121
|
+
{
|
|
1122
|
+
commitId: 'commit-1',
|
|
1123
|
+
workItems: [{ id: 1 }],
|
|
1124
|
+
},
|
|
1125
|
+
],
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const mockPopulatedWI = { id: 1, fields: { 'System.Title': 'Test WI' } };
|
|
1129
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1130
|
+
|
|
1131
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1132
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1133
|
+
.mockResolvedValueOnce(mockPRsResponse);
|
|
1134
|
+
|
|
1135
|
+
// Act
|
|
1136
|
+
const result = await gitDataProvider.GetItemsInCommitRange(projectId, repoId, commitRange, null, false);
|
|
1137
|
+
|
|
1138
|
+
// Assert
|
|
1139
|
+
expect(result.commitChangesArray).toHaveLength(1);
|
|
1140
|
+
expect(result.commitsWithNoRelations).toHaveLength(0);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it('should include unlinked commits when flag is true', async () => {
|
|
1144
|
+
// Arrange
|
|
1145
|
+
const projectId = 'project-123';
|
|
1146
|
+
const repoId = 'repo-456';
|
|
1147
|
+
const commitRange = {
|
|
1148
|
+
value: [
|
|
1149
|
+
{
|
|
1150
|
+
commitId: 'commit-1',
|
|
1151
|
+
workItems: [],
|
|
1152
|
+
committer: { date: '2023-01-01', name: 'Test User' },
|
|
1153
|
+
comment: 'Test commit',
|
|
1154
|
+
remoteUrl: 'https://example.com/commit/1',
|
|
1155
|
+
},
|
|
1156
|
+
],
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1160
|
+
|
|
1161
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockPRsResponse);
|
|
1162
|
+
|
|
1163
|
+
// Act
|
|
1164
|
+
const result = await gitDataProvider.GetItemsInCommitRange(projectId, repoId, commitRange, null, true);
|
|
1165
|
+
|
|
1166
|
+
// Assert
|
|
1167
|
+
expect(result.commitChangesArray).toHaveLength(0);
|
|
1168
|
+
expect(result.commitsWithNoRelations).toHaveLength(1);
|
|
1169
|
+
expect(result.commitsWithNoRelations[0].commitId).toBe('commit-1');
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
describe('GitDataProvider - getItemsForPipelineRange', () => {
|
|
1174
|
+
let gitDataProvider: GitDataProvider;
|
|
1175
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1176
|
+
const mockToken = 'mock-token';
|
|
1177
|
+
|
|
1178
|
+
beforeEach(() => {
|
|
1179
|
+
jest.clearAllMocks();
|
|
1180
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('should throw error when extended commits is empty', async () => {
|
|
1184
|
+
// Arrange
|
|
1185
|
+
const teamProject = 'project-123';
|
|
1186
|
+
const extendedCommits: any[] = [];
|
|
1187
|
+
const targetRepo = { url: 'https://example.com/repo' };
|
|
1188
|
+
const addedWorkItemByIdSet = new Set<number>();
|
|
1189
|
+
|
|
1190
|
+
// Act
|
|
1191
|
+
const result = await gitDataProvider.getItemsForPipelineRange(
|
|
1192
|
+
teamProject,
|
|
1193
|
+
extendedCommits,
|
|
1194
|
+
targetRepo,
|
|
1195
|
+
addedWorkItemByIdSet
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
// Assert - should log error and return empty arrays
|
|
1199
|
+
expect(logger.error).toHaveBeenCalledWith('extended commits cannot be empty');
|
|
1200
|
+
expect(result.commitChangesArray).toHaveLength(0);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it('should process commits with work items', async () => {
|
|
1204
|
+
// Arrange
|
|
1205
|
+
const teamProject = 'project-123';
|
|
1206
|
+
const extendedCommits = [
|
|
1207
|
+
{
|
|
1208
|
+
commit: {
|
|
1209
|
+
commitId: 'commit-1',
|
|
1210
|
+
workItems: [{ id: 1 }],
|
|
1211
|
+
committer: { date: '2023-01-01', name: 'Test User' },
|
|
1212
|
+
comment: 'Test commit',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
];
|
|
1216
|
+
const targetRepo = { url: 'https://example.com/repo' };
|
|
1217
|
+
const addedWorkItemByIdSet = new Set<number>();
|
|
1218
|
+
|
|
1219
|
+
const mockRepoData = {
|
|
1220
|
+
_links: { web: { href: 'https://example.com/repo-web' } },
|
|
1221
|
+
project: { id: 'project-id' },
|
|
1222
|
+
};
|
|
1223
|
+
const mockPopulatedWI = { id: 1, fields: { 'System.Title': 'Test WI' } };
|
|
1224
|
+
|
|
1225
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1226
|
+
.mockResolvedValueOnce(mockRepoData)
|
|
1227
|
+
.mockResolvedValueOnce(mockPopulatedWI);
|
|
1228
|
+
|
|
1229
|
+
// Act
|
|
1230
|
+
const result = await gitDataProvider.getItemsForPipelineRange(
|
|
1231
|
+
teamProject,
|
|
1232
|
+
extendedCommits,
|
|
1233
|
+
targetRepo,
|
|
1234
|
+
addedWorkItemByIdSet
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
// Assert
|
|
1238
|
+
expect(result.commitChangesArray).toHaveLength(1);
|
|
1239
|
+
expect(result.commitChangesArray[0].workItem.id).toBe(1);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it('should include unlinked commits when flag is true', async () => {
|
|
1243
|
+
// Arrange
|
|
1244
|
+
const teamProject = 'project-123';
|
|
1245
|
+
const extendedCommits = [
|
|
1246
|
+
{
|
|
1247
|
+
commit: {
|
|
1248
|
+
commitId: 'commit-1',
|
|
1249
|
+
workItems: [],
|
|
1250
|
+
committer: { date: '2023-01-01', name: 'Test User' },
|
|
1251
|
+
comment: 'Test commit',
|
|
1252
|
+
remoteUrl: 'https://example.com/commit/1',
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
];
|
|
1256
|
+
const targetRepo = { url: 'https://example.com/repo' };
|
|
1257
|
+
const addedWorkItemByIdSet = new Set<number>();
|
|
1258
|
+
|
|
1259
|
+
const mockRepoData = {
|
|
1260
|
+
_links: { web: { href: 'https://example.com/repo-web' } },
|
|
1261
|
+
project: { id: 'project-id' },
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockRepoData);
|
|
1265
|
+
|
|
1266
|
+
// Act
|
|
1267
|
+
const result = await gitDataProvider.getItemsForPipelineRange(
|
|
1268
|
+
teamProject,
|
|
1269
|
+
extendedCommits,
|
|
1270
|
+
targetRepo,
|
|
1271
|
+
addedWorkItemByIdSet,
|
|
1272
|
+
undefined,
|
|
1273
|
+
true
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
// Assert
|
|
1277
|
+
expect(result.commitsWithNoRelations).toHaveLength(1);
|
|
1278
|
+
expect(result.commitsWithNoRelations[0].commitId).toBe('commit-1');
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('should not add duplicate work items', async () => {
|
|
1282
|
+
// Arrange
|
|
1283
|
+
const teamProject = 'project-123';
|
|
1284
|
+
const extendedCommits = [
|
|
1285
|
+
{ commit: { commitId: 'commit-1', workItems: [{ id: 1 }] } },
|
|
1286
|
+
{ commit: { commitId: 'commit-2', workItems: [{ id: 1 }] } }, // Same WI
|
|
1287
|
+
];
|
|
1288
|
+
const targetRepo = { url: 'https://example.com/repo' };
|
|
1289
|
+
const addedWorkItemByIdSet = new Set<number>();
|
|
1290
|
+
|
|
1291
|
+
const mockRepoData = {
|
|
1292
|
+
_links: { web: { href: 'https://example.com/repo-web' } },
|
|
1293
|
+
project: { id: 'project-id' },
|
|
1294
|
+
};
|
|
1295
|
+
const mockPopulatedWI = { id: 1, fields: { 'System.Title': 'Test WI' } };
|
|
1296
|
+
|
|
1297
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1298
|
+
.mockResolvedValueOnce(mockRepoData)
|
|
1299
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1300
|
+
.mockResolvedValueOnce(mockPopulatedWI);
|
|
1301
|
+
|
|
1302
|
+
// Act
|
|
1303
|
+
const result = await gitDataProvider.getItemsForPipelineRange(
|
|
1304
|
+
teamProject,
|
|
1305
|
+
extendedCommits,
|
|
1306
|
+
targetRepo,
|
|
1307
|
+
addedWorkItemByIdSet
|
|
1308
|
+
);
|
|
1309
|
+
|
|
1310
|
+
// Assert - should only have 1 work item (no duplicates)
|
|
1311
|
+
expect(result.commitChangesArray).toHaveLength(1);
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
describe('GitDataProvider - GetItemsInPullRequestRange', () => {
|
|
1316
|
+
let gitDataProvider: GitDataProvider;
|
|
1317
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1318
|
+
const mockToken = 'mock-token';
|
|
1319
|
+
|
|
1320
|
+
beforeEach(() => {
|
|
1321
|
+
jest.clearAllMocks();
|
|
1322
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
it('should fetch items from PRs in range', async () => {
|
|
1326
|
+
// Arrange
|
|
1327
|
+
const projectId = 'project-123';
|
|
1328
|
+
const repoId = 'repo-456';
|
|
1329
|
+
const prIds = [101];
|
|
1330
|
+
|
|
1331
|
+
const mockPRsResponse = {
|
|
1332
|
+
count: 1,
|
|
1333
|
+
value: [{ pullRequestId: 101, _links: { workItems: { href: 'https://example.com/wi1' } } }],
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
const mockWIResponse = { count: 1, value: [{ id: 1 }] };
|
|
1337
|
+
const mockPopulatedWI = { id: 1, fields: { 'System.Title': 'WI 1' } };
|
|
1338
|
+
|
|
1339
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1340
|
+
.mockResolvedValueOnce(mockPRsResponse)
|
|
1341
|
+
.mockResolvedValueOnce(mockWIResponse)
|
|
1342
|
+
.mockResolvedValueOnce(mockPopulatedWI);
|
|
1343
|
+
|
|
1344
|
+
// Act
|
|
1345
|
+
const result = await gitDataProvider.GetItemsInPullRequestRange(projectId, repoId, prIds);
|
|
1346
|
+
|
|
1347
|
+
// Assert
|
|
1348
|
+
expect(result).toHaveLength(1);
|
|
1349
|
+
expect(result[0].workItem.id).toBe(1);
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
describe('GitDataProvider - GetRepoTagsWithCommits', () => {
|
|
1354
|
+
let gitDataProvider: GitDataProvider;
|
|
1355
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1356
|
+
const mockToken = 'mock-token';
|
|
1357
|
+
|
|
1358
|
+
beforeEach(() => {
|
|
1359
|
+
jest.clearAllMocks();
|
|
1360
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it('should return empty array when response is null', async () => {
|
|
1364
|
+
// Arrange
|
|
1365
|
+
const repoApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1366
|
+
|
|
1367
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(null);
|
|
1368
|
+
|
|
1369
|
+
// Act
|
|
1370
|
+
const result = await gitDataProvider.GetRepoTagsWithCommits(repoApiUrl);
|
|
1371
|
+
|
|
1372
|
+
// Assert
|
|
1373
|
+
expect(result).toEqual([]);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
it('should return empty array when no tags exist', async () => {
|
|
1377
|
+
// Arrange
|
|
1378
|
+
const repoApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1379
|
+
const mockTagsResponse = { count: 0, value: [] };
|
|
1380
|
+
|
|
1381
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTagsResponse);
|
|
1382
|
+
|
|
1383
|
+
// Act
|
|
1384
|
+
const result = await gitDataProvider.GetRepoTagsWithCommits(repoApiUrl);
|
|
1385
|
+
|
|
1386
|
+
// Assert
|
|
1387
|
+
expect(result).toEqual([]);
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
describe('GitDataProvider - GetCommitBatch', () => {
|
|
1392
|
+
let gitDataProvider: GitDataProvider;
|
|
1393
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1394
|
+
const mockToken = 'mock-token';
|
|
1395
|
+
|
|
1396
|
+
beforeEach(() => {
|
|
1397
|
+
jest.clearAllMocks();
|
|
1398
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1399
|
+
// Mock postRequest
|
|
1400
|
+
(TFSServices as any).postRequest = jest.fn();
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it('should fetch commits in batches', async () => {
|
|
1404
|
+
// Arrange
|
|
1405
|
+
const gitUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1406
|
+
const itemVersion = { version: 'main', versionType: 'branch' };
|
|
1407
|
+
const compareVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
1408
|
+
|
|
1409
|
+
const mockCommitsResponse = {
|
|
1410
|
+
data: {
|
|
1411
|
+
count: 1,
|
|
1412
|
+
value: [
|
|
1413
|
+
{
|
|
1414
|
+
commitId: 'abc123',
|
|
1415
|
+
committer: { name: 'Test User', date: '2023-01-01T00:00:00Z' },
|
|
1416
|
+
comment: 'Test commit',
|
|
1417
|
+
},
|
|
1418
|
+
],
|
|
1419
|
+
},
|
|
1420
|
+
};
|
|
1421
|
+
const mockEmptyResponse = { data: { count: 0, value: [] } };
|
|
1422
|
+
|
|
1423
|
+
(TFSServices as any).postRequest
|
|
1424
|
+
.mockResolvedValueOnce(mockCommitsResponse)
|
|
1425
|
+
.mockResolvedValueOnce(mockEmptyResponse);
|
|
1426
|
+
|
|
1427
|
+
// Act
|
|
1428
|
+
const result = await gitDataProvider.GetCommitBatch(gitUrl, itemVersion, compareVersion);
|
|
1429
|
+
|
|
1430
|
+
// Assert
|
|
1431
|
+
expect(result).toHaveLength(1);
|
|
1432
|
+
expect(result[0].commit.commitId).toBe('abc123');
|
|
1433
|
+
expect(result[0].committerName).toBe('Test User');
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('should handle empty commits response', async () => {
|
|
1437
|
+
// Arrange
|
|
1438
|
+
const gitUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1439
|
+
const itemVersion = { version: 'main', versionType: 'branch' };
|
|
1440
|
+
const compareVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
1441
|
+
|
|
1442
|
+
const mockEmptyResponse = { data: { count: 0, value: [] } };
|
|
1443
|
+
|
|
1444
|
+
(TFSServices as any).postRequest.mockResolvedValueOnce(mockEmptyResponse);
|
|
1445
|
+
|
|
1446
|
+
// Act
|
|
1447
|
+
const result = await gitDataProvider.GetCommitBatch(gitUrl, itemVersion, compareVersion);
|
|
1448
|
+
|
|
1449
|
+
// Assert
|
|
1450
|
+
expect(result).toHaveLength(0);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('should include specificItemPath when provided', async () => {
|
|
1454
|
+
// Arrange
|
|
1455
|
+
const gitUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1456
|
+
const itemVersion = { version: 'main', versionType: 'branch' };
|
|
1457
|
+
const compareVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
1458
|
+
const specificItemPath = '/src/file.ts';
|
|
1459
|
+
|
|
1460
|
+
const mockEmptyResponse = { data: { count: 0, value: [] } };
|
|
1461
|
+
|
|
1462
|
+
(TFSServices as any).postRequest.mockResolvedValueOnce(mockEmptyResponse);
|
|
1463
|
+
|
|
1464
|
+
// Act
|
|
1465
|
+
await gitDataProvider.GetCommitBatch(gitUrl, itemVersion, compareVersion, specificItemPath);
|
|
1466
|
+
|
|
1467
|
+
// Assert
|
|
1468
|
+
expect((TFSServices as any).postRequest).toHaveBeenCalledWith(
|
|
1469
|
+
expect.any(String),
|
|
1470
|
+
mockToken,
|
|
1471
|
+
undefined,
|
|
1472
|
+
expect.objectContaining({
|
|
1473
|
+
itemPath: specificItemPath,
|
|
1474
|
+
historyMode: 'fullHistory',
|
|
1475
|
+
})
|
|
1476
|
+
);
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
describe('GitDataProvider - getSubmodulesData', () => {
|
|
1481
|
+
let gitDataProvider: GitDataProvider;
|
|
1482
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1483
|
+
const mockToken = 'mock-token';
|
|
1484
|
+
|
|
1485
|
+
beforeEach(() => {
|
|
1486
|
+
jest.clearAllMocks();
|
|
1487
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
it('should return empty array when no .gitmodules file exists', async () => {
|
|
1491
|
+
// Arrange
|
|
1492
|
+
const projectName = 'project-123';
|
|
1493
|
+
const repoId = 'repo-456';
|
|
1494
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
1495
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
1496
|
+
const allCommitsExtended: any[] = [];
|
|
1497
|
+
|
|
1498
|
+
// Mock GetFileFromGitRepo to return undefined (no .gitmodules)
|
|
1499
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
|
|
1500
|
+
|
|
1501
|
+
// Act
|
|
1502
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
1503
|
+
projectName,
|
|
1504
|
+
repoId,
|
|
1505
|
+
targetVersion,
|
|
1506
|
+
sourceVersion,
|
|
1507
|
+
allCommitsExtended
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
// Assert
|
|
1511
|
+
expect(result).toEqual([]);
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
describe('GitDataProvider - GetRepoTagsWithCommits (extended)', () => {
|
|
1516
|
+
let gitDataProvider: GitDataProvider;
|
|
1517
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1518
|
+
const mockToken = 'mock-token';
|
|
1519
|
+
|
|
1520
|
+
beforeEach(() => {
|
|
1521
|
+
jest.clearAllMocks();
|
|
1522
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
it('should fetch tags with commit dates', async () => {
|
|
1526
|
+
// Arrange
|
|
1527
|
+
const repoApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1528
|
+
const mockTagsResponse = {
|
|
1529
|
+
count: 1,
|
|
1530
|
+
value: [{ name: 'refs/tags/v1.0.0', objectId: 'abc123', peeledObjectId: 'def456' }],
|
|
1531
|
+
};
|
|
1532
|
+
const mockCommitResponse = { committer: { date: '2023-01-01' } };
|
|
1533
|
+
|
|
1534
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1535
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1536
|
+
.mockResolvedValueOnce(mockCommitResponse);
|
|
1537
|
+
|
|
1538
|
+
// Act
|
|
1539
|
+
const result = await gitDataProvider.GetRepoTagsWithCommits(repoApiUrl);
|
|
1540
|
+
|
|
1541
|
+
// Assert
|
|
1542
|
+
expect(result).toHaveLength(1);
|
|
1543
|
+
expect(result[0].name).toBe('v1.0.0');
|
|
1544
|
+
expect(result[0].commitId).toBe('def456');
|
|
1545
|
+
expect(result[0].date).toBe('2023-01-01');
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
it('should skip tags without commitId', async () => {
|
|
1549
|
+
// Arrange
|
|
1550
|
+
const repoApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1551
|
+
const mockTagsResponse = {
|
|
1552
|
+
count: 2,
|
|
1553
|
+
value: [
|
|
1554
|
+
{ name: 'refs/tags/v1.0.0' }, // No objectId or peeledObjectId
|
|
1555
|
+
{ name: 'refs/tags/v2.0.0', objectId: 'abc123' },
|
|
1556
|
+
],
|
|
1557
|
+
};
|
|
1558
|
+
const mockCommitResponse = { committer: { date: '2023-01-01' } };
|
|
1559
|
+
|
|
1560
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1561
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1562
|
+
.mockResolvedValueOnce(mockCommitResponse);
|
|
1563
|
+
|
|
1564
|
+
// Act
|
|
1565
|
+
const result = await gitDataProvider.GetRepoTagsWithCommits(repoApiUrl);
|
|
1566
|
+
|
|
1567
|
+
// Assert
|
|
1568
|
+
expect(result).toHaveLength(1);
|
|
1569
|
+
expect(result[0].name).toBe('v2.0.0');
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
it('should handle commit fetch failure gracefully', async () => {
|
|
1573
|
+
// Arrange
|
|
1574
|
+
const repoApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1575
|
+
const mockTagsResponse = {
|
|
1576
|
+
count: 1,
|
|
1577
|
+
value: [{ name: 'refs/tags/v1.0.0', objectId: 'abc123' }],
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1581
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1582
|
+
.mockRejectedValueOnce(new Error('Commit not found'));
|
|
1583
|
+
|
|
1584
|
+
// Act
|
|
1585
|
+
const result = await gitDataProvider.GetRepoTagsWithCommits(repoApiUrl);
|
|
1586
|
+
|
|
1587
|
+
// Assert
|
|
1588
|
+
expect(result).toHaveLength(1);
|
|
1589
|
+
expect(result[0].date).toBeUndefined();
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
it('should use author date when committer date is missing', async () => {
|
|
1593
|
+
// Arrange
|
|
1594
|
+
const repoApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
1595
|
+
const mockTagsResponse = {
|
|
1596
|
+
count: 1,
|
|
1597
|
+
value: [{ name: 'refs/tags/v1.0.0', objectId: 'abc123' }],
|
|
1598
|
+
};
|
|
1599
|
+
const mockCommitResponse = { author: { date: '2023-02-01' } }; // No committer
|
|
1600
|
+
|
|
1601
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1602
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1603
|
+
.mockResolvedValueOnce(mockCommitResponse);
|
|
1604
|
+
|
|
1605
|
+
// Act
|
|
1606
|
+
const result = await gitDataProvider.GetRepoTagsWithCommits(repoApiUrl);
|
|
1607
|
+
|
|
1608
|
+
// Assert
|
|
1609
|
+
expect(result[0].date).toBe('2023-02-01');
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
describe('GitDataProvider - createLinkedRelatedItemsForSVD (via GetItemsInCommitRange)', () => {
|
|
1614
|
+
let gitDataProvider: GitDataProvider;
|
|
1615
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1616
|
+
const mockToken = 'mock-token';
|
|
1617
|
+
|
|
1618
|
+
beforeEach(() => {
|
|
1619
|
+
jest.clearAllMocks();
|
|
1620
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
it('should create linked items for requirements with Affects relationship', async () => {
|
|
1624
|
+
// Arrange
|
|
1625
|
+
const projectId = 'project-123';
|
|
1626
|
+
const repoId = 'repo-456';
|
|
1627
|
+
const commitRange = {
|
|
1628
|
+
value: [
|
|
1629
|
+
{
|
|
1630
|
+
commitId: 'commit-1',
|
|
1631
|
+
workItems: [{ id: 1 }],
|
|
1632
|
+
},
|
|
1633
|
+
],
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
const linkedWiOptions = {
|
|
1637
|
+
isEnabled: true,
|
|
1638
|
+
linkedWiTypes: 'reqOnly',
|
|
1639
|
+
linkedWiRelationship: 'affectsOnly',
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
const mockPopulatedWI = {
|
|
1643
|
+
id: 1,
|
|
1644
|
+
fields: { 'System.Title': 'Test WI' },
|
|
1645
|
+
relations: [
|
|
1646
|
+
{
|
|
1647
|
+
url: 'https://example.com/workItems/2',
|
|
1648
|
+
rel: 'System.LinkTypes.Affects-Forward',
|
|
1649
|
+
attributes: { name: 'Affects' },
|
|
1650
|
+
},
|
|
1651
|
+
],
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
const mockRelatedWI = {
|
|
1655
|
+
id: 2,
|
|
1656
|
+
fields: {
|
|
1657
|
+
'System.WorkItemType': 'Requirement',
|
|
1658
|
+
'System.Title': 'Related Requirement',
|
|
1659
|
+
},
|
|
1660
|
+
_links: { html: { href: 'https://example.com/wi/2' } },
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1664
|
+
|
|
1665
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1666
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1667
|
+
.mockResolvedValueOnce(mockRelatedWI)
|
|
1668
|
+
.mockResolvedValueOnce(mockPRsResponse);
|
|
1669
|
+
|
|
1670
|
+
// Act
|
|
1671
|
+
const result = await gitDataProvider.GetItemsInCommitRange(
|
|
1672
|
+
projectId,
|
|
1673
|
+
repoId,
|
|
1674
|
+
commitRange,
|
|
1675
|
+
linkedWiOptions,
|
|
1676
|
+
false
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1679
|
+
// Assert
|
|
1680
|
+
expect(result.commitChangesArray).toHaveLength(1);
|
|
1681
|
+
expect(result.commitChangesArray[0].linkedItems).toBeDefined();
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
it('should filter out non-workItem relations', async () => {
|
|
1685
|
+
// Arrange
|
|
1686
|
+
const projectId = 'project-123';
|
|
1687
|
+
const repoId = 'repo-456';
|
|
1688
|
+
const commitRange = {
|
|
1689
|
+
value: [
|
|
1690
|
+
{
|
|
1691
|
+
commitId: 'commit-1',
|
|
1692
|
+
workItems: [{ id: 1 }],
|
|
1693
|
+
},
|
|
1694
|
+
],
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
const linkedWiOptions = {
|
|
1698
|
+
isEnabled: true,
|
|
1699
|
+
linkedWiTypes: 'both',
|
|
1700
|
+
linkedWiRelationship: 'both',
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
const mockPopulatedWI = {
|
|
1704
|
+
id: 1,
|
|
1705
|
+
fields: { 'System.Title': 'Test WI' },
|
|
1706
|
+
relations: [
|
|
1707
|
+
{
|
|
1708
|
+
url: 'https://example.com/attachments/file.txt', // Not a workItem
|
|
1709
|
+
rel: 'AttachedFile',
|
|
1710
|
+
attributes: {},
|
|
1711
|
+
},
|
|
1712
|
+
],
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1716
|
+
|
|
1717
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1718
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1719
|
+
.mockResolvedValueOnce(mockPRsResponse);
|
|
1720
|
+
|
|
1721
|
+
// Act
|
|
1722
|
+
const result = await gitDataProvider.GetItemsInCommitRange(
|
|
1723
|
+
projectId,
|
|
1724
|
+
repoId,
|
|
1725
|
+
commitRange,
|
|
1726
|
+
linkedWiOptions,
|
|
1727
|
+
false
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
// Assert
|
|
1731
|
+
expect(result.commitChangesArray).toHaveLength(1);
|
|
1732
|
+
expect(result.commitChangesArray[0].linkedItems).toEqual([]);
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
it('should handle linkedWiTypes=none', async () => {
|
|
1736
|
+
// Arrange
|
|
1737
|
+
const projectId = 'project-123';
|
|
1738
|
+
const repoId = 'repo-456';
|
|
1739
|
+
const commitRange = {
|
|
1740
|
+
value: [
|
|
1741
|
+
{
|
|
1742
|
+
commitId: 'commit-1',
|
|
1743
|
+
workItems: [{ id: 1 }],
|
|
1744
|
+
},
|
|
1745
|
+
],
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
const linkedWiOptions = {
|
|
1749
|
+
isEnabled: true,
|
|
1750
|
+
linkedWiTypes: 'none',
|
|
1751
|
+
linkedWiRelationship: 'both',
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
const mockPopulatedWI = {
|
|
1755
|
+
id: 1,
|
|
1756
|
+
fields: { 'System.Title': 'Test WI' },
|
|
1757
|
+
relations: [
|
|
1758
|
+
{
|
|
1759
|
+
url: 'https://example.com/workItems/2',
|
|
1760
|
+
rel: 'System.LinkTypes.Affects-Forward',
|
|
1761
|
+
attributes: { name: 'Affects' },
|
|
1762
|
+
},
|
|
1763
|
+
],
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1767
|
+
|
|
1768
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1769
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1770
|
+
.mockResolvedValueOnce(mockPRsResponse);
|
|
1771
|
+
|
|
1772
|
+
// Act
|
|
1773
|
+
const result = await gitDataProvider.GetItemsInCommitRange(
|
|
1774
|
+
projectId,
|
|
1775
|
+
repoId,
|
|
1776
|
+
commitRange,
|
|
1777
|
+
linkedWiOptions,
|
|
1778
|
+
false
|
|
1779
|
+
);
|
|
1780
|
+
|
|
1781
|
+
// Assert
|
|
1782
|
+
expect(result.commitChangesArray[0].linkedItems).toEqual([]);
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
it('should handle Feature type with featureOnly option', async () => {
|
|
1786
|
+
// Arrange
|
|
1787
|
+
const projectId = 'project-123';
|
|
1788
|
+
const repoId = 'repo-456';
|
|
1789
|
+
const commitRange = {
|
|
1790
|
+
value: [
|
|
1791
|
+
{
|
|
1792
|
+
commitId: 'commit-1',
|
|
1793
|
+
workItems: [{ id: 1 }],
|
|
1794
|
+
},
|
|
1795
|
+
],
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
const linkedWiOptions = {
|
|
1799
|
+
isEnabled: true,
|
|
1800
|
+
linkedWiTypes: 'featureOnly',
|
|
1801
|
+
linkedWiRelationship: 'coversOnly',
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
const mockPopulatedWI = {
|
|
1805
|
+
id: 1,
|
|
1806
|
+
fields: { 'System.Title': 'Test WI' },
|
|
1807
|
+
relations: [
|
|
1808
|
+
{
|
|
1809
|
+
url: 'https://example.com/workItems/2',
|
|
1810
|
+
rel: 'System.LinkTypes.CoveredBy-Forward',
|
|
1811
|
+
attributes: { name: 'CoveredBy' },
|
|
1812
|
+
},
|
|
1813
|
+
],
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
const mockRelatedWI = {
|
|
1817
|
+
id: 2,
|
|
1818
|
+
fields: {
|
|
1819
|
+
'System.WorkItemType': 'Feature',
|
|
1820
|
+
'System.Title': 'Related Feature',
|
|
1821
|
+
},
|
|
1822
|
+
_links: { html: { href: 'https://example.com/wi/2' } },
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1826
|
+
|
|
1827
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1828
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1829
|
+
.mockResolvedValueOnce(mockRelatedWI)
|
|
1830
|
+
.mockResolvedValueOnce(mockPRsResponse);
|
|
1831
|
+
|
|
1832
|
+
// Act
|
|
1833
|
+
const result = await gitDataProvider.GetItemsInCommitRange(
|
|
1834
|
+
projectId,
|
|
1835
|
+
repoId,
|
|
1836
|
+
commitRange,
|
|
1837
|
+
linkedWiOptions,
|
|
1838
|
+
false
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
// Assert
|
|
1842
|
+
expect(result.commitChangesArray).toHaveLength(1);
|
|
1843
|
+
});
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
describe('GitDataProvider - GetRepoReferences (extended)', () => {
|
|
1847
|
+
let gitDataProvider: GitDataProvider;
|
|
1848
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1849
|
+
const mockToken = 'mock-token';
|
|
1850
|
+
|
|
1851
|
+
beforeEach(() => {
|
|
1852
|
+
jest.clearAllMocks();
|
|
1853
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
it('should sort tags by commit date (most recent first)', async () => {
|
|
1857
|
+
// Arrange
|
|
1858
|
+
const projectId = 'project-123';
|
|
1859
|
+
const repoId = 'repo-456';
|
|
1860
|
+
const mockTagsResponse = {
|
|
1861
|
+
count: 2,
|
|
1862
|
+
value: [
|
|
1863
|
+
{ name: 'refs/tags/v1.0.0', objectId: 'abc123' },
|
|
1864
|
+
{ name: 'refs/tags/v2.0.0', objectId: 'def456', peeledObjectId: 'ghi789' },
|
|
1865
|
+
],
|
|
1866
|
+
};
|
|
1867
|
+
const mockCommit1 = { committer: { date: '2023-01-01' } };
|
|
1868
|
+
const mockCommit2 = { committer: { date: '2023-06-01' } };
|
|
1869
|
+
|
|
1870
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1871
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1872
|
+
.mockResolvedValueOnce(mockCommit1)
|
|
1873
|
+
.mockResolvedValueOnce(mockCommit2);
|
|
1874
|
+
|
|
1875
|
+
// Act
|
|
1876
|
+
const result = await gitDataProvider.GetRepoReferences(projectId, repoId, 'tag');
|
|
1877
|
+
|
|
1878
|
+
// Assert
|
|
1879
|
+
expect(result).toHaveLength(2);
|
|
1880
|
+
// v2.0.0 should be first (more recent)
|
|
1881
|
+
expect(result[0].name).toBe('v2.0.0');
|
|
1882
|
+
expect(result[1].name).toBe('v1.0.0');
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
it('should sort branches by commit date', async () => {
|
|
1886
|
+
// Arrange
|
|
1887
|
+
const projectId = 'project-123';
|
|
1888
|
+
const repoId = 'repo-456';
|
|
1889
|
+
const mockBranchesResponse = {
|
|
1890
|
+
count: 2,
|
|
1891
|
+
value: [
|
|
1892
|
+
{ name: 'refs/heads/main', objectId: 'abc123' },
|
|
1893
|
+
{ name: 'refs/heads/develop', objectId: 'def456' },
|
|
1894
|
+
],
|
|
1895
|
+
};
|
|
1896
|
+
const mockCommit1 = { committer: { date: '2023-01-01' } };
|
|
1897
|
+
const mockCommit2 = { committer: { date: '2023-06-01' } };
|
|
1898
|
+
|
|
1899
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1900
|
+
.mockResolvedValueOnce(mockBranchesResponse)
|
|
1901
|
+
.mockResolvedValueOnce(mockCommit1)
|
|
1902
|
+
.mockResolvedValueOnce(mockCommit2);
|
|
1903
|
+
|
|
1904
|
+
// Act
|
|
1905
|
+
const result = await gitDataProvider.GetRepoReferences(projectId, repoId, 'branch');
|
|
1906
|
+
|
|
1907
|
+
// Assert
|
|
1908
|
+
expect(result).toHaveLength(2);
|
|
1909
|
+
// develop should be first (more recent)
|
|
1910
|
+
expect(result[0].name).toBe('develop');
|
|
1911
|
+
expect(result[1].name).toBe('main');
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
it('should handle commit resolution failure in tag sorting', async () => {
|
|
1915
|
+
// Arrange
|
|
1916
|
+
const projectId = 'project-123';
|
|
1917
|
+
const repoId = 'repo-456';
|
|
1918
|
+
const mockTagsResponse = {
|
|
1919
|
+
count: 1,
|
|
1920
|
+
value: [{ name: 'refs/tags/v1.0.0', objectId: 'abc123' }],
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1924
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1925
|
+
.mockRejectedValueOnce(new Error('Commit not found'));
|
|
1926
|
+
|
|
1927
|
+
// Act
|
|
1928
|
+
const result = await gitDataProvider.GetRepoReferences(projectId, repoId, 'tag');
|
|
1929
|
+
|
|
1930
|
+
// Assert
|
|
1931
|
+
expect(result).toHaveLength(1);
|
|
1932
|
+
expect(result[0].date).toBeUndefined();
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
it('should set date undefined when tag commit has no committer/author date', async () => {
|
|
1936
|
+
const projectId = 'project-123';
|
|
1937
|
+
const repoId = 'repo-456';
|
|
1938
|
+
const mockTagsResponse = {
|
|
1939
|
+
count: 1,
|
|
1940
|
+
value: [{ name: 'refs/tags/v0.0.1', objectId: 'abc123' }],
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1944
|
+
.mockResolvedValueOnce(mockTagsResponse)
|
|
1945
|
+
.mockResolvedValueOnce({});
|
|
1946
|
+
|
|
1947
|
+
const result = await gitDataProvider.GetRepoReferences(projectId, repoId, 'tag');
|
|
1948
|
+
|
|
1949
|
+
expect(result).toHaveLength(1);
|
|
1950
|
+
expect(result[0]).toEqual(expect.objectContaining({ name: 'v0.0.1', date: undefined }));
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
it('should set date undefined when branch commit cannot be resolved to a date', async () => {
|
|
1954
|
+
const projectId = 'project-123';
|
|
1955
|
+
const repoId = 'repo-456';
|
|
1956
|
+
const mockBranchesResponse = {
|
|
1957
|
+
count: 1,
|
|
1958
|
+
value: [{ name: 'refs/heads/feature', objectId: 'abc123' }],
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1962
|
+
.mockResolvedValueOnce(mockBranchesResponse)
|
|
1963
|
+
.mockResolvedValueOnce(null);
|
|
1964
|
+
|
|
1965
|
+
const result = await gitDataProvider.GetRepoReferences(projectId, repoId, 'branch');
|
|
1966
|
+
|
|
1967
|
+
expect(result).toHaveLength(1);
|
|
1968
|
+
expect(result[0]).toEqual(expect.objectContaining({ name: 'feature', date: undefined }));
|
|
1969
|
+
});
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
describe('GitDataProvider - duplicate work item removal', () => {
|
|
1973
|
+
let gitDataProvider: GitDataProvider;
|
|
1974
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
1975
|
+
const mockToken = 'mock-token';
|
|
1976
|
+
|
|
1977
|
+
beforeEach(() => {
|
|
1978
|
+
jest.clearAllMocks();
|
|
1979
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
it('should remove duplicate work items in GetItemsInCommitRange', async () => {
|
|
1983
|
+
// Arrange
|
|
1984
|
+
const projectId = 'project-123';
|
|
1985
|
+
const repoId = 'repo-456';
|
|
1986
|
+
const commitRange = {
|
|
1987
|
+
value: [
|
|
1988
|
+
{ commitId: 'commit-1', workItems: [{ id: 1 }] },
|
|
1989
|
+
{ commitId: 'commit-2', workItems: [{ id: 1 }] },
|
|
1990
|
+
],
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
const mockPopulatedWI = { id: 1, fields: { 'System.Title': 'Test WI' } };
|
|
1994
|
+
const mockPRsResponse = { count: 0, value: [] };
|
|
1995
|
+
|
|
1996
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
1997
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1998
|
+
.mockResolvedValueOnce(mockPopulatedWI)
|
|
1999
|
+
.mockResolvedValueOnce(mockPRsResponse);
|
|
2000
|
+
|
|
2001
|
+
// Act
|
|
2002
|
+
const result = await gitDataProvider.GetItemsInCommitRange(projectId, repoId, commitRange, null, false);
|
|
2003
|
+
|
|
2004
|
+
// Assert
|
|
2005
|
+
expect(result.commitChangesArray.length).toBeLessThanOrEqual(2);
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
describe('GitDataProvider - version encoding edge cases', () => {
|
|
2010
|
+
let gitDataProvider: GitDataProvider;
|
|
2011
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
2012
|
+
const mockToken = 'mock-token';
|
|
2013
|
+
|
|
2014
|
+
beforeEach(() => {
|
|
2015
|
+
jest.clearAllMocks();
|
|
2016
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
it('should handle version with null gracefully in GetFileFromGitRepo', async () => {
|
|
2020
|
+
// Arrange
|
|
2021
|
+
const projectName = 'project-123';
|
|
2022
|
+
const repoId = 'repo-456';
|
|
2023
|
+
const fileName = 'test.txt';
|
|
2024
|
+
const version = { version: null as any, versionType: 'branch' };
|
|
2025
|
+
|
|
2026
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({ content: 'file content' });
|
|
2027
|
+
|
|
2028
|
+
// Act
|
|
2029
|
+
const result = await gitDataProvider.GetFileFromGitRepo(projectName, repoId, fileName, version);
|
|
2030
|
+
|
|
2031
|
+
// Assert
|
|
2032
|
+
expect(result).toBe('file content');
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
it('should handle version with null gracefully in CheckIfItemExist', async () => {
|
|
2036
|
+
// Arrange
|
|
2037
|
+
const gitApiUrl = 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id';
|
|
2038
|
+
const itemPath = 'path/to/file.txt';
|
|
2039
|
+
const version = { version: null as any, versionType: 'branch' };
|
|
2040
|
+
|
|
2041
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({ path: itemPath });
|
|
2042
|
+
|
|
2043
|
+
// Act
|
|
2044
|
+
const result = await gitDataProvider.CheckIfItemExist(gitApiUrl, itemPath, version);
|
|
2045
|
+
|
|
2046
|
+
// Assert
|
|
2047
|
+
expect(result).toBe(true);
|
|
2048
|
+
});
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
describe('GitDataProvider - GetCommitsForRepo edge cases', () => {
|
|
2052
|
+
let gitDataProvider: GitDataProvider;
|
|
2053
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
2054
|
+
const mockToken = 'mock-token';
|
|
2055
|
+
|
|
2056
|
+
beforeEach(() => {
|
|
2057
|
+
jest.clearAllMocks();
|
|
2058
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
it('should handle commits without committer date', async () => {
|
|
2062
|
+
// Arrange
|
|
2063
|
+
const projectName = 'project-123';
|
|
2064
|
+
const repoId = 'repo-456';
|
|
2065
|
+
const mockResponse = {
|
|
2066
|
+
count: 1,
|
|
2067
|
+
value: [{ commitId: 'abc123', comment: 'Test commit' }],
|
|
2068
|
+
};
|
|
2069
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
2070
|
+
|
|
2071
|
+
// Act
|
|
2072
|
+
const result = await gitDataProvider.GetCommitsForRepo(projectName, repoId);
|
|
2073
|
+
|
|
2074
|
+
// Assert
|
|
2075
|
+
expect(result).toHaveLength(1);
|
|
2076
|
+
expect(result[0].date).toBeUndefined();
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
it('should fetch commits without version identifier', async () => {
|
|
2080
|
+
// Arrange
|
|
2081
|
+
const projectName = 'project-123';
|
|
2082
|
+
const repoId = 'repo-456';
|
|
2083
|
+
const mockResponse = {
|
|
2084
|
+
count: 1,
|
|
2085
|
+
value: [{ commitId: 'abc123', comment: 'Test', committer: { date: '2023-01-01' } }],
|
|
2086
|
+
};
|
|
2087
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
2088
|
+
|
|
2089
|
+
// Act
|
|
2090
|
+
const result = await gitDataProvider.GetCommitsForRepo(projectName, repoId, '');
|
|
2091
|
+
|
|
2092
|
+
// Assert
|
|
2093
|
+
expect(result).toHaveLength(1);
|
|
2094
|
+
});
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
describe('GitDataProvider - GetItemsInPullRequestRange edge cases', () => {
|
|
2098
|
+
let gitDataProvider: GitDataProvider;
|
|
2099
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
2100
|
+
const mockToken = 'mock-token';
|
|
2101
|
+
|
|
2102
|
+
beforeEach(() => {
|
|
2103
|
+
jest.clearAllMocks();
|
|
2104
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
it('should handle PR without workItems link', async () => {
|
|
2108
|
+
// Arrange
|
|
2109
|
+
const projectId = 'project-123';
|
|
2110
|
+
const repoId = 'repo-456';
|
|
2111
|
+
const prIds = [101];
|
|
2112
|
+
|
|
2113
|
+
const mockPRsResponse = {
|
|
2114
|
+
count: 1,
|
|
2115
|
+
value: [{ pullRequestId: 101, _links: {} }],
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockPRsResponse);
|
|
2119
|
+
|
|
2120
|
+
// Act
|
|
2121
|
+
const result = await gitDataProvider.GetItemsInPullRequestRange(projectId, repoId, prIds);
|
|
2122
|
+
|
|
2123
|
+
// Assert
|
|
2124
|
+
expect(result).toHaveLength(0);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
it('should handle errors when fetching work items', async () => {
|
|
2128
|
+
// Arrange
|
|
2129
|
+
const projectId = 'project-123';
|
|
2130
|
+
const repoId = 'repo-456';
|
|
2131
|
+
const prIds = [101];
|
|
2132
|
+
|
|
2133
|
+
const mockPRsResponse = {
|
|
2134
|
+
count: 1,
|
|
2135
|
+
value: [{ pullRequestId: 101, _links: { workItems: { href: 'https://example.com/wi' } } }],
|
|
2136
|
+
};
|
|
2137
|
+
|
|
2138
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2139
|
+
.mockResolvedValueOnce(mockPRsResponse)
|
|
2140
|
+
.mockRejectedValueOnce(new Error('Failed to fetch'));
|
|
2141
|
+
|
|
2142
|
+
// Act
|
|
2143
|
+
const result = await gitDataProvider.GetItemsInPullRequestRange(projectId, repoId, prIds);
|
|
2144
|
+
|
|
2145
|
+
// Assert
|
|
2146
|
+
expect(result).toHaveLength(0);
|
|
2147
|
+
expect(logger.error).toHaveBeenCalled();
|
|
2148
|
+
});
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
describe('GitDataProvider - getSubmodulesData', () => {
|
|
2152
|
+
let gitDataProvider: GitDataProvider;
|
|
2153
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
2154
|
+
const mockToken = 'mock-token';
|
|
2155
|
+
|
|
2156
|
+
beforeEach(() => {
|
|
2157
|
+
jest.clearAllMocks();
|
|
2158
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
it('should parse .gitmodules file and return submodule data', async () => {
|
|
2162
|
+
// Arrange
|
|
2163
|
+
const projectName = 'project-123';
|
|
2164
|
+
const repoId = 'repo-456';
|
|
2165
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2166
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2167
|
+
const allCommitsExtended: any[] = [];
|
|
2168
|
+
|
|
2169
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2170
|
+
path = libs/common
|
|
2171
|
+
url = https://dev.azure.com/org/project/_git/common-lib`;
|
|
2172
|
+
|
|
2173
|
+
// Mock GetFileFromGitRepo calls
|
|
2174
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2175
|
+
.mockResolvedValueOnce({ content: gitModulesContent }) // .gitmodules file
|
|
2176
|
+
.mockResolvedValueOnce({ content: 'target-sha-123' }) // target SHA
|
|
2177
|
+
.mockResolvedValueOnce({ content: 'source-sha-456' }); // source SHA
|
|
2178
|
+
|
|
2179
|
+
// Act
|
|
2180
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2181
|
+
projectName,
|
|
2182
|
+
repoId,
|
|
2183
|
+
targetVersion,
|
|
2184
|
+
sourceVersion,
|
|
2185
|
+
allCommitsExtended
|
|
2186
|
+
);
|
|
2187
|
+
|
|
2188
|
+
// Assert
|
|
2189
|
+
expect(result).toHaveLength(1);
|
|
2190
|
+
expect(result[0].gitSubModuleName).toBe('libs_common');
|
|
2191
|
+
expect(result[0].targetSha1).toBe('target-sha-123');
|
|
2192
|
+
expect(result[0].sourceSha1).toBe('source-sha-456');
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
it('should handle .gitmodules with CRLF line endings', async () => {
|
|
2196
|
+
// Arrange
|
|
2197
|
+
const projectName = 'project-123';
|
|
2198
|
+
const repoId = 'repo-456';
|
|
2199
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2200
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2201
|
+
const allCommitsExtended: any[] = [];
|
|
2202
|
+
|
|
2203
|
+
const gitModulesContent = `[submodule "libs/common"]\r\n\tpath = libs/common\r\n\turl = https://example.com/repo`;
|
|
2204
|
+
|
|
2205
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2206
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2207
|
+
.mockResolvedValueOnce({ content: 'target-sha' })
|
|
2208
|
+
.mockResolvedValueOnce({ content: 'source-sha' });
|
|
2209
|
+
|
|
2210
|
+
// Act
|
|
2211
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2212
|
+
projectName,
|
|
2213
|
+
repoId,
|
|
2214
|
+
targetVersion,
|
|
2215
|
+
sourceVersion,
|
|
2216
|
+
allCommitsExtended
|
|
2217
|
+
);
|
|
2218
|
+
|
|
2219
|
+
// Assert
|
|
2220
|
+
expect(result).toHaveLength(1);
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
it('should handle relative URL paths in submodules', async () => {
|
|
2224
|
+
// Arrange
|
|
2225
|
+
const projectName = 'project-123';
|
|
2226
|
+
const repoId = 'repo-456';
|
|
2227
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2228
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2229
|
+
const allCommitsExtended: any[] = [];
|
|
2230
|
+
|
|
2231
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2232
|
+
path = libs/common
|
|
2233
|
+
url = ../common-lib`;
|
|
2234
|
+
|
|
2235
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2236
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2237
|
+
.mockResolvedValueOnce({ content: 'target-sha' })
|
|
2238
|
+
.mockResolvedValueOnce({ content: 'source-sha' });
|
|
2239
|
+
|
|
2240
|
+
// Act
|
|
2241
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2242
|
+
projectName,
|
|
2243
|
+
repoId,
|
|
2244
|
+
targetVersion,
|
|
2245
|
+
sourceVersion,
|
|
2246
|
+
allCommitsExtended
|
|
2247
|
+
);
|
|
2248
|
+
|
|
2249
|
+
// Assert
|
|
2250
|
+
expect(result).toHaveLength(1);
|
|
2251
|
+
expect(result[0].gitSubRepoUrl).toContain('common-lib');
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
it('should skip submodule when source SHA not found', async () => {
|
|
2255
|
+
// Arrange
|
|
2256
|
+
const projectName = 'project-123';
|
|
2257
|
+
const repoId = 'repo-456';
|
|
2258
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2259
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2260
|
+
const allCommitsExtended: any[] = [];
|
|
2261
|
+
|
|
2262
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2263
|
+
path = libs/common
|
|
2264
|
+
url = https://example.com/repo`;
|
|
2265
|
+
|
|
2266
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2267
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2268
|
+
.mockResolvedValueOnce({ content: 'target-sha' })
|
|
2269
|
+
.mockResolvedValueOnce({ content: undefined }); // source not found
|
|
2270
|
+
|
|
2271
|
+
// Act
|
|
2272
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2273
|
+
projectName,
|
|
2274
|
+
repoId,
|
|
2275
|
+
targetVersion,
|
|
2276
|
+
sourceVersion,
|
|
2277
|
+
allCommitsExtended
|
|
2278
|
+
);
|
|
2279
|
+
|
|
2280
|
+
// Assert
|
|
2281
|
+
expect(result).toHaveLength(0);
|
|
2282
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
it('should skip submodule when target SHA not found', async () => {
|
|
2286
|
+
// Arrange
|
|
2287
|
+
const projectName = 'project-123';
|
|
2288
|
+
const repoId = 'repo-456';
|
|
2289
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2290
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2291
|
+
const allCommitsExtended: any[] = [];
|
|
2292
|
+
|
|
2293
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2294
|
+
path = libs/common
|
|
2295
|
+
url = https://example.com/repo`;
|
|
2296
|
+
|
|
2297
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2298
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2299
|
+
.mockResolvedValueOnce({ content: undefined }) // target not found
|
|
2300
|
+
.mockResolvedValueOnce({ content: 'source-sha' });
|
|
2301
|
+
|
|
2302
|
+
// Act
|
|
2303
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2304
|
+
projectName,
|
|
2305
|
+
repoId,
|
|
2306
|
+
targetVersion,
|
|
2307
|
+
sourceVersion,
|
|
2308
|
+
allCommitsExtended
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
// Assert
|
|
2312
|
+
expect(result).toHaveLength(0);
|
|
2313
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
it('should skip submodule when source and target SHA are the same', async () => {
|
|
2317
|
+
// Arrange
|
|
2318
|
+
const projectName = 'project-123';
|
|
2319
|
+
const repoId = 'repo-456';
|
|
2320
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2321
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2322
|
+
const allCommitsExtended: any[] = [];
|
|
2323
|
+
|
|
2324
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2325
|
+
path = libs/common
|
|
2326
|
+
url = https://example.com/repo`;
|
|
2327
|
+
|
|
2328
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2329
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2330
|
+
.mockResolvedValueOnce({ content: 'same-sha' })
|
|
2331
|
+
.mockResolvedValueOnce({ content: 'same-sha' });
|
|
2332
|
+
|
|
2333
|
+
// Act
|
|
2334
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2335
|
+
projectName,
|
|
2336
|
+
repoId,
|
|
2337
|
+
targetVersion,
|
|
2338
|
+
sourceVersion,
|
|
2339
|
+
allCommitsExtended
|
|
2340
|
+
);
|
|
2341
|
+
|
|
2342
|
+
// Assert
|
|
2343
|
+
expect(result).toHaveLength(0);
|
|
2344
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2347
|
+
it('should search commits for source SHA when not found initially', async () => {
|
|
2348
|
+
// Arrange
|
|
2349
|
+
const projectName = 'project-123';
|
|
2350
|
+
const repoId = 'repo-456';
|
|
2351
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2352
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2353
|
+
const allCommitsExtended = [{ commit: { commitId: 'commit-1' } }, { commit: { commitId: 'commit-2' } }];
|
|
2354
|
+
|
|
2355
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2356
|
+
path = libs/common
|
|
2357
|
+
url = https://example.com/repo`;
|
|
2358
|
+
|
|
2359
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2360
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2361
|
+
.mockResolvedValueOnce({ content: 'target-sha' })
|
|
2362
|
+
.mockResolvedValueOnce({ content: undefined }) // source not found initially
|
|
2363
|
+
.mockResolvedValueOnce({ content: 'source-sha-from-commit' }); // found in commit search
|
|
2364
|
+
|
|
2365
|
+
// Act
|
|
2366
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2367
|
+
projectName,
|
|
2368
|
+
repoId,
|
|
2369
|
+
targetVersion,
|
|
2370
|
+
sourceVersion,
|
|
2371
|
+
allCommitsExtended
|
|
2372
|
+
);
|
|
2373
|
+
|
|
2374
|
+
// Assert
|
|
2375
|
+
expect(result).toHaveLength(1);
|
|
2376
|
+
expect(result[0].sourceSha1).toBe('source-sha-from-commit');
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
it('should handle commits with direct commitId property', async () => {
|
|
2380
|
+
// Arrange
|
|
2381
|
+
const projectName = 'project-123';
|
|
2382
|
+
const repoId = 'repo-456';
|
|
2383
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2384
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2385
|
+
const allCommitsExtended = [{ commitId: 'direct-commit-1' }, { commitId: 'direct-commit-2' }];
|
|
2386
|
+
|
|
2387
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2388
|
+
path = libs/common
|
|
2389
|
+
url = https://example.com/repo`;
|
|
2390
|
+
|
|
2391
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2392
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2393
|
+
.mockResolvedValueOnce({ content: 'target-sha' })
|
|
2394
|
+
.mockResolvedValueOnce({ content: undefined })
|
|
2395
|
+
.mockResolvedValueOnce({ content: 'source-sha' });
|
|
2396
|
+
|
|
2397
|
+
// Act
|
|
2398
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2399
|
+
projectName,
|
|
2400
|
+
repoId,
|
|
2401
|
+
targetVersion,
|
|
2402
|
+
sourceVersion,
|
|
2403
|
+
allCommitsExtended
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
// Assert
|
|
2407
|
+
expect(result).toHaveLength(1);
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
it('should warn when commit not found in extended commits', async () => {
|
|
2411
|
+
// Arrange
|
|
2412
|
+
const projectName = 'project-123';
|
|
2413
|
+
const repoId = 'repo-456';
|
|
2414
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2415
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2416
|
+
const allCommitsExtended = [
|
|
2417
|
+
{ noCommitId: true }, // No commitId or commit property
|
|
2418
|
+
{ noCommitId: true },
|
|
2419
|
+
];
|
|
2420
|
+
|
|
2421
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2422
|
+
path = libs/common
|
|
2423
|
+
url = https://example.com/repo`;
|
|
2424
|
+
|
|
2425
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2426
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2427
|
+
.mockResolvedValueOnce({ content: 'target-sha' })
|
|
2428
|
+
.mockResolvedValueOnce({ content: undefined });
|
|
2429
|
+
|
|
2430
|
+
// Act
|
|
2431
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2432
|
+
projectName,
|
|
2433
|
+
repoId,
|
|
2434
|
+
targetVersion,
|
|
2435
|
+
sourceVersion,
|
|
2436
|
+
allCommitsExtended
|
|
2437
|
+
);
|
|
2438
|
+
|
|
2439
|
+
// Assert
|
|
2440
|
+
expect(result).toHaveLength(0);
|
|
2441
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
it('should handle errors gracefully', async () => {
|
|
2445
|
+
// Arrange
|
|
2446
|
+
const projectName = 'project-123';
|
|
2447
|
+
const repoId = 'repo-456';
|
|
2448
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2449
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2450
|
+
const allCommitsExtended: any[] = [];
|
|
2451
|
+
|
|
2452
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('API error'));
|
|
2453
|
+
|
|
2454
|
+
// Act
|
|
2455
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2456
|
+
projectName,
|
|
2457
|
+
repoId,
|
|
2458
|
+
targetVersion,
|
|
2459
|
+
sourceVersion,
|
|
2460
|
+
allCommitsExtended
|
|
2461
|
+
);
|
|
2462
|
+
|
|
2463
|
+
// Assert - returns empty array when .gitmodules fetch fails
|
|
2464
|
+
expect(result).toEqual([]);
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
it('should handle multiple submodules', async () => {
|
|
2468
|
+
// Arrange
|
|
2469
|
+
const projectName = 'project-123';
|
|
2470
|
+
const repoId = 'repo-456';
|
|
2471
|
+
const targetVersion = { version: 'main', versionType: 'branch' };
|
|
2472
|
+
const sourceVersion = { version: 'v1.0.0', versionType: 'tag' };
|
|
2473
|
+
const allCommitsExtended: any[] = [];
|
|
2474
|
+
|
|
2475
|
+
const gitModulesContent = `[submodule "libs/common"]
|
|
2476
|
+
path = libs/common
|
|
2477
|
+
url = https://example.com/common
|
|
2478
|
+
[submodule "libs/utils"]
|
|
2479
|
+
path = libs/utils
|
|
2480
|
+
url = https://example.com/utils`;
|
|
2481
|
+
|
|
2482
|
+
(TFSServices.getItemContent as jest.Mock)
|
|
2483
|
+
.mockResolvedValueOnce({ content: gitModulesContent })
|
|
2484
|
+
.mockResolvedValueOnce({ content: 'target-sha-1' })
|
|
2485
|
+
.mockResolvedValueOnce({ content: 'source-sha-1' })
|
|
2486
|
+
.mockResolvedValueOnce({ content: 'target-sha-2' })
|
|
2487
|
+
.mockResolvedValueOnce({ content: 'source-sha-2' });
|
|
2488
|
+
|
|
2489
|
+
// Act
|
|
2490
|
+
const result = await gitDataProvider.getSubmodulesData(
|
|
2491
|
+
projectName,
|
|
2492
|
+
repoId,
|
|
2493
|
+
targetVersion,
|
|
2494
|
+
sourceVersion,
|
|
2495
|
+
allCommitsExtended
|
|
2496
|
+
);
|
|
2497
|
+
|
|
2498
|
+
// Assert
|
|
2499
|
+
expect(result).toHaveLength(2);
|
|
2500
|
+
expect(result[0].gitSubModuleName).toBe('libs_common');
|
|
2501
|
+
expect(result[1].gitSubModuleName).toBe('libs_utils');
|
|
2502
|
+
});
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
describe('GitDataProvider - createLinkedRelatedItemsForSVD', () => {
|
|
2506
|
+
let gitDataProvider: GitDataProvider;
|
|
2507
|
+
const mockOrgUrl = 'https://dev.azure.com/orgname/';
|
|
2508
|
+
const mockToken = 'mock-token';
|
|
2509
|
+
|
|
2510
|
+
beforeEach(() => {
|
|
2511
|
+
jest.clearAllMocks();
|
|
2512
|
+
gitDataProvider = new GitDataProvider(mockOrgUrl, mockToken);
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
it('should add Requirement when linkedWiTypes=reqOnly and linkedWiRelationship=affectsOnly', async () => {
|
|
2516
|
+
jest.spyOn((gitDataProvider as any).ticketsDataProvider, 'GetWorkItemByUrl').mockResolvedValueOnce({
|
|
2517
|
+
id: 10,
|
|
2518
|
+
fields: { 'System.WorkItemType': 'Requirement', 'System.Title': 'Req' },
|
|
2519
|
+
_links: { html: { href: 'http://example.com/10' } },
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
const res = await (gitDataProvider as any).createLinkedRelatedItemsForSVD(
|
|
2523
|
+
{ isEnabled: true, linkedWiTypes: 'reqOnly', linkedWiRelationship: 'affectsOnly' },
|
|
2524
|
+
{
|
|
2525
|
+
id: 1,
|
|
2526
|
+
relations: [
|
|
2527
|
+
{
|
|
2528
|
+
url: 'https://example.com/_apis/wit/workItems/10',
|
|
2529
|
+
rel: 'Affects',
|
|
2530
|
+
attributes: { name: 'Affects' },
|
|
2531
|
+
},
|
|
2532
|
+
],
|
|
2533
|
+
}
|
|
2534
|
+
);
|
|
2535
|
+
|
|
2536
|
+
expect(res).toHaveLength(1);
|
|
2537
|
+
expect(res[0]).toEqual(
|
|
2538
|
+
expect.objectContaining({
|
|
2539
|
+
id: 10,
|
|
2540
|
+
wiType: 'Requirement',
|
|
2541
|
+
relationType: 'Affects',
|
|
2542
|
+
title: 'Req',
|
|
2543
|
+
url: 'http://example.com/10',
|
|
2544
|
+
})
|
|
2545
|
+
);
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2548
|
+
it('should add Feature when linkedWiTypes=featureOnly and linkedWiRelationship=coversOnly', async () => {
|
|
2549
|
+
jest.spyOn((gitDataProvider as any).ticketsDataProvider, 'GetWorkItemByUrl').mockResolvedValueOnce({
|
|
2550
|
+
id: 11,
|
|
2551
|
+
fields: { 'System.WorkItemType': 'Feature', 'System.Title': 'Feat' },
|
|
2552
|
+
_links: { html: { href: 'http://example.com/11' } },
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
const res = await (gitDataProvider as any).createLinkedRelatedItemsForSVD(
|
|
2556
|
+
{ isEnabled: true, linkedWiTypes: 'featureOnly', linkedWiRelationship: 'coversOnly' },
|
|
2557
|
+
{
|
|
2558
|
+
id: 2,
|
|
2559
|
+
relations: [
|
|
2560
|
+
{
|
|
2561
|
+
url: 'https://example.com/_apis/wit/workItems/11',
|
|
2562
|
+
rel: 'CoveredBy',
|
|
2563
|
+
attributes: { name: 'CoveredBy' },
|
|
2564
|
+
},
|
|
2565
|
+
],
|
|
2566
|
+
}
|
|
2567
|
+
);
|
|
2568
|
+
|
|
2569
|
+
expect(res).toHaveLength(1);
|
|
2570
|
+
expect(res[0]).toEqual(
|
|
2571
|
+
expect.objectContaining({
|
|
2572
|
+
id: 11,
|
|
2573
|
+
wiType: 'Feature',
|
|
2574
|
+
relationType: 'CoveredBy',
|
|
2575
|
+
title: 'Feat',
|
|
2576
|
+
url: 'http://example.com/11',
|
|
2577
|
+
})
|
|
2578
|
+
);
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2581
|
+
it('should add items when linkedWiTypes=both and linkedWiRelationship=both', async () => {
|
|
2582
|
+
jest.spyOn((gitDataProvider as any).ticketsDataProvider, 'GetWorkItemByUrl').mockResolvedValueOnce({
|
|
2583
|
+
id: 12,
|
|
2584
|
+
fields: { 'System.WorkItemType': 'Requirement', 'System.Title': 'Req 12' },
|
|
2585
|
+
_links: { html: { href: 'http://example.com/12' } },
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2588
|
+
const res = await (gitDataProvider as any).createLinkedRelatedItemsForSVD(
|
|
2589
|
+
{ isEnabled: true, linkedWiTypes: 'both', linkedWiRelationship: 'both' },
|
|
2590
|
+
{
|
|
2591
|
+
id: 3,
|
|
2592
|
+
relations: [
|
|
2593
|
+
{
|
|
2594
|
+
url: 'https://example.com/_apis/wit/workItems/12',
|
|
2595
|
+
rel: 'Affects',
|
|
2596
|
+
attributes: { name: 'Affects' },
|
|
2597
|
+
},
|
|
2598
|
+
],
|
|
2599
|
+
}
|
|
2600
|
+
);
|
|
2601
|
+
|
|
2602
|
+
expect(res).toHaveLength(1);
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
it('should skip when linkedWiTypes is enabled but item type does not match', async () => {
|
|
2606
|
+
jest.spyOn((gitDataProvider as any).ticketsDataProvider, 'GetWorkItemByUrl').mockResolvedValueOnce({
|
|
2607
|
+
id: 13,
|
|
2608
|
+
fields: { 'System.WorkItemType': 'Task', 'System.Title': 'Task 13' },
|
|
2609
|
+
_links: { html: { href: 'http://example.com/13' } },
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
const res = await (gitDataProvider as any).createLinkedRelatedItemsForSVD(
|
|
2613
|
+
{ isEnabled: true, linkedWiTypes: 'reqOnly', linkedWiRelationship: 'both' },
|
|
2614
|
+
{
|
|
2615
|
+
id: 4,
|
|
2616
|
+
relations: [
|
|
2617
|
+
{
|
|
2618
|
+
url: 'https://example.com/_apis/wit/workItems/13',
|
|
2619
|
+
rel: 'Affects',
|
|
2620
|
+
attributes: { name: 'Affects' },
|
|
2621
|
+
},
|
|
2622
|
+
],
|
|
2623
|
+
}
|
|
2624
|
+
);
|
|
2625
|
+
|
|
2626
|
+
expect(res).toEqual([]);
|
|
2627
|
+
});
|
|
2628
|
+
});
|