@elisra-devops/docgen-data-provider 1.63.12 → 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.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +26 -9
  2. package/.github/workflows/release.yml +9 -10
  3. package/bin/helpers/tfs.d.ts +3 -0
  4. package/bin/helpers/tfs.js +44 -7
  5. package/bin/helpers/tfs.js.map +1 -1
  6. package/bin/modules/GitDataProvider.d.ts +10 -0
  7. package/bin/modules/GitDataProvider.js +10 -0
  8. package/bin/modules/GitDataProvider.js.map +1 -1
  9. package/bin/modules/MangementDataProvider.js +7 -1
  10. package/bin/modules/MangementDataProvider.js.map +1 -1
  11. package/bin/modules/TestDataProvider.js +0 -1
  12. package/bin/modules/TestDataProvider.js.map +1 -1
  13. package/bin/modules/TicketsDataProvider.d.ts +63 -27
  14. package/bin/modules/TicketsDataProvider.js +226 -122
  15. package/bin/modules/TicketsDataProvider.js.map +1 -1
  16. package/bin/tests/helpers/helper.test.js +279 -0
  17. package/bin/tests/helpers/helper.test.js.map +1 -0
  18. package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
  19. package/bin/tests/helpers/tfs.test.js.map +1 -0
  20. package/bin/tests/index.test.js +25 -0
  21. package/bin/tests/index.test.js.map +1 -0
  22. package/bin/tests/models/tfs-data.test.js +160 -0
  23. package/bin/tests/models/tfs-data.test.js.map +1 -0
  24. package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
  25. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
  26. package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
  27. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
  28. package/bin/tests/modules/gitDataProvider.test.js +1888 -0
  29. package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
  30. package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +39 -31
  31. package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
  32. package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
  33. package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
  34. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
  35. package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
  36. package/bin/tests/modules/testDataProvider.test.js +717 -0
  37. package/bin/tests/modules/testDataProvider.test.js.map +1 -0
  38. package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
  39. package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
  40. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
  41. package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
  42. package/bin/tests/utils/DataProviderUtils.test.js +61 -0
  43. package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
  44. package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
  45. package/bin/tests/utils/testStepParserHelper.test.js +359 -0
  46. package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
  47. package/package.json +10 -1
  48. package/src/helpers/tfs.ts +51 -7
  49. package/src/modules/GitDataProvider.ts +10 -0
  50. package/src/modules/MangementDataProvider.ts +6 -1
  51. package/src/modules/TestDataProvider.ts +0 -1
  52. package/src/modules/TicketsDataProvider.ts +311 -151
  53. package/src/tests/helpers/helper.test.ts +337 -0
  54. package/src/tests/helpers/tfs.test.ts +1092 -0
  55. package/src/tests/index.test.ts +28 -0
  56. package/src/tests/models/tfs-data.test.ts +203 -0
  57. package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
  58. package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
  59. package/src/tests/modules/gitDataProvider.test.ts +2628 -0
  60. package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +63 -32
  61. package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
  62. package/src/tests/modules/testDataProvider.test.ts +1046 -0
  63. package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
  64. package/src/tests/utils/DataProviderUtils.test.ts +76 -0
  65. package/src/tests/utils/testStepParserHelper.test.ts +437 -0
  66. package/tsconfig.json +1 -0
  67. package/bin/helpers/test/tfs.test.js.map +0 -1
  68. package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
  69. package/bin/modules/test/ResultDataProvider.test.js +0 -444
  70. package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
  71. package/bin/modules/test/gitDataProvider.test.js +0 -433
  72. package/bin/modules/test/gitDataProvider.test.js.map +0 -1
  73. package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
  74. package/bin/modules/test/pipelineDataProvider.test.js +0 -237
  75. package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
  76. package/bin/modules/test/testDataProvider.test.js +0 -234
  77. package/bin/modules/test/testDataProvider.test.js.map +0 -1
  78. package/bin/modules/test/ticketsDataProvider.test.js +0 -322
  79. package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
  80. package/src/helpers/test/tfs.test.ts +0 -748
  81. package/src/modules/test/JfrogDataProvider.test.ts +0 -171
  82. package/src/modules/test/ResultDataProvider.test.ts +0 -542
  83. package/src/modules/test/gitDataProvider.test.ts +0 -691
  84. package/src/modules/test/pipelineDataProvider.test.ts +0 -292
  85. package/src/modules/test/testDataProvider.test.ts +0 -318
  86. package/src/modules/test/ticketsDataProvider.test.ts +0 -434
  87. /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
  88. /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
  89. /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
  90. /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
  91. /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
  92. /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
  93. /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
  94. /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
@@ -0,0 +1,1038 @@
1
+ import { PipelineRun, Repository, ResourceRepository } from '../../models/tfs-data';
2
+ import { TFSServices } from '../../helpers/tfs';
3
+ import PipelinesDataProvider from '../../modules/PipelinesDataProvider';
4
+ import GitDataProvider from '../../modules/GitDataProvider';
5
+ import logger from '../../utils/logger';
6
+
7
+ jest.mock('../../helpers/tfs');
8
+ jest.mock('../../utils/logger');
9
+ jest.mock('../../modules/GitDataProvider');
10
+
11
+ describe('PipelinesDataProvider', () => {
12
+ let pipelinesDataProvider: PipelinesDataProvider;
13
+ const mockOrgUrl = 'https://dev.azure.com/orgname/';
14
+ const mockToken = 'mock-token';
15
+
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ pipelinesDataProvider = new PipelinesDataProvider(mockOrgUrl, mockToken);
19
+ });
20
+
21
+ describe('isMatchingPipeline', () => {
22
+ // Create test method to access private method
23
+ const invokeIsMatchingPipeline = (
24
+ fromPipeline: PipelineRun,
25
+ targetPipeline: PipelineRun,
26
+ searchPrevPipelineFromDifferentCommit: boolean
27
+ ): boolean => {
28
+ return (pipelinesDataProvider as any).isMatchingPipeline(
29
+ fromPipeline,
30
+ targetPipeline,
31
+ searchPrevPipelineFromDifferentCommit
32
+ );
33
+ };
34
+
35
+ it('should return false when repository IDs are different', () => {
36
+ // Arrange
37
+ const fromPipeline = {
38
+ resources: {
39
+ repositories: {
40
+ '0': {
41
+ self: {
42
+ repository: { id: 'repo1' },
43
+ version: 'v1',
44
+ refName: 'refs/heads/main',
45
+ },
46
+ },
47
+ },
48
+ },
49
+ } as unknown as PipelineRun;
50
+
51
+ const targetPipeline = {
52
+ resources: {
53
+ repositories: {
54
+ '0': {
55
+ self: {
56
+ repository: { id: 'repo2' },
57
+ version: 'v1',
58
+ refName: 'refs/heads/main',
59
+ },
60
+ },
61
+ },
62
+ },
63
+ } as unknown as PipelineRun;
64
+
65
+ // Act
66
+ const result = invokeIsMatchingPipeline(fromPipeline, targetPipeline, false);
67
+
68
+ // Assert
69
+ expect(result).toBe(false);
70
+ });
71
+
72
+ it('should return true when versions are the same and searchPrevPipelineFromDifferentCommit is false', () => {
73
+ // Arrange
74
+ const fromPipeline = {
75
+ resources: {
76
+ repositories: {
77
+ '0': {
78
+ self: {
79
+ repository: { id: 'repo1' },
80
+ version: 'v1',
81
+ refName: 'refs/heads/main',
82
+ },
83
+ },
84
+ },
85
+ },
86
+ } as unknown as PipelineRun;
87
+
88
+ const targetPipeline = {
89
+ resources: {
90
+ repositories: {
91
+ '0': {
92
+ self: {
93
+ repository: { id: 'repo1' },
94
+ version: 'v1',
95
+ refName: 'refs/heads/main',
96
+ },
97
+ },
98
+ },
99
+ },
100
+ } as unknown as PipelineRun;
101
+
102
+ // Act
103
+ const result = invokeIsMatchingPipeline(fromPipeline, targetPipeline, false);
104
+
105
+ // Assert
106
+ expect(result).toBe(true);
107
+ });
108
+
109
+ it('should return false when versions are the same and searchPrevPipelineFromDifferentCommit is true', () => {
110
+ // Arrange
111
+ const fromPipeline = {
112
+ resources: {
113
+ repositories: {
114
+ '0': {
115
+ self: {
116
+ repository: { id: 'repo1' },
117
+ version: 'v1',
118
+ refName: 'refs/heads/main',
119
+ },
120
+ },
121
+ },
122
+ },
123
+ } as unknown as PipelineRun;
124
+
125
+ const targetPipeline = {
126
+ resources: {
127
+ repositories: {
128
+ '0': {
129
+ self: {
130
+ repository: { id: 'repo1' },
131
+ version: 'v1',
132
+ refName: 'refs/heads/main',
133
+ },
134
+ },
135
+ },
136
+ },
137
+ } as unknown as PipelineRun;
138
+
139
+ // Act
140
+ const result = invokeIsMatchingPipeline(fromPipeline, targetPipeline, true);
141
+
142
+ // Assert
143
+ expect(result).toBe(false);
144
+ });
145
+
146
+ it('should return true when refNames match but versions differ', () => {
147
+ // Arrange
148
+ const fromPipeline = {
149
+ resources: {
150
+ repositories: {
151
+ '0': {
152
+ self: {
153
+ repository: { id: 'repo1' },
154
+ version: 'v1',
155
+ refName: 'refs/heads/main',
156
+ },
157
+ },
158
+ },
159
+ },
160
+ } as unknown as PipelineRun;
161
+
162
+ const targetPipeline = {
163
+ resources: {
164
+ repositories: {
165
+ '0': {
166
+ self: {
167
+ repository: { id: 'repo1' },
168
+ version: 'v2',
169
+ refName: 'refs/heads/main',
170
+ },
171
+ },
172
+ },
173
+ },
174
+ } as unknown as PipelineRun;
175
+
176
+ // Act
177
+ const result = invokeIsMatchingPipeline(fromPipeline, targetPipeline, true);
178
+
179
+ // Assert
180
+ expect(result).toBe(true);
181
+ });
182
+
183
+ it('should use __designer_repo when self is not available', () => {
184
+ // Arrange
185
+ const fromPipeline = {
186
+ resources: {
187
+ repositories: {
188
+ __designer_repo: {
189
+ repository: { id: 'repo1' },
190
+ version: 'v1',
191
+ refName: 'refs/heads/main',
192
+ },
193
+ },
194
+ },
195
+ } as unknown as PipelineRun;
196
+
197
+ const targetPipeline = {
198
+ resources: {
199
+ repositories: {
200
+ __designer_repo: {
201
+ repository: { id: 'repo1' },
202
+ version: 'v1',
203
+ refName: 'refs/heads/main',
204
+ },
205
+ },
206
+ },
207
+ } as unknown as PipelineRun;
208
+
209
+ // Act
210
+ const result = invokeIsMatchingPipeline(fromPipeline, targetPipeline, false);
211
+
212
+ // Assert
213
+ expect(result).toBe(true);
214
+ });
215
+ });
216
+
217
+ describe('getPipelineRunDetails', () => {
218
+ it('should call TFSServices.getItemContent with correct parameters', async () => {
219
+ // Arrange
220
+ const projectName = 'project1';
221
+ const pipelineId = 123;
222
+ const runId = 456;
223
+ const mockResponse = { id: runId, resources: {} };
224
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
225
+
226
+ // Act
227
+ const result = await pipelinesDataProvider.getPipelineRunDetails(projectName, pipelineId, runId);
228
+
229
+ // Assert
230
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
231
+ `${mockOrgUrl}${projectName}/_apis/pipelines/${pipelineId}/runs/${runId}`,
232
+ mockToken
233
+ );
234
+ expect(result).toEqual(mockResponse);
235
+ });
236
+ });
237
+
238
+ describe('GetPipelineRunHistory', () => {
239
+ it('should return filtered pipeline run history', async () => {
240
+ // Arrange
241
+ const projectName = 'project1';
242
+ const pipelineId = '123';
243
+ const mockResponse = {
244
+ value: [
245
+ { id: 1, result: 'succeeded' },
246
+ { id: 2, result: 'failed' },
247
+ { id: 3, result: 'canceled' },
248
+ { id: 4, result: 'succeeded' },
249
+ ],
250
+ };
251
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
252
+
253
+ // Act
254
+ const result = await pipelinesDataProvider.GetPipelineRunHistory(projectName, pipelineId);
255
+
256
+ // Assert
257
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
258
+ `${mockOrgUrl}${projectName}/_apis/pipelines/${pipelineId}/runs`,
259
+ mockToken,
260
+ 'get',
261
+ null,
262
+ null
263
+ );
264
+ expect(result).toEqual({
265
+ count: 4, // Note: Current filter logic keeps all runs where result is not 'failed' AND not 'canceled'
266
+ value: mockResponse.value,
267
+ });
268
+ });
269
+
270
+ it('should handle API errors gracefully', async () => {
271
+ // Arrange
272
+ const projectName = 'project1';
273
+ const pipelineId = '123';
274
+ const expectedError = new Error('API error');
275
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(expectedError);
276
+
277
+ // Act
278
+ const result = await pipelinesDataProvider.GetPipelineRunHistory(projectName, pipelineId);
279
+
280
+ // Assert
281
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
282
+ `${mockOrgUrl}${projectName}/_apis/pipelines/${pipelineId}/runs`,
283
+ mockToken,
284
+ 'get',
285
+ null,
286
+ null
287
+ );
288
+ expect(logger.error).toHaveBeenCalledWith(
289
+ `Could not fetch Pipeline Run History: ${expectedError.message}`
290
+ );
291
+ expect(result).toBeUndefined();
292
+ });
293
+
294
+ it('should return response when value is undefined', async () => {
295
+ // Arrange
296
+ const projectName = 'project1';
297
+ const pipelineId = '123';
298
+ const mockResponse = { count: 0 };
299
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
300
+
301
+ // Act
302
+ const result = await pipelinesDataProvider.GetPipelineRunHistory(projectName, pipelineId);
303
+
304
+ // Assert
305
+ expect(result).toEqual(mockResponse);
306
+ });
307
+ });
308
+
309
+ describe('getPipelineBuildByBuildId', () => {
310
+ it('should fetch pipeline build by build ID', async () => {
311
+ // Arrange
312
+ const projectName = 'project1';
313
+ const buildId = 123;
314
+ const mockResponse = { id: buildId, buildNumber: '20231201.1' };
315
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
316
+
317
+ // Act
318
+ const result = await pipelinesDataProvider.getPipelineBuildByBuildId(projectName, buildId);
319
+
320
+ // Assert
321
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
322
+ `${mockOrgUrl}${projectName}/_apis/build/builds/${buildId}`,
323
+ mockToken,
324
+ 'get'
325
+ );
326
+ expect(result).toEqual(mockResponse);
327
+ });
328
+ });
329
+
330
+ describe('TriggerBuildById', () => {
331
+ it('should trigger a build with parameters', async () => {
332
+ // Arrange
333
+ const projectName = 'project1';
334
+ const buildDefId = '456';
335
+ const parameters = '{"Test":"123"}';
336
+ const mockResponse = { id: 789, status: 'queued' };
337
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
338
+
339
+ // Act
340
+ const result = await pipelinesDataProvider.TriggerBuildById(projectName, buildDefId, parameters);
341
+
342
+ // Assert
343
+ expect(TFSServices.postRequest).toHaveBeenCalledWith(
344
+ `${mockOrgUrl}${projectName}/_apis/build/builds?api-version=5.0`,
345
+ mockToken,
346
+ 'post',
347
+ {
348
+ definition: { id: buildDefId },
349
+ parameters: parameters,
350
+ },
351
+ null
352
+ );
353
+ expect(result).toEqual(mockResponse);
354
+ });
355
+ });
356
+
357
+ describe('GetArtifactByBuildId', () => {
358
+ it('should return empty response when no artifacts exist', async () => {
359
+ // Arrange
360
+ const projectName = 'project1';
361
+ const buildId = '123';
362
+ const artifactName = 'drop';
363
+ const mockResponse = { count: 0 };
364
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
365
+
366
+ // Act
367
+ const result = await pipelinesDataProvider.GetArtifactByBuildId(projectName, buildId, artifactName);
368
+
369
+ // Assert
370
+ expect(result).toEqual(mockResponse);
371
+ });
372
+
373
+ it('should download artifact when it exists', async () => {
374
+ // Arrange
375
+ const projectName = 'project1';
376
+ const buildId = '123';
377
+ const artifactName = 'drop';
378
+ const mockArtifactsResponse = { count: 1 };
379
+ const mockArtifactResponse = {
380
+ resource: { downloadUrl: 'https://example.com/download' },
381
+ };
382
+ const mockDownloadResult = { data: Buffer.from('zip content') };
383
+
384
+ (TFSServices.getItemContent as jest.Mock)
385
+ .mockResolvedValueOnce(mockArtifactsResponse)
386
+ .mockResolvedValueOnce(mockArtifactResponse);
387
+ (TFSServices.downloadZipFile as jest.Mock).mockResolvedValueOnce(mockDownloadResult);
388
+
389
+ // Act
390
+ const result = await pipelinesDataProvider.GetArtifactByBuildId(projectName, buildId, artifactName);
391
+
392
+ // Assert
393
+ expect(TFSServices.downloadZipFile).toHaveBeenCalledWith('https://example.com/download', mockToken);
394
+ expect(result).toEqual(mockDownloadResult);
395
+ });
396
+
397
+ it('should throw error when artifact fetch fails', async () => {
398
+ // Arrange
399
+ const projectName = 'project1';
400
+ const buildId = '123';
401
+ const artifactName = 'drop';
402
+ const mockError = new Error('Artifact not found');
403
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
404
+
405
+ // Act & Assert
406
+ await expect(
407
+ pipelinesDataProvider.GetArtifactByBuildId(projectName, buildId, artifactName)
408
+ ).rejects.toThrow();
409
+ expect(logger.error).toHaveBeenCalled();
410
+ });
411
+ });
412
+
413
+ describe('getPipelineStageName', () => {
414
+ it('should return the matching Stage record', async () => {
415
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
416
+ records: [
417
+ { type: 'Stage', name: 'Deploy', state: 'completed', result: 'succeeded' },
418
+ { type: 'Job', name: 'Job1' },
419
+ ],
420
+ });
421
+
422
+ const record = await (pipelinesDataProvider as any).getPipelineStageName(123, 'project1', 'Deploy');
423
+ expect(record).toEqual(expect.objectContaining({ type: 'Stage', name: 'Deploy' }));
424
+ });
425
+
426
+ it('should return undefined when no matching stage exists', async () => {
427
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
428
+ records: [{ type: 'Stage', name: 'Build', state: 'completed', result: 'succeeded' }],
429
+ });
430
+
431
+ const record = await (pipelinesDataProvider as any).getPipelineStageName(123, 'project1', 'Deploy');
432
+ expect(record).toBeUndefined();
433
+ });
434
+
435
+ it('should return undefined on fetch error and log', async () => {
436
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('boom'));
437
+
438
+ const record = await (pipelinesDataProvider as any).getPipelineStageName(123, 'project1', 'Deploy');
439
+ expect(record).toBeUndefined();
440
+ expect(logger.error).toHaveBeenCalled();
441
+ });
442
+ });
443
+
444
+ describe('isStageSuccessful', () => {
445
+ it('should return false when stage is missing', async () => {
446
+ jest.spyOn(pipelinesDataProvider as any, 'getPipelineStageName').mockResolvedValueOnce(undefined);
447
+ await expect(
448
+ (pipelinesDataProvider as any).isStageSuccessful({ id: 1 }, 'project1', 'Deploy')
449
+ ).resolves.toBeUndefined();
450
+ });
451
+
452
+ it('should return false when stage is not completed', async () => {
453
+ jest
454
+ .spyOn(pipelinesDataProvider as any, 'getPipelineStageName')
455
+ .mockResolvedValueOnce({ state: 'inProgress', result: 'succeeded' });
456
+ await expect(
457
+ (pipelinesDataProvider as any).isStageSuccessful({ id: 1 }, 'project1', 'Deploy')
458
+ ).resolves.toBe(false);
459
+ });
460
+
461
+ it('should return false when stage result is not succeeded', async () => {
462
+ jest
463
+ .spyOn(pipelinesDataProvider as any, 'getPipelineStageName')
464
+ .mockResolvedValueOnce({ state: 'completed', result: 'failed' });
465
+ await expect(
466
+ (pipelinesDataProvider as any).isStageSuccessful({ id: 1 }, 'project1', 'Deploy')
467
+ ).resolves.toBe(false);
468
+ });
469
+
470
+ it('should return true when stage is completed and succeeded', async () => {
471
+ jest
472
+ .spyOn(pipelinesDataProvider as any, 'getPipelineStageName')
473
+ .mockResolvedValueOnce({ state: 'completed', result: 'succeeded' });
474
+ await expect(
475
+ (pipelinesDataProvider as any).isStageSuccessful({ id: 1 }, 'project1', 'Deploy')
476
+ ).resolves.toBe(true);
477
+ });
478
+ });
479
+
480
+ describe('GetReleaseByReleaseId', () => {
481
+ it('should fetch release by ID', async () => {
482
+ // Arrange
483
+ const projectName = 'project1';
484
+ const releaseId = 123;
485
+ const mockResponse = { id: releaseId, name: 'Release-1' };
486
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
487
+
488
+ // Act
489
+ const result = await pipelinesDataProvider.GetReleaseByReleaseId(projectName, releaseId);
490
+
491
+ // Assert
492
+ expect(result).toEqual(mockResponse);
493
+ });
494
+
495
+ it('should replace dev.azure.com with vsrm.dev.azure.com for release URL', async () => {
496
+ // Arrange
497
+ const projectName = 'project1';
498
+ const releaseId = 123;
499
+ const mockResponse = { id: releaseId };
500
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
501
+
502
+ // Act
503
+ await pipelinesDataProvider.GetReleaseByReleaseId(projectName, releaseId);
504
+
505
+ // Assert
506
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
507
+ expect.stringContaining('vsrm.dev.azure.com'),
508
+ mockToken,
509
+ 'get',
510
+ null,
511
+ null
512
+ );
513
+ });
514
+ });
515
+
516
+ describe('GetReleaseHistory', () => {
517
+ it('should fetch release history for a definition', async () => {
518
+ // Arrange
519
+ const projectName = 'project1';
520
+ const definitionId = '456';
521
+ const mockResponse = { value: [{ id: 1 }, { id: 2 }] };
522
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
523
+
524
+ // Act
525
+ const result = await pipelinesDataProvider.GetReleaseHistory(projectName, definitionId);
526
+
527
+ // Assert
528
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
529
+ expect.stringContaining(`definitionId=${definitionId}`),
530
+ mockToken,
531
+ 'get',
532
+ null,
533
+ null
534
+ );
535
+ expect(result).toEqual(mockResponse);
536
+ });
537
+ });
538
+
539
+ describe('GetAllReleaseHistory', () => {
540
+ it('should fetch all releases with pagination', async () => {
541
+ // Arrange
542
+ const projectName = 'project1';
543
+ const definitionId = '456';
544
+ const mockResponse1 = {
545
+ data: { value: [{ id: 1 }, { id: 2 }] },
546
+ headers: { 'x-ms-continuationtoken': 'token123' },
547
+ };
548
+ const mockResponse2 = {
549
+ data: { value: [{ id: 3 }] },
550
+ headers: {},
551
+ };
552
+ (TFSServices.getItemContentWithHeaders as jest.Mock)
553
+ .mockResolvedValueOnce(mockResponse1)
554
+ .mockResolvedValueOnce(mockResponse2);
555
+
556
+ // Act
557
+ const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId);
558
+
559
+ // Assert
560
+ expect(result.count).toBe(3);
561
+ expect(result.value).toHaveLength(3);
562
+ });
563
+
564
+ it('should support x-ms-continuation-token header and default value when data is missing', async () => {
565
+ const projectName = 'project1';
566
+ const definitionId = '456';
567
+
568
+ (TFSServices.getItemContentWithHeaders as jest.Mock)
569
+ .mockResolvedValueOnce({
570
+ data: undefined,
571
+ headers: { 'x-ms-continuation-token': 'token123' },
572
+ })
573
+ .mockResolvedValueOnce({
574
+ data: { value: [{ id: 1 }] },
575
+ headers: {},
576
+ });
577
+
578
+ const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId);
579
+
580
+ expect(result.count).toBe(1);
581
+ expect(result.value).toEqual([{ id: 1 }]);
582
+ });
583
+
584
+ it('should handle errors during pagination', async () => {
585
+ // Arrange
586
+ const projectName = 'project1';
587
+ const definitionId = '456';
588
+ (TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
589
+
590
+ // Act
591
+ const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId);
592
+
593
+ // Assert
594
+ expect(result).toEqual({ count: 0, value: [] });
595
+ expect(logger.error).toHaveBeenCalled();
596
+ });
597
+ });
598
+
599
+ describe('GetAllPipelines', () => {
600
+ it('should fetch all pipelines for a project', async () => {
601
+ // Arrange
602
+ const projectName = 'project1';
603
+ const mockResponse = { value: [{ id: 1 }, { id: 2 }], count: 2 };
604
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
605
+
606
+ // Act
607
+ const result = await pipelinesDataProvider.GetAllPipelines(projectName);
608
+
609
+ // Assert
610
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
611
+ `${mockOrgUrl}${projectName}/_apis/pipelines?$top=2000`,
612
+ mockToken,
613
+ 'get',
614
+ null,
615
+ null
616
+ );
617
+ expect(result).toEqual(mockResponse);
618
+ });
619
+ });
620
+
621
+ describe('GetAllReleaseDefenitions', () => {
622
+ it('should fetch all release definitions', async () => {
623
+ // Arrange
624
+ const projectName = 'project1';
625
+ const mockResponse = { value: [{ id: 1 }, { id: 2 }] };
626
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
627
+
628
+ // Act
629
+ const result = await pipelinesDataProvider.GetAllReleaseDefenitions(projectName);
630
+
631
+ // Assert
632
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
633
+ expect.stringContaining('vsrm.dev.azure.com'),
634
+ mockToken,
635
+ 'get',
636
+ null,
637
+ null
638
+ );
639
+ expect(result).toEqual(mockResponse);
640
+ });
641
+ });
642
+
643
+ describe('GetRecentReleaseArtifactInfo', () => {
644
+ it('should return empty array when no releases exist', async () => {
645
+ // Arrange
646
+ const projectName = 'project1';
647
+ const mockResponse = { value: [] };
648
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResponse);
649
+
650
+ // Act
651
+ const result = await pipelinesDataProvider.GetRecentReleaseArtifactInfo(projectName);
652
+
653
+ // Assert
654
+ expect(result).toEqual([]);
655
+ });
656
+
657
+ it('should return artifact info from most recent release', async () => {
658
+ // Arrange
659
+ const projectName = 'project1';
660
+ const mockReleasesResponse = { value: [{ id: 123 }] };
661
+ const mockReleaseResponse = {
662
+ artifacts: [
663
+ {
664
+ definitionReference: {
665
+ definition: { name: 'artifact1' },
666
+ version: { name: '1.0.0' },
667
+ },
668
+ },
669
+ ],
670
+ };
671
+ (TFSServices.getItemContent as jest.Mock)
672
+ .mockResolvedValueOnce(mockReleasesResponse)
673
+ .mockResolvedValueOnce(mockReleaseResponse);
674
+
675
+ // Act
676
+ const result = await pipelinesDataProvider.GetRecentReleaseArtifactInfo(projectName);
677
+
678
+ // Assert
679
+ expect(result).toEqual([{ artifactName: 'artifact1', artifactVersion: '1.0.0' }]);
680
+ });
681
+ });
682
+
683
+ describe('getPipelineResourcePipelinesFromObject', () => {
684
+ it('should return empty set when no pipeline resources exist', async () => {
685
+ // Arrange
686
+ const inPipeline = {
687
+ resources: {},
688
+ } as unknown as PipelineRun;
689
+
690
+ // Act
691
+ const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
692
+
693
+ // Assert
694
+ expect(result).toEqual(new Set());
695
+ });
696
+
697
+ it('should extract pipeline resources from pipeline object', async () => {
698
+ // Arrange
699
+ const inPipeline = {
700
+ resources: {
701
+ pipelines: {
702
+ myPipeline: {
703
+ pipeline: {
704
+ id: 123,
705
+ url: 'https://dev.azure.com/org/project/_apis/pipelines/123?revision=1',
706
+ },
707
+ },
708
+ },
709
+ },
710
+ } as unknown as PipelineRun;
711
+
712
+ const mockBuildResponse = {
713
+ definition: { id: 456, type: 'build' },
714
+ buildNumber: '20231201.1',
715
+ project: { name: 'project1' },
716
+ repository: { type: 'TfsGit' },
717
+ };
718
+
719
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockBuildResponse);
720
+
721
+ // Act
722
+ const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
723
+
724
+ // Assert
725
+ expect(result).toHaveLength(1);
726
+ expect((result as any[])[0]).toEqual({
727
+ name: 'myPipeline',
728
+ buildId: 123,
729
+ definitionId: 456,
730
+ buildNumber: '20231201.1',
731
+ teamProject: 'project1',
732
+ provider: 'TfsGit',
733
+ });
734
+ });
735
+
736
+ it('should handle errors when fetching pipeline resources', async () => {
737
+ // Arrange
738
+ const inPipeline = {
739
+ resources: {
740
+ pipelines: {
741
+ myPipeline: {
742
+ pipeline: {
743
+ id: 123,
744
+ url: 'https://dev.azure.com/org/project/_apis/pipelines/123?revision=1',
745
+ },
746
+ },
747
+ },
748
+ },
749
+ } as unknown as PipelineRun;
750
+
751
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
752
+
753
+ // Act
754
+ const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
755
+
756
+ // Assert
757
+ expect(result).toEqual([]);
758
+ expect(logger.error).toHaveBeenCalled();
759
+ });
760
+ });
761
+
762
+ describe('getPipelineResourceRepositoriesFromObject', () => {
763
+ it('should return empty map when no repository resources exist', async () => {
764
+ // Arrange
765
+ const inPipeline = {
766
+ resources: {},
767
+ } as unknown as PipelineRun;
768
+ const mockGitDataProvider = {} as GitDataProvider;
769
+
770
+ // Act
771
+ const result = await pipelinesDataProvider.getPipelineResourceRepositoriesFromObject(
772
+ inPipeline,
773
+ mockGitDataProvider
774
+ );
775
+
776
+ // Assert
777
+ expect(result).toEqual(new Map());
778
+ });
779
+
780
+ it('should extract repository resources from pipeline object', async () => {
781
+ // Arrange
782
+ const inPipeline = {
783
+ resources: {
784
+ repositories: {
785
+ self: {
786
+ repository: { id: 'repo-123', type: 'azureReposGit' },
787
+ version: 'abc123',
788
+ },
789
+ },
790
+ },
791
+ } as unknown as PipelineRun;
792
+
793
+ const mockRepo = {
794
+ name: 'MyRepo',
795
+ url: 'https://dev.azure.com/org/project/_git/MyRepo',
796
+ };
797
+
798
+ const mockGitDataProvider = {
799
+ GetGitRepoFromRepoId: jest.fn().mockResolvedValue(mockRepo),
800
+ } as unknown as GitDataProvider;
801
+
802
+ // Act
803
+ const result = await pipelinesDataProvider.getPipelineResourceRepositoriesFromObject(
804
+ inPipeline,
805
+ mockGitDataProvider
806
+ );
807
+
808
+ // Assert
809
+ expect(result).toHaveLength(1);
810
+ expect((result as any[])[0]).toEqual({
811
+ repoName: 'MyRepo',
812
+ repoSha1: 'abc123',
813
+ url: 'https://dev.azure.com/org/project/_git/MyRepo',
814
+ });
815
+ });
816
+
817
+ it('should skip non-azureReposGit repositories', async () => {
818
+ // Arrange
819
+ const inPipeline = {
820
+ resources: {
821
+ repositories: {
822
+ external: {
823
+ repository: { id: 'repo-123', type: 'GitHub' },
824
+ version: 'abc123',
825
+ },
826
+ },
827
+ },
828
+ } as unknown as PipelineRun;
829
+
830
+ const mockGitDataProvider = {
831
+ GetGitRepoFromRepoId: jest.fn(),
832
+ } as unknown as GitDataProvider;
833
+
834
+ // Act
835
+ const result = await pipelinesDataProvider.getPipelineResourceRepositoriesFromObject(
836
+ inPipeline,
837
+ mockGitDataProvider
838
+ );
839
+
840
+ // Assert
841
+ expect(result).toHaveLength(0);
842
+ expect(mockGitDataProvider.GetGitRepoFromRepoId).not.toHaveBeenCalled();
843
+ });
844
+ });
845
+
846
+ describe('findPreviousPipeline', () => {
847
+ it('should return undefined when no pipeline runs exist', async () => {
848
+ // Arrange
849
+ const teamProject = 'project1';
850
+ const pipelineId = '123';
851
+ const toPipelineRunId = 100;
852
+ const targetPipeline = {} as PipelineRun;
853
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
854
+
855
+ // Act
856
+ const result = await pipelinesDataProvider.findPreviousPipeline(
857
+ teamProject,
858
+ pipelineId,
859
+ toPipelineRunId,
860
+ targetPipeline,
861
+ false
862
+ );
863
+
864
+ // Assert
865
+ expect(result).toBeUndefined();
866
+ });
867
+
868
+ it('should skip invalid runs and return first matching previous pipeline', async () => {
869
+ const teamProject = 'project1';
870
+ const pipelineId = '123';
871
+ const toPipelineRunId = 100;
872
+ const targetPipeline = {
873
+ resources: {
874
+ repositories: { '0': { self: { repository: { id: 'r' }, version: 'v2', refName: 'main' } } },
875
+ },
876
+ } as any;
877
+
878
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
879
+ value: [
880
+ { id: 100, result: 'succeeded' },
881
+ { id: 99, result: 'succeeded' },
882
+ ],
883
+ });
884
+
885
+ jest.spyOn(pipelinesDataProvider as any, 'getPipelineRunDetails').mockResolvedValueOnce({
886
+ resources: {
887
+ repositories: { '0': { self: { repository: { id: 'r' }, version: 'v1', refName: 'main' } } },
888
+ },
889
+ });
890
+ jest.spyOn(pipelinesDataProvider as any, 'isMatchingPipeline').mockReturnValueOnce(true);
891
+
892
+ const res = await pipelinesDataProvider.findPreviousPipeline(
893
+ teamProject,
894
+ pipelineId,
895
+ toPipelineRunId,
896
+ targetPipeline,
897
+ true
898
+ );
899
+ expect(res).toBe(99);
900
+ });
901
+
902
+ it('should skip when fromStage provided but stage is not successful', async () => {
903
+ const teamProject = 'project1';
904
+ const pipelineId = '123';
905
+ const toPipelineRunId = 100;
906
+ const targetPipeline = {} as PipelineRun;
907
+
908
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
909
+ value: [{ id: 99, result: 'succeeded' }],
910
+ });
911
+ jest.spyOn(pipelinesDataProvider as any, 'isStageSuccessful').mockResolvedValueOnce(false);
912
+ const detailsSpy = jest.spyOn(pipelinesDataProvider as any, 'getPipelineRunDetails');
913
+
914
+ const res = await pipelinesDataProvider.findPreviousPipeline(
915
+ teamProject,
916
+ pipelineId,
917
+ toPipelineRunId,
918
+ targetPipeline,
919
+ false,
920
+ 'Deploy'
921
+ );
922
+
923
+ expect(res).toBeUndefined();
924
+ expect(detailsSpy).not.toHaveBeenCalled();
925
+ });
926
+
927
+ it('should skip when pipeline details do not include repositories', async () => {
928
+ const teamProject = 'project1';
929
+ const pipelineId = '123';
930
+ const toPipelineRunId = 100;
931
+ const targetPipeline = {} as PipelineRun;
932
+
933
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
934
+ value: [{ id: 99, result: 'succeeded' }],
935
+ });
936
+ jest
937
+ .spyOn(pipelinesDataProvider as any, 'getPipelineRunDetails')
938
+ .mockResolvedValueOnce({ resources: {} });
939
+
940
+ const res = await pipelinesDataProvider.findPreviousPipeline(
941
+ teamProject,
942
+ pipelineId,
943
+ toPipelineRunId,
944
+ targetPipeline,
945
+ false
946
+ );
947
+
948
+ expect(res).toBeUndefined();
949
+ });
950
+
951
+ it('should find matching previous pipeline', async () => {
952
+ // Arrange
953
+ const teamProject = 'project1';
954
+ const pipelineId = '123';
955
+ const toPipelineRunId = 100;
956
+ const targetPipeline = {
957
+ resources: {
958
+ repositories: {
959
+ '0': {
960
+ self: {
961
+ repository: { id: 'repo1' },
962
+ version: 'v2',
963
+ refName: 'refs/heads/main',
964
+ },
965
+ },
966
+ },
967
+ },
968
+ } as unknown as PipelineRun;
969
+
970
+ const mockRunHistory = {
971
+ value: [{ id: 99, result: 'succeeded' }],
972
+ };
973
+
974
+ const mockPipelineDetails = {
975
+ resources: {
976
+ repositories: {
977
+ '0': {
978
+ self: {
979
+ repository: { id: 'repo1' },
980
+ version: 'v1',
981
+ refName: 'refs/heads/main',
982
+ },
983
+ },
984
+ },
985
+ },
986
+ };
987
+
988
+ (TFSServices.getItemContent as jest.Mock)
989
+ .mockResolvedValueOnce(mockRunHistory)
990
+ .mockResolvedValueOnce(mockPipelineDetails);
991
+
992
+ // Act
993
+ const result = await pipelinesDataProvider.findPreviousPipeline(
994
+ teamProject,
995
+ pipelineId,
996
+ toPipelineRunId,
997
+ targetPipeline,
998
+ true
999
+ );
1000
+
1001
+ // Assert
1002
+ expect(result).toBe(99);
1003
+ });
1004
+ });
1005
+
1006
+ describe('isInvalidPipelineRun', () => {
1007
+ const invokeIsInvalidPipelineRun = (
1008
+ pipelineRun: any,
1009
+ toPipelineRunId: number,
1010
+ fromStage: string
1011
+ ): boolean => {
1012
+ return (pipelinesDataProvider as any).isInvalidPipelineRun(pipelineRun, toPipelineRunId, fromStage);
1013
+ };
1014
+
1015
+ it('should return true when pipeline run id >= toPipelineRunId', () => {
1016
+ expect(invokeIsInvalidPipelineRun({ id: 100, result: 'succeeded' }, 100, '')).toBe(true);
1017
+ expect(invokeIsInvalidPipelineRun({ id: 101, result: 'succeeded' }, 100, '')).toBe(true);
1018
+ });
1019
+
1020
+ it('should return true for canceled/failed/canceling results', () => {
1021
+ expect(invokeIsInvalidPipelineRun({ id: 99, result: 'canceled' }, 100, '')).toBe(true);
1022
+ expect(invokeIsInvalidPipelineRun({ id: 99, result: 'failed' }, 100, '')).toBe(true);
1023
+ expect(invokeIsInvalidPipelineRun({ id: 99, result: 'canceling' }, 100, '')).toBe(true);
1024
+ });
1025
+
1026
+ it('should return true for unknown result without fromStage', () => {
1027
+ expect(invokeIsInvalidPipelineRun({ id: 99, result: 'unknown' }, 100, '')).toBe(true);
1028
+ });
1029
+
1030
+ it('should return false for valid pipeline run with fromStage', () => {
1031
+ expect(invokeIsInvalidPipelineRun({ id: 99, result: 'unknown' }, 100, 'Deploy')).toBe(false);
1032
+ });
1033
+
1034
+ it('should return false for succeeded result', () => {
1035
+ expect(invokeIsInvalidPipelineRun({ id: 99, result: 'succeeded' }, 100, '')).toBe(false);
1036
+ });
1037
+ });
1038
+ });