@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,1046 @@
1
+ import { TFSServices } from '../../helpers/tfs';
2
+ import { Helper, suiteData } from '../../helpers/helper';
3
+ import TestDataProvider from '../../modules/TestDataProvider';
4
+ import Utils from '../../utils/testStepParserHelper';
5
+ import logger from '../../utils/logger';
6
+ import { TestCase } from '../../models/tfs-data';
7
+
8
+ jest.mock('../../helpers/tfs');
9
+ jest.mock('../../utils/logger');
10
+ jest.mock('../../helpers/helper');
11
+ jest.mock('../../utils/testStepParserHelper', () => {
12
+ return {
13
+ __esModule: true,
14
+ default: jest.fn().mockImplementation(() => ({
15
+ parseTestSteps: jest.fn(),
16
+ })),
17
+ };
18
+ });
19
+ jest.mock('p-limit', () => jest.fn(() => (fn: Function) => fn()));
20
+
21
+ describe('TestDataProvider', () => {
22
+ let testDataProvider: TestDataProvider;
23
+ const mockOrgUrl = 'https://dev.azure.com/orgname/';
24
+ const mockToken = 'mock-token';
25
+ const mockProject = 'project-123';
26
+ const mockPlanId = '456';
27
+ const mockSuiteId = '789';
28
+ const mockTestCaseId = '101112';
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+
33
+ testDataProvider = new TestDataProvider(mockOrgUrl, mockToken);
34
+ });
35
+
36
+ // Helper to access private method for testing
37
+ const invokeFetchWithCache = async (instance: any, url: string, ttl = 60000) => {
38
+ return instance.fetchWithCache.call(instance, url, ttl);
39
+ };
40
+
41
+ describe('fetchWithCache', () => {
42
+ it('should return cached data when available and not expired', async () => {
43
+ // Arrange
44
+ const mockUrl = `${mockOrgUrl}_apis/test/endpoint`;
45
+ const mockData = { value: 'test data' };
46
+ const cache = new Map();
47
+ cache.set(mockUrl, {
48
+ data: mockData,
49
+ timestamp: Date.now(),
50
+ });
51
+ (testDataProvider as any).cache = cache;
52
+
53
+ // Act
54
+ const result = await invokeFetchWithCache(testDataProvider, mockUrl);
55
+
56
+ // Assert
57
+ expect(result).toEqual(mockData);
58
+ expect(TFSServices.getItemContent).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it('should fetch new data when cache is expired', async () => {
62
+ // Arrange
63
+ const mockUrl = `${mockOrgUrl}_apis/test/endpoint`;
64
+ const mockData = { value: 'old data' };
65
+ const newData = { value: 'new data' };
66
+ const cache = new Map();
67
+ cache.set(mockUrl, {
68
+ data: mockData,
69
+ timestamp: Date.now() - 70000, // Expired (default TTL is 60000ms)
70
+ });
71
+ (testDataProvider as any).cache = cache;
72
+
73
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(newData);
74
+
75
+ // Act
76
+ const result = await invokeFetchWithCache(testDataProvider, mockUrl);
77
+
78
+ // Assert
79
+ expect(result).toEqual(newData);
80
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(mockUrl, mockToken);
81
+ });
82
+
83
+ it('should fetch and cache new data when not in cache', async () => {
84
+ // Arrange
85
+ const mockUrl = `${mockOrgUrl}_apis/test/endpoint`;
86
+ const mockData = { value: 'new data' };
87
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
88
+
89
+ // Act
90
+ const result = await invokeFetchWithCache(testDataProvider, mockUrl);
91
+
92
+ // Assert
93
+ expect(result).toEqual(mockData);
94
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(mockUrl, mockToken);
95
+ expect((testDataProvider as any).cache.has(mockUrl)).toBeTruthy();
96
+ expect((testDataProvider as any).cache.get(mockUrl).data).toEqual(mockData);
97
+ });
98
+
99
+ it('should throw and log error when fetch fails', async () => {
100
+ // Arrange
101
+ const mockUrl = `${mockOrgUrl}_apis/test/endpoint`;
102
+ const mockError = new Error('API call failed');
103
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
104
+
105
+ // Act & Assert
106
+ await expect(invokeFetchWithCache(testDataProvider, mockUrl)).rejects.toThrow('API call failed');
107
+ expect(logger.error).toHaveBeenCalledWith(`Error fetching ${mockUrl}: API call failed`);
108
+ });
109
+ });
110
+
111
+ describe('GetTestSuiteByTestCase', () => {
112
+ it('should return test suites for a given test case ID', async () => {
113
+ // Arrange
114
+ const mockData = { value: [{ id: '123', name: 'Test Suite 1' }] };
115
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
116
+
117
+ // Act
118
+ const result = await testDataProvider.GetTestSuiteByTestCase(mockTestCaseId);
119
+
120
+ // Assert
121
+ expect(result).toEqual(mockData);
122
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
123
+ `${mockOrgUrl}_apis/testplan/suites?testCaseId=${mockTestCaseId}`,
124
+ mockToken
125
+ );
126
+ });
127
+ });
128
+
129
+ describe('GetTestPlans', () => {
130
+ it('should return test plans for a given project', async () => {
131
+ // Arrange
132
+ const mockData = {
133
+ value: [
134
+ { id: '456', name: 'Test Plan 1' },
135
+ { id: '789', name: 'Test Plan 2' },
136
+ ],
137
+ };
138
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
139
+
140
+ // Act
141
+ const result = await testDataProvider.GetTestPlans(mockProject);
142
+
143
+ // Assert
144
+ expect(result).toEqual(mockData);
145
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
146
+ `${mockOrgUrl}${mockProject}/_apis/test/plans`,
147
+ mockToken
148
+ );
149
+ });
150
+ });
151
+
152
+ describe('GetTestSuites', () => {
153
+ it('should return test suites for a given project and plan ID', async () => {
154
+ // Arrange
155
+ const mockData = {
156
+ value: [
157
+ { id: '123', name: 'Test Suite 1' },
158
+ { id: '456', name: 'Test Suite 2' },
159
+ ],
160
+ };
161
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
162
+
163
+ // Act
164
+ const result = await testDataProvider.GetTestSuites(mockProject, mockPlanId);
165
+
166
+ // Assert
167
+ expect(result).toEqual(mockData);
168
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
169
+ `${mockOrgUrl}${mockProject}/_apis/test/Plans/${mockPlanId}/suites`,
170
+ mockToken
171
+ );
172
+ });
173
+
174
+ it('should return null and log error if fetching test suites fails', async () => {
175
+ // Arrange
176
+ const mockError = new Error('Failed to get test suites');
177
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
178
+
179
+ // Act
180
+ const result = await testDataProvider.GetTestSuites(mockProject, mockPlanId);
181
+
182
+ // Assert
183
+ expect(result).toBeNull();
184
+ });
185
+ });
186
+
187
+ describe('GetTestSuitesForPlan', () => {
188
+ it('should throw error when project is not provided', async () => {
189
+ // Act & Assert
190
+ await expect(testDataProvider.GetTestSuitesForPlan('', mockPlanId)).rejects.toThrow(
191
+ 'Project not selected'
192
+ );
193
+ });
194
+
195
+ it('should throw error when plan ID is not provided', async () => {
196
+ // Act & Assert
197
+ await expect(testDataProvider.GetTestSuitesForPlan(mockProject, '')).rejects.toThrow(
198
+ 'Plan not selected'
199
+ );
200
+ });
201
+
202
+ it('should return test suites for a plan', async () => {
203
+ // Arrange
204
+ const mockData = {
205
+ testSuites: [
206
+ { id: '123', name: 'Test Suite 1' },
207
+ { id: '456', name: 'Test Suite 2' },
208
+ ],
209
+ };
210
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
211
+
212
+ // Act
213
+ const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
214
+
215
+ // Assert
216
+ expect(result).toEqual(mockData);
217
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
218
+ `${mockOrgUrl}/${mockProject}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${mockPlanId}`,
219
+ mockToken
220
+ );
221
+ });
222
+ });
223
+
224
+ describe('GetTestSuiteById', () => {
225
+ it('should call GetTestSuitesForPlan and Helper.findSuitesRecursive with correct params', async () => {
226
+ // Arrange
227
+ const mockTestSuites = { testSuites: [{ id: '123', name: 'Test Suite 1' }] };
228
+ const mockSuiteData = [new suiteData('Test Suite 1', '123', '456', 1)];
229
+
230
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTestSuites);
231
+ (Helper.findSuitesRecursive as jest.Mock).mockReturnValueOnce(mockSuiteData);
232
+
233
+ // Act
234
+ const result = await testDataProvider.GetTestSuiteById(mockProject, mockPlanId, mockSuiteId, true);
235
+
236
+ // Assert
237
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
238
+ `${mockOrgUrl}/${mockProject}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${mockPlanId}`,
239
+ mockToken
240
+ );
241
+ expect(Helper.findSuitesRecursive).toHaveBeenCalledWith(
242
+ mockPlanId,
243
+ mockOrgUrl,
244
+ mockProject,
245
+ mockTestSuites.testSuites,
246
+ mockSuiteId,
247
+ true
248
+ );
249
+ expect(result).toEqual(mockSuiteData);
250
+ });
251
+ });
252
+
253
+ describe('GetTestCases', () => {
254
+ it('should return test cases for a given project, plan ID, and suite ID', async () => {
255
+ // Arrange
256
+ const mockData = {
257
+ count: 2,
258
+ value: [
259
+ { testCase: { id: '101', name: 'Test Case 1', url: 'url1' } },
260
+ { testCase: { id: '102', name: 'Test Case 2', url: 'url2' } },
261
+ ],
262
+ };
263
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
264
+
265
+ // Act
266
+ const result = await testDataProvider.GetTestCases(mockProject, mockPlanId, mockSuiteId);
267
+
268
+ // Assert
269
+ expect(result).toEqual(mockData);
270
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
271
+ `${mockOrgUrl}${mockProject}/_apis/test/Plans/${mockPlanId}/suites/${mockSuiteId}/testcases/`,
272
+ mockToken
273
+ );
274
+ expect(logger.debug).toHaveBeenCalledWith(
275
+ `test cases for plan ${mockPlanId} and ${mockSuiteId} were found`
276
+ );
277
+ });
278
+ });
279
+
280
+ describe('clearCache', () => {
281
+ it('should clear the cache', () => {
282
+ // Arrange
283
+ const mockUrl = `${mockOrgUrl}_apis/test/endpoint`;
284
+ const mockData = { value: 'test data' };
285
+ const cache = new Map();
286
+ cache.set(mockUrl, {
287
+ data: mockData,
288
+ timestamp: Date.now(),
289
+ });
290
+ (testDataProvider as any).cache = cache;
291
+
292
+ // Act
293
+ testDataProvider.clearCache();
294
+
295
+ // Assert
296
+ expect((testDataProvider as any).cache.size).toBe(0);
297
+ expect(logger.debug).toHaveBeenCalledWith('Cache cleared');
298
+ });
299
+ });
300
+
301
+ describe('UpdateTestRun', () => {
302
+ it('should update a test run with the correct state', async () => {
303
+ // Arrange
304
+ const mockRunId = '12345';
305
+ const mockState = 'Completed';
306
+ const mockResponse = { id: mockRunId, state: mockState };
307
+
308
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
309
+
310
+ // Act
311
+ const result = await testDataProvider.UpdateTestRun(mockProject, mockRunId, mockState);
312
+
313
+ // Assert
314
+ expect(result).toEqual(mockResponse);
315
+ expect(TFSServices.postRequest).toHaveBeenCalledWith(
316
+ `${mockOrgUrl}${mockProject}/_apis/test/Runs/${mockRunId}?api-version=5.0`,
317
+ mockToken,
318
+ 'PATCH',
319
+ { state: mockState },
320
+ null
321
+ );
322
+ expect(logger.info).toHaveBeenCalledWith(`Update runId : ${mockRunId} to state : ${mockState}`);
323
+ });
324
+ });
325
+
326
+ describe('GetTestSuitesByPlan', () => {
327
+ it('should return suites without filter using default suiteId', async () => {
328
+ // Arrange
329
+ const mockTestSuites = { testSuites: [{ id: 457, name: 'Suite 1', parentSuiteId: 0 }] };
330
+ const mockSuiteData = [new suiteData('Suite 1', '457', '456', 1)];
331
+
332
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValue(mockTestSuites);
333
+ (Helper.findSuitesRecursive as jest.Mock).mockReturnValue(mockSuiteData);
334
+
335
+ // Act
336
+ const result = await testDataProvider.GetTestSuitesByPlan(mockProject, mockPlanId, true);
337
+
338
+ // Assert
339
+ expect(Helper.findSuitesRecursive).toHaveBeenCalled();
340
+ });
341
+
342
+ it('should process multiple top-level suite hierarchies and combine results', async () => {
343
+ jest.spyOn(testDataProvider, 'GetTestSuitesForPlan').mockResolvedValueOnce({
344
+ testSuites: [
345
+ { id: 1, parentSuiteId: 0 },
346
+ { id: 2, parentSuiteId: 0 },
347
+ { id: 10, parentSuiteId: 1 },
348
+ { id: 11, parentSuiteId: 2 },
349
+ ],
350
+ } as any);
351
+
352
+ const suiteIdsFilter = [10, 11];
353
+ const getByIdSpy = jest
354
+ .spyOn(testDataProvider, 'GetTestSuiteById')
355
+ .mockResolvedValueOnce([{ id: '10' }])
356
+ .mockResolvedValueOnce([{ id: '11' }]);
357
+
358
+ const res = await testDataProvider.GetTestSuitesByPlan(mockProject, mockPlanId, true, suiteIdsFilter);
359
+
360
+ expect(getByIdSpy).toHaveBeenCalledTimes(2);
361
+ expect(res).toEqual([{ id: '10' }, { id: '11' }]);
362
+ });
363
+
364
+ it('should fallback to first suite when no top-level suites can be determined', async () => {
365
+ jest.spyOn(testDataProvider, 'GetTestSuitesForPlan').mockResolvedValueOnce({
366
+ testSuites: [{ id: 1, parentSuiteId: 0 }],
367
+ } as any);
368
+
369
+ const getByIdSpy = jest
370
+ .spyOn(testDataProvider, 'GetTestSuiteById')
371
+ .mockResolvedValueOnce([{ id: '1' }]);
372
+
373
+ const res = await testDataProvider.GetTestSuitesByPlan(mockProject, mockPlanId, true, [1]);
374
+
375
+ expect(getByIdSpy).toHaveBeenCalledWith(mockProject, mockPlanId, '1', true, [1]);
376
+ expect(res).toEqual([{ id: '1' }]);
377
+ });
378
+ });
379
+
380
+ describe('createNewRequirement', () => {
381
+ it('should pick customer id from any supported customer fields when enabled', () => {
382
+ const rel = (testDataProvider as any).createNewRequirement(true, {
383
+ id: '123',
384
+ fields: {
385
+ 'System.Title': 'Req title',
386
+ 'Custom.CustomerID': 'CID-1',
387
+ },
388
+ });
389
+
390
+ expect(rel).toEqual({ type: 'requirement', id: '123', title: 'Req title', customerId: 'CID-1' });
391
+ });
392
+
393
+ it('should default customerId to a single space when enabled and no field exists', () => {
394
+ const rel = (testDataProvider as any).createNewRequirement(true, {
395
+ id: '123',
396
+ fields: {
397
+ 'System.Title': 'Req title',
398
+ },
399
+ });
400
+
401
+ expect(rel).toEqual({ type: 'requirement', id: '123', title: 'Req title', customerId: ' ' });
402
+ });
403
+ });
404
+
405
+ describe('addToMap', () => {
406
+ it('should create array for missing key and append values', () => {
407
+ const map = new Map<string, string[]>();
408
+ (testDataProvider as any).addToMap(map, 'k', 'v1');
409
+ (testDataProvider as any).addToMap(map, 'k', 'v2');
410
+ expect(map.get('k')).toEqual(['v1', 'v2']);
411
+ });
412
+ });
413
+
414
+ describe('GetTestSuiteById with filtering', () => {
415
+ it('should filter suites when suiteIdsFilter is provided', async () => {
416
+ // Arrange
417
+ const suiteIdsFilter = [123, 456];
418
+ const mockTestSuites = {
419
+ testSuites: [
420
+ { id: 123, name: 'Suite 1', parentSuiteId: 100 },
421
+ { id: 456, name: 'Suite 2', parentSuiteId: 100 },
422
+ ],
423
+ };
424
+ const mockSuiteData = [new suiteData('Suite 1', '123', '100', 1)];
425
+
426
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValue(mockTestSuites);
427
+ (Helper.findSuitesRecursive as jest.Mock).mockReturnValue(mockSuiteData);
428
+
429
+ // Act
430
+ const result = await testDataProvider.GetTestSuiteById(
431
+ mockProject,
432
+ mockPlanId,
433
+ '123',
434
+ true,
435
+ suiteIdsFilter
436
+ );
437
+
438
+ // Assert
439
+ expect(Helper.findSuitesRecursive).toHaveBeenCalled();
440
+ });
441
+ });
442
+
443
+ describe('GetTestPoint', () => {
444
+ it('should return test points for a test case', async () => {
445
+ // Arrange
446
+ const mockResponse = { value: [{ id: 1, testCaseId: mockTestCaseId }] };
447
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValue(mockResponse);
448
+
449
+ // Act
450
+ const result = await testDataProvider.GetTestPoint(
451
+ mockProject,
452
+ mockPlanId,
453
+ mockSuiteId,
454
+ mockTestCaseId
455
+ );
456
+
457
+ // Assert
458
+ expect(result).toEqual(mockResponse);
459
+ });
460
+ });
461
+
462
+ describe('CreateTestRun', () => {
463
+ it('should create a test run successfully', async () => {
464
+ // Arrange
465
+ const testRunName = 'Test Run 1';
466
+ const testPointId = '12345';
467
+ const mockResponse = { data: { id: 1, name: testRunName } };
468
+
469
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
470
+
471
+ // Act
472
+ const result = await testDataProvider.CreateTestRun(mockProject, testRunName, mockPlanId, testPointId);
473
+
474
+ // Assert
475
+ expect(result).toEqual(mockResponse);
476
+ expect(TFSServices.postRequest).toHaveBeenCalledWith(
477
+ `${mockOrgUrl}${mockProject}/_apis/test/runs`,
478
+ mockToken,
479
+ 'Post',
480
+ {
481
+ name: testRunName,
482
+ plan: { id: mockPlanId },
483
+ pointIds: [testPointId],
484
+ },
485
+ null
486
+ );
487
+ });
488
+
489
+ it('should throw error when creation fails', async () => {
490
+ // Arrange
491
+ const testRunName = 'Test Run 1';
492
+ const testPointId = '12345';
493
+ const mockError = new Error('Creation failed');
494
+
495
+ (TFSServices.postRequest as jest.Mock).mockRejectedValueOnce(mockError);
496
+
497
+ // Act & Assert
498
+ await expect(
499
+ testDataProvider.CreateTestRun(mockProject, testRunName, mockPlanId, testPointId)
500
+ ).rejects.toThrow('Error: Creation failed');
501
+ });
502
+ });
503
+
504
+ describe('UpdateTestCase', () => {
505
+ it('should update test case to Active state (0)', async () => {
506
+ // Arrange
507
+ const mockRunId = '12345';
508
+ const mockResponse = { data: { outcome: '0' } };
509
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
510
+
511
+ // Act
512
+ const result = await testDataProvider.UpdateTestCase(mockProject, mockRunId, 0);
513
+
514
+ // Assert
515
+ expect(result).toEqual(mockResponse);
516
+ expect(logger.info).toHaveBeenCalledWith('Reset test case to Active state ');
517
+ });
518
+
519
+ it('should update test case to Completed state (1)', async () => {
520
+ // Arrange
521
+ const mockRunId = '12345';
522
+ const mockResponse = { data: { state: 'Completed', outcome: '1' } };
523
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
524
+
525
+ // Act
526
+ const result = await testDataProvider.UpdateTestCase(mockProject, mockRunId, 1);
527
+
528
+ // Assert
529
+ expect(result).toEqual(mockResponse);
530
+ expect(logger.info).toHaveBeenCalledWith('Update test case to complite state ');
531
+ });
532
+
533
+ it('should update test case to Passed state (2)', async () => {
534
+ // Arrange
535
+ const mockRunId = '12345';
536
+ const mockResponse = { data: { state: 'Completed', outcome: '2' } };
537
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
538
+
539
+ // Act
540
+ const result = await testDataProvider.UpdateTestCase(mockProject, mockRunId, 2);
541
+
542
+ // Assert
543
+ expect(result).toEqual(mockResponse);
544
+ expect(logger.info).toHaveBeenCalledWith('Update test case to passed state ');
545
+ });
546
+
547
+ it('should update test case to Failed state (3)', async () => {
548
+ // Arrange
549
+ const mockRunId = '12345';
550
+ const mockResponse = { data: { state: 'Completed', outcome: '3' } };
551
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
552
+
553
+ // Act
554
+ const result = await testDataProvider.UpdateTestCase(mockProject, mockRunId, 3);
555
+
556
+ // Assert
557
+ expect(result).toEqual(mockResponse);
558
+ expect(logger.info).toHaveBeenCalledWith('Update test case to failed state ');
559
+ });
560
+ });
561
+
562
+ describe('UploadTestAttachment', () => {
563
+ it('should upload attachment to test run', async () => {
564
+ // Arrange
565
+ const runId = '12345';
566
+ const stream = 'base64encodeddata';
567
+ const fileName = 'test.png';
568
+ const comment = 'Test attachment';
569
+ const attachmentType = 'GeneralAttachment';
570
+ const mockResponse = { data: { id: 1 } };
571
+
572
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
573
+
574
+ // Act
575
+ const result = await testDataProvider.UploadTestAttachment(
576
+ runId,
577
+ mockProject,
578
+ stream,
579
+ fileName,
580
+ comment,
581
+ attachmentType
582
+ );
583
+
584
+ // Assert
585
+ expect(result).toEqual(mockResponse);
586
+ expect(TFSServices.postRequest).toHaveBeenCalledWith(
587
+ `${mockOrgUrl}${mockProject}/_apis/test/Runs/${runId}/attachments?api-version=5.0-preview.1`,
588
+ mockToken,
589
+ 'Post',
590
+ { stream, fileName, comment, attachmentType },
591
+ null
592
+ );
593
+ });
594
+ });
595
+
596
+ describe('GetTestRunById', () => {
597
+ it('should return test run by ID', async () => {
598
+ // Arrange
599
+ const runId = '12345';
600
+ const mockResponse = { id: runId, name: 'Test Run 1' };
601
+
602
+ // Act
603
+ const result = await testDataProvider.GetTestRunById(mockProject, runId);
604
+
605
+ // Assert
606
+ expect(TFSServices.getItemContent).toHaveBeenCalled();
607
+ });
608
+ });
609
+
610
+ describe('GetTestPointByTestCaseId', () => {
611
+ it('should return test points for a test case ID', async () => {
612
+ // Arrange
613
+ const mockResponse = { data: { value: [{ id: 1 }] } };
614
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
615
+
616
+ // Act
617
+ const result = await testDataProvider.GetTestPointByTestCaseId(mockProject, mockTestCaseId);
618
+
619
+ // Assert
620
+ expect(result).toEqual(mockResponse);
621
+ expect(TFSServices.postRequest).toHaveBeenCalledWith(
622
+ `${mockOrgUrl}${mockProject}/_apis/test/points`,
623
+ mockToken,
624
+ 'Post',
625
+ { PointsFilter: { TestcaseIds: [mockTestCaseId] } },
626
+ null
627
+ );
628
+ });
629
+ });
630
+
631
+ describe('GetTestCasesBySuites', () => {
632
+ it('should return test cases for suites', async () => {
633
+ // Arrange
634
+ const mockSuiteData = [new suiteData('Suite 1', '123', '456', 1)];
635
+ const mockTestCases = {
636
+ count: 1,
637
+ value: [{ testCase: { id: '101', url: 'http://test.com/101' } }],
638
+ };
639
+ const mockTestCaseDetails = {
640
+ id: 101,
641
+ fields: {
642
+ 'System.Title': 'Test Case 1',
643
+ 'System.AreaPath': 'Area/Path',
644
+ 'System.Description': 'Description',
645
+ 'Microsoft.VSTS.TCM.Steps': null,
646
+ },
647
+ };
648
+
649
+ (TFSServices.getItemContent as jest.Mock)
650
+ .mockResolvedValueOnce({ testSuites: mockSuiteData })
651
+ .mockResolvedValueOnce(mockTestCases)
652
+ .mockResolvedValueOnce(mockTestCaseDetails);
653
+ (Helper.findSuitesRecursive as jest.Mock).mockReturnValueOnce(mockSuiteData);
654
+
655
+ // Act
656
+ const result = await testDataProvider.GetTestCasesBySuites(
657
+ mockProject,
658
+ mockPlanId,
659
+ mockSuiteId,
660
+ false,
661
+ false,
662
+ false,
663
+ false
664
+ );
665
+
666
+ // Assert
667
+ expect(result.testCasesList).toBeDefined();
668
+ expect(result.requirementToTestCaseTraceMap).toBeDefined();
669
+ expect(result.testCaseToRequirementsTraceMap).toBeDefined();
670
+ });
671
+
672
+ it('should use pre-filtered suites when provided', async () => {
673
+ // Arrange
674
+ const preFilteredSuites = [new suiteData('Suite 1', '123', '456', 1)];
675
+ const mockTestCases = {
676
+ count: 1,
677
+ value: [{ testCase: { id: '101', url: 'http://test.com/101' } }],
678
+ };
679
+ const mockTestCaseDetails = {
680
+ id: 101,
681
+ fields: {
682
+ 'System.Title': 'Test Case 1',
683
+ 'System.AreaPath': 'Area/Path',
684
+ 'System.Description': 'Description',
685
+ 'Microsoft.VSTS.TCM.Steps': null,
686
+ },
687
+ };
688
+
689
+ (TFSServices.getItemContent as jest.Mock)
690
+ .mockResolvedValueOnce(mockTestCases)
691
+ .mockResolvedValueOnce(mockTestCaseDetails);
692
+
693
+ // Act
694
+ const result = await testDataProvider.GetTestCasesBySuites(
695
+ mockProject,
696
+ mockPlanId,
697
+ mockSuiteId,
698
+ false,
699
+ false,
700
+ false,
701
+ false,
702
+ undefined,
703
+ undefined,
704
+ preFilteredSuites
705
+ );
706
+
707
+ // Assert
708
+ expect(result.testCasesList).toBeDefined();
709
+ });
710
+
711
+ it('should handle errors in suite processing', async () => {
712
+ // Arrange
713
+ const preFilteredSuites = [new suiteData('Suite 1', '123', '456', 1)];
714
+
715
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
716
+
717
+ // Act
718
+ const result = await testDataProvider.GetTestCasesBySuites(
719
+ mockProject,
720
+ mockPlanId,
721
+ mockSuiteId,
722
+ false,
723
+ false,
724
+ false,
725
+ false,
726
+ undefined,
727
+ undefined,
728
+ preFilteredSuites
729
+ );
730
+
731
+ // Assert
732
+ expect(result.testCasesList).toEqual([]);
733
+ });
734
+ });
735
+
736
+ describe('StructureTestCase', () => {
737
+ it('should return empty array when no test cases', async () => {
738
+ // Arrange
739
+ const suite = new suiteData('Suite 1', '123', '456', 1);
740
+ const testCases = { count: 0, value: [] };
741
+
742
+ // Act
743
+ const result = await testDataProvider.StructureTestCase(
744
+ mockProject,
745
+ testCases,
746
+ suite,
747
+ false,
748
+ false,
749
+ false,
750
+ new Map(),
751
+ new Map()
752
+ );
753
+
754
+ // Assert
755
+ expect(result).toEqual([]);
756
+ expect(logger.warn).toHaveBeenCalled();
757
+ });
758
+
759
+ it('should warn and return [] when no testCases are provided', async () => {
760
+ const res = await testDataProvider.StructureTestCase(
761
+ mockProject,
762
+ { value: [], count: 0 } as any,
763
+ { id: '1', name: 'Suite 1' } as any,
764
+ true,
765
+ true,
766
+ false,
767
+ new Map<string, string[]>(),
768
+ new Map<string, string[]>()
769
+ );
770
+
771
+ expect(res).toEqual([]);
772
+ expect(logger.warn).toHaveBeenCalledWith('No test cases found for suite: 1');
773
+ });
774
+
775
+ it('should parse steps, add requirement relation when enabled, and add mom relation when enabled', async () => {
776
+ const UtilsMock: any = require('../../utils/testStepParserHelper').default;
777
+ const parserInstance = UtilsMock.mock.results[0].value;
778
+ parserInstance.parseTestSteps.mockResolvedValueOnce([{ stepId: '1' }]);
779
+
780
+ const suite = { id: '1', name: 'Suite 1' } as any;
781
+ const testCases = {
782
+ count: 1,
783
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
784
+ };
785
+
786
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockImplementation(async (...args: any[]) => {
787
+ const url = String(args[0] || '');
788
+ if (url.includes('testcase/123') && url.includes('?$expand=All')) {
789
+ return {
790
+ id: 123,
791
+ fields: {
792
+ 'System.Title': 'TC 123',
793
+ 'System.AreaPath': 'A',
794
+ 'System.Description': 'D',
795
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
796
+ },
797
+ relations: [
798
+ { url: 'https://example.com/_apis/wit/workItems/200' },
799
+ { url: 'https://example.com/not-work-items/ignore' },
800
+ { url: 'https://example.com/_apis/wit/workItems/201' },
801
+ ],
802
+ };
803
+ }
804
+ if (url.includes('/workItems/200')) {
805
+ return {
806
+ id: 200,
807
+ fields: {
808
+ 'System.WorkItemType': 'Requirement',
809
+ 'System.Title': 'REQ 200',
810
+ 'Custom.CustomerRequirementId': 'C-200',
811
+ },
812
+ _links: { html: { href: 'http://example.com/200' } },
813
+ };
814
+ }
815
+ if (url.includes('/workItems/201')) {
816
+ return {
817
+ id: 201,
818
+ fields: {
819
+ 'System.WorkItemType': 'Bug',
820
+ 'System.Title': 'BUG 201',
821
+ 'System.State': 'Active',
822
+ },
823
+ _links: { html: { href: 'http://example.com/201' } },
824
+ };
825
+ }
826
+ throw new Error(`unexpected url ${url}`);
827
+ });
828
+
829
+ const requirementToTestCaseTraceMap = new Map<string, string[]>();
830
+ const testCaseToRequirementsTraceMap = new Map<string, string[]>();
831
+
832
+ const res = await testDataProvider.StructureTestCase(
833
+ mockProject,
834
+ testCases as any,
835
+ suite,
836
+ true,
837
+ true,
838
+ true,
839
+ requirementToTestCaseTraceMap,
840
+ testCaseToRequirementsTraceMap
841
+ );
842
+
843
+ expect(res).toHaveLength(1);
844
+ expect(res[0].steps).toEqual([{ stepId: '1' }]);
845
+ expect(res[0].relations).toEqual(
846
+ expect.arrayContaining([
847
+ expect.objectContaining({ type: 'requirement', id: 200, customerId: 'C-200' }),
848
+ ])
849
+ );
850
+ expect(res[0].relations).toEqual(
851
+ expect.arrayContaining([expect.objectContaining({ type: 'Bug', id: 201 })])
852
+ );
853
+ expect(requirementToTestCaseTraceMap.size).toBe(1);
854
+ expect(testCaseToRequirementsTraceMap.size).toBe(1);
855
+ });
856
+
857
+ it('should use stepResultDetailsMap when provided and skip parseTestSteps', async () => {
858
+ const UtilsMock: any = require('../../utils/testStepParserHelper').default;
859
+ const parserInstance = UtilsMock.mock.results[0].value;
860
+ parserInstance.parseTestSteps.mockClear();
861
+
862
+ const suite = { id: '1', name: 'Suite 1' } as any;
863
+ const testCases = {
864
+ count: 1,
865
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
866
+ };
867
+
868
+ const stepResultDetailsMap = new Map<string, any>();
869
+ stepResultDetailsMap.set('123', {
870
+ testCaseRevision: 7,
871
+ stepList: [{ stepId: 'from-cache' }],
872
+ caseEvidenceAttachments: [{ id: 1 }],
873
+ });
874
+
875
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockResolvedValueOnce({
876
+ id: 123,
877
+ fields: {
878
+ 'System.Title': 'TC 123',
879
+ 'System.AreaPath': 'A',
880
+ 'System.Description': 'D',
881
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
882
+ },
883
+ relations: [],
884
+ });
885
+
886
+ const res = await testDataProvider.StructureTestCase(
887
+ mockProject,
888
+ testCases as any,
889
+ suite,
890
+ true,
891
+ false,
892
+ false,
893
+ new Map<string, string[]>(),
894
+ new Map<string, string[]>(),
895
+ stepResultDetailsMap
896
+ );
897
+
898
+ expect(res).toHaveLength(1);
899
+ expect(res[0].steps).toEqual([{ stepId: 'from-cache' }]);
900
+ expect(res[0].caseEvidenceAttachments).toEqual([{ id: 1 }]);
901
+ expect(parserInstance.parseTestSteps).not.toHaveBeenCalled();
902
+ });
903
+
904
+ it('should log error and continue when fetching relation content fails', async () => {
905
+ const suite = { id: '1', name: 'Suite 1' } as any;
906
+ const testCases = {
907
+ count: 1,
908
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
909
+ };
910
+
911
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockImplementation(async (...args: any[]) => {
912
+ const url = String(args[0] || '');
913
+ if (url.includes('testcase/123') && url.includes('?$expand=All')) {
914
+ return {
915
+ id: 123,
916
+ fields: {
917
+ 'System.Title': 'TC 123',
918
+ 'System.AreaPath': 'A',
919
+ 'System.Description': 'D',
920
+ 'Microsoft.VSTS.TCM.Steps': null,
921
+ },
922
+ relations: [{ url: 'https://example.com/_apis/wit/workItems/200' }],
923
+ };
924
+ }
925
+ if (url.includes('/workItems/200')) {
926
+ throw new Error('boom');
927
+ }
928
+ return null;
929
+ });
930
+
931
+ const res = await testDataProvider.StructureTestCase(
932
+ mockProject,
933
+ testCases as any,
934
+ suite,
935
+ true,
936
+ true,
937
+ false,
938
+ new Map<string, string[]>(),
939
+ new Map<string, string[]>()
940
+ );
941
+
942
+ expect(res).toHaveLength(1);
943
+ expect(logger.error).toHaveBeenCalledWith(
944
+ expect.stringContaining(
945
+ 'Failed to fetch relation content for URL https://example.com/_apis/wit/workItems/200'
946
+ )
947
+ );
948
+ });
949
+
950
+ it('should append linked MOMs from testCaseToLinkedMomLookup when provided', async () => {
951
+ const suite = { id: '1', name: 'Suite 1' } as any;
952
+ const testCases = {
953
+ count: 1,
954
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
955
+ };
956
+
957
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockResolvedValueOnce({
958
+ id: 123,
959
+ fields: {
960
+ 'System.Title': 'TC 123',
961
+ 'System.AreaPath': 'A',
962
+ 'System.Description': 'D',
963
+ 'Microsoft.VSTS.TCM.Steps': null,
964
+ },
965
+ relations: [],
966
+ });
967
+
968
+ const testCaseToLinkedMomLookup = new Map<number, Set<any>>();
969
+ testCaseToLinkedMomLookup.set(
970
+ 123,
971
+ new Set([
972
+ {
973
+ id: 900,
974
+ fields: {
975
+ 'System.WorkItemType': 'Task',
976
+ 'System.Title': 'Task 900',
977
+ 'System.State': 'Active',
978
+ },
979
+ _links: { html: { href: 'http://example.com/900' } },
980
+ },
981
+ ])
982
+ );
983
+
984
+ const res = await testDataProvider.StructureTestCase(
985
+ mockProject,
986
+ testCases as any,
987
+ suite,
988
+ false,
989
+ false,
990
+ false,
991
+ new Map<string, string[]>(),
992
+ new Map<string, string[]>(),
993
+ undefined,
994
+ testCaseToLinkedMomLookup
995
+ );
996
+
997
+ expect(res).toHaveLength(1);
998
+ expect(res[0].relations).toEqual(
999
+ expect.arrayContaining([expect.objectContaining({ type: 'Task', id: 900 })])
1000
+ );
1001
+ });
1002
+
1003
+ it('should return empty list when test case fetch fails', async () => {
1004
+ const suite = { id: '1', name: 'Suite 1' } as any;
1005
+ const testCases = {
1006
+ count: 1,
1007
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
1008
+ };
1009
+
1010
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockRejectedValueOnce(new Error('boom'));
1011
+
1012
+ const res = await testDataProvider.StructureTestCase(
1013
+ mockProject,
1014
+ testCases as any,
1015
+ suite,
1016
+ false,
1017
+ false,
1018
+ false,
1019
+ new Map<string, string[]>(),
1020
+ new Map<string, string[]>()
1021
+ );
1022
+
1023
+ expect(res).toEqual([]);
1024
+ expect(logger.error).toHaveBeenCalledWith('Error: ran into an issue while retrieving testCase 123');
1025
+ });
1026
+ });
1027
+
1028
+ describe('ParseSteps', () => {
1029
+ it('should parse XML steps correctly', () => {
1030
+ // Arrange
1031
+ const stepsXml = `<steps>
1032
+ <step>
1033
+ <parameterizedString>Action 1</parameterizedString>
1034
+ <parameterizedString>Expected 1</parameterizedString>
1035
+ </step>
1036
+ </steps>`;
1037
+
1038
+ // Act
1039
+ const result = testDataProvider.ParseSteps(stepsXml);
1040
+
1041
+ // Assert - ParseSteps uses xml2js.parseString which is synchronous callback-based
1042
+ // The result may be empty if parsing fails or steps format doesn't match
1043
+ expect(Array.isArray(result)).toBe(true);
1044
+ });
1045
+ });
1046
+ });