@elisra-devops/docgen-data-provider 1.63.13 → 1.68.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 (92) hide show
  1. package/.github/workflows/ci.yml +26 -9
  2. package/.github/workflows/release.yml +9 -10
  3. package/README.md +50 -24
  4. package/bin/helpers/tfs.d.ts +3 -0
  5. package/bin/helpers/tfs.js +44 -7
  6. package/bin/helpers/tfs.js.map +1 -1
  7. package/bin/modules/GitDataProvider.d.ts +10 -0
  8. package/bin/modules/GitDataProvider.js +10 -0
  9. package/bin/modules/GitDataProvider.js.map +1 -1
  10. package/bin/modules/TestDataProvider.js +0 -1
  11. package/bin/modules/TestDataProvider.js.map +1 -1
  12. package/bin/modules/TicketsDataProvider.d.ts +63 -24
  13. package/bin/modules/TicketsDataProvider.js +216 -114
  14. package/bin/modules/TicketsDataProvider.js.map +1 -1
  15. package/bin/tests/helpers/helper.test.js +279 -0
  16. package/bin/tests/helpers/helper.test.js.map +1 -0
  17. package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
  18. package/bin/tests/helpers/tfs.test.js.map +1 -0
  19. package/bin/tests/index.test.js +25 -0
  20. package/bin/tests/index.test.js.map +1 -0
  21. package/bin/tests/models/tfs-data.test.js +160 -0
  22. package/bin/tests/models/tfs-data.test.js.map +1 -0
  23. package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
  24. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
  25. package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
  26. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
  27. package/bin/tests/modules/gitDataProvider.test.js +1888 -0
  28. package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
  29. package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +13 -1
  30. package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
  31. package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
  32. package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
  33. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
  34. package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
  35. package/bin/tests/modules/testDataProvider.test.js +717 -0
  36. package/bin/tests/modules/testDataProvider.test.js.map +1 -0
  37. package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
  38. package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
  39. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
  40. package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
  41. package/bin/tests/utils/DataProviderUtils.test.js +61 -0
  42. package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
  43. package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
  44. package/bin/tests/utils/testStepParserHelper.test.js +359 -0
  45. package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
  46. package/package.json +9 -1
  47. package/src/helpers/tfs.ts +51 -7
  48. package/src/modules/GitDataProvider.ts +10 -0
  49. package/src/modules/TestDataProvider.ts +0 -1
  50. package/src/modules/TicketsDataProvider.ts +298 -141
  51. package/src/tests/helpers/helper.test.ts +337 -0
  52. package/src/tests/helpers/tfs.test.ts +1092 -0
  53. package/src/tests/index.test.ts +28 -0
  54. package/src/tests/models/tfs-data.test.ts +203 -0
  55. package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
  56. package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
  57. package/src/tests/modules/gitDataProvider.test.ts +2628 -0
  58. package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +33 -1
  59. package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
  60. package/src/tests/modules/testDataProvider.test.ts +1046 -0
  61. package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
  62. package/src/tests/utils/DataProviderUtils.test.ts +76 -0
  63. package/src/tests/utils/testStepParserHelper.test.ts +437 -0
  64. package/tsconfig.json +1 -0
  65. package/bin/helpers/test/tfs.test.js.map +0 -1
  66. package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
  67. package/bin/modules/test/ResultDataProvider.test.js +0 -444
  68. package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
  69. package/bin/modules/test/gitDataProvider.test.js +0 -428
  70. package/bin/modules/test/gitDataProvider.test.js.map +0 -1
  71. package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
  72. package/bin/modules/test/pipelineDataProvider.test.js +0 -237
  73. package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
  74. package/bin/modules/test/testDataProvider.test.js +0 -234
  75. package/bin/modules/test/testDataProvider.test.js.map +0 -1
  76. package/bin/modules/test/ticketsDataProvider.test.js +0 -348
  77. package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
  78. package/src/helpers/test/tfs.test.ts +0 -748
  79. package/src/modules/test/JfrogDataProvider.test.ts +0 -171
  80. package/src/modules/test/ResultDataProvider.test.ts +0 -542
  81. package/src/modules/test/gitDataProvider.test.ts +0 -645
  82. package/src/modules/test/pipelineDataProvider.test.ts +0 -292
  83. package/src/modules/test/testDataProvider.test.ts +0 -318
  84. package/src/modules/test/ticketsDataProvider.test.ts +0 -462
  85. /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
  86. /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
  87. /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
  88. /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
  89. /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
  90. /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
  91. /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
  92. /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
@@ -0,0 +1,1092 @@
1
+ // First, mock axios BEFORE importing TFSServices
2
+ import axios from 'axios';
3
+ jest.mock('axios');
4
+
5
+ // Set up the mock axios instance that axios.create will return
6
+ const mockAxiosInstance = {
7
+ request: jest.fn(),
8
+ };
9
+ (axios.create as jest.Mock).mockReturnValue(mockAxiosInstance);
10
+
11
+ // NOW import TFSServices (it will use our mock)
12
+ import { TFSServices } from '../../helpers/tfs';
13
+ import logger from '../../utils/logger';
14
+
15
+ // Mock logger
16
+ jest.mock('../../utils/logger');
17
+
18
+ describe('TFSServices', () => {
19
+ // Store the original implementation of random to restore it later
20
+ const originalRandom = Math.random;
21
+
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ // Mock Math.random to return a predictable value for tests with retry
25
+ Math.random = jest.fn().mockReturnValue(0.5);
26
+ });
27
+
28
+ afterEach(() => {
29
+ // Restore the original Math.random implementation
30
+ Math.random = originalRandom;
31
+ });
32
+
33
+ describe('downloadZipFile', () => {
34
+ it('should download a zip file successfully', async () => {
35
+ // Arrange
36
+ const url = 'https://example.com/file.zip';
37
+ const pat = 'token123';
38
+ const mockResponse = { data: Buffer.from('zip-file-content') };
39
+
40
+ // Configure mock response
41
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
42
+
43
+ // Act
44
+ const result = await TFSServices.downloadZipFile(url, pat);
45
+
46
+ // Assert
47
+ expect(result).toEqual(mockResponse);
48
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
49
+ url,
50
+ headers: { 'Content-Type': 'application/zip' },
51
+ auth: { username: '', password: pat },
52
+ });
53
+ });
54
+
55
+ it('should use bearer Authorization header when token is bearer-prefixed', async () => {
56
+ // Arrange
57
+ const url = 'https://example.com/file.zip';
58
+ const token = 'bearer:abc123';
59
+ const mockResponse = { data: Buffer.from('zip-file-content') };
60
+
61
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
62
+
63
+ // Act
64
+ const result = await TFSServices.downloadZipFile(url, token);
65
+
66
+ // Assert
67
+ expect(result).toEqual(mockResponse);
68
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
69
+ url,
70
+ headers: { 'Content-Type': 'application/zip', Authorization: 'Bearer abc123' },
71
+ });
72
+ });
73
+
74
+ it('should log and throw error when download fails', async () => {
75
+ // Arrange
76
+ const url = 'https://example.com/file.zip';
77
+ const pat = 'token123';
78
+ const mockError = new Error('Network error');
79
+
80
+ // Configure mock to throw error
81
+ mockAxiosInstance.request.mockRejectedValueOnce(mockError);
82
+
83
+ // Act & Assert
84
+ await expect(TFSServices.downloadZipFile(url, pat)).rejects.toThrow();
85
+ expect(logger.error).toHaveBeenCalledWith(`error download zip file , url : ${url}`);
86
+ });
87
+ });
88
+
89
+ describe('fetchAzureDevOpsImageAsBase64', () => {
90
+ it('should fetch and convert image to base64', async () => {
91
+ // Arrange
92
+ const url = 'https://example.com/image.png';
93
+ const pat = 'token123';
94
+ const mockResponse = {
95
+ data: Buffer.from('image-data'),
96
+ headers: { 'content-type': 'image/png' },
97
+ };
98
+
99
+ // Configure mock response
100
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
101
+
102
+ // Act
103
+ const result = await TFSServices.fetchAzureDevOpsImageAsBase64(url, pat);
104
+
105
+ // Assert
106
+ expect(result).toEqual('data:image/png;base64,aW1hZ2UtZGF0YQ==');
107
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
108
+ expect.objectContaining({
109
+ url,
110
+ method: 'get',
111
+ auth: { username: '', password: pat },
112
+ responseType: 'arraybuffer',
113
+ })
114
+ );
115
+ });
116
+
117
+ it('should send bearer Authorization header when bearer token is provided', async () => {
118
+ // Arrange
119
+ const url = 'https://example.com/image.png';
120
+ const token = 'bearer:abc123';
121
+ const mockResponse = {
122
+ data: Buffer.from('image-data'),
123
+ headers: { 'content-type': 'image/png' },
124
+ };
125
+
126
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
127
+
128
+ // Act
129
+ const result = await TFSServices.fetchAzureDevOpsImageAsBase64(url, token);
130
+
131
+ // Assert
132
+ expect(result).toEqual('data:image/png;base64,aW1hZ2UtZGF0YQ==');
133
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ url,
136
+ method: 'get',
137
+ responseType: 'arraybuffer',
138
+ headers: expect.objectContaining({ Authorization: 'Bearer abc123' }),
139
+ })
140
+ );
141
+ });
142
+
143
+ it('should handle errors and retry for retryable errors', async () => {
144
+ // Arrange
145
+ const url = 'https://example.com/image.png';
146
+ const pat = 'token123';
147
+
148
+ // Create a rate limit error (retry-eligible)
149
+ const rateLimitError = new Error('Rate limit exceeded');
150
+ (rateLimitError as any).response = { status: 429 };
151
+
152
+ // Configure mock to fail once then succeed
153
+ mockAxiosInstance.request.mockRejectedValueOnce(rateLimitError).mockResolvedValueOnce({
154
+ data: Buffer.from('image-data'),
155
+ headers: { 'content-type': 'image/png' },
156
+ });
157
+
158
+ // Mock setTimeout to execute immediately
159
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
160
+ fn();
161
+ return {} as any;
162
+ });
163
+
164
+ // Act
165
+ const result = await TFSServices.fetchAzureDevOpsImageAsBase64(url, pat);
166
+
167
+ // Assert
168
+ expect(result).toEqual('data:image/png;base64,aW1hZ2UtZGF0YQ==');
169
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(2);
170
+ expect(logger.warn).toHaveBeenCalled();
171
+ });
172
+ });
173
+
174
+ describe('getItemContent', () => {
175
+ it('should get item content successfully with GET request', async () => {
176
+ // Arrange
177
+ const url = 'https://example.com/api/item';
178
+ const pat = 'token123';
179
+ const mockResponse = { data: { id: 123, name: 'Test Item' } };
180
+
181
+ // Configure mock response
182
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
183
+
184
+ // Act
185
+ const result = await TFSServices.getItemContent(url, pat);
186
+
187
+ // Assert
188
+ expect(result).toEqual(mockResponse.data);
189
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
190
+ expect.objectContaining({
191
+ url: url.replace(/ /g, '%20'),
192
+ method: 'get',
193
+ auth: { username: '', password: pat },
194
+ timeout: 10000, // Verify the actual timeout value is 10000ms
195
+ })
196
+ );
197
+ });
198
+
199
+ it('should use bearer Authorization header when bearer token is provided', async () => {
200
+ // Arrange
201
+ const url = 'https://example.com/api/item';
202
+ const token = 'bearer:abc123';
203
+ const mockResponse = { data: { id: 123, name: 'Test Item' } };
204
+
205
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
206
+
207
+ // Act
208
+ const result = await TFSServices.getItemContent(url, token);
209
+
210
+ // Assert
211
+ expect(result).toEqual(mockResponse.data);
212
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
213
+ expect.objectContaining({
214
+ url: url.replace(/ /g, '%20'),
215
+ method: 'get',
216
+ headers: expect.objectContaining({ Authorization: 'Bearer abc123' }),
217
+ })
218
+ );
219
+ });
220
+
221
+ it('should fail when request times out', async () => {
222
+ // Arrange
223
+ const url = 'https://example.com/api/slow-item';
224
+ const pat = 'token123';
225
+
226
+ // Create a timeout error
227
+ const timeoutError = new Error('timeout of 1000ms exceeded');
228
+ timeoutError.name = 'TimeoutError';
229
+ (timeoutError as any).code = 'ECONNABORTED';
230
+
231
+ // Configure mock to simulate timeout (will retry 3 times by default)
232
+ mockAxiosInstance.request
233
+ .mockRejectedValueOnce(timeoutError)
234
+ .mockRejectedValueOnce(timeoutError)
235
+ .mockRejectedValueOnce(timeoutError);
236
+
237
+ // Mock setTimeout to execute immediately for faster tests
238
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
239
+ fn();
240
+ return {} as any;
241
+ });
242
+
243
+ // Act & Assert
244
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow('timeout');
245
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(3); // Initial + 2 retries
246
+ expect(logger.warn).toHaveBeenCalledTimes(2); // Two retry warnings
247
+ });
248
+
249
+ it('should fail when network connection fails', async () => {
250
+ // Arrange
251
+ const url = 'https://example.com/api/item';
252
+ const pat = 'token123';
253
+
254
+ // Create different network errors
255
+ const connectionResetError = new Error('socket hang up');
256
+ (connectionResetError as any).code = 'ECONNRESET';
257
+
258
+ const connectionRefusedError = new Error('connect ECONNREFUSED');
259
+ (connectionRefusedError as any).code = 'ECONNREFUSED';
260
+
261
+ // Configure mock to simulate different network failures on each retry
262
+ mockAxiosInstance.request
263
+ .mockRejectedValueOnce(connectionResetError)
264
+ .mockRejectedValueOnce(connectionRefusedError)
265
+ .mockRejectedValueOnce(connectionResetError);
266
+
267
+ // Mock setTimeout to execute immediately for faster tests
268
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
269
+ fn();
270
+ return {} as any;
271
+ });
272
+
273
+ // Act & Assert
274
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
275
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(2); // Initial + 2 retries
276
+ expect(logger.error).toHaveBeenCalled(); // Should log detailed error
277
+ });
278
+
279
+ it('should handle DNS resolution failures', async () => {
280
+ // Arrange
281
+ const url = 'https://nonexistent-domain.example.com/api/item';
282
+ const pat = 'token123';
283
+
284
+ // Create DNS resolution error
285
+ const dnsError = new Error('getaddrinfo ENOTFOUND nonexistent-domain.example.com');
286
+ (dnsError as any).code = 'ENOTFOUND';
287
+
288
+ // Configure mock to simulate DNS failure
289
+ mockAxiosInstance.request.mockRejectedValue(dnsError);
290
+
291
+ // Act & Assert
292
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow('ENOTFOUND');
293
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(2); // Should retry DNS failures too
294
+ });
295
+
296
+ it('should handle spaces in URL by replacing them with %20', async () => {
297
+ // Arrange
298
+ const url = 'https://example.com/api/item with spaces';
299
+ const pat = 'token123';
300
+ const mockResponse = { data: { id: 123, name: 'Test Item' } };
301
+
302
+ // Configure mock response
303
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
304
+
305
+ // Act
306
+ await TFSServices.getItemContent(url, pat);
307
+
308
+ // Assert
309
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
310
+ expect.objectContaining({ url: 'https://example.com/api/item%20with%20spaces' })
311
+ );
312
+ });
313
+ });
314
+
315
+ describe('getItemContentWithHeaders', () => {
316
+ it('should return data and headers from response', async () => {
317
+ // Arrange
318
+ const url = 'https://example.com/api/item';
319
+ const pat = 'token123';
320
+ const mockResponse = {
321
+ data: { id: 123, name: 'Test Item' },
322
+ headers: { 'x-custom-header': 'value' },
323
+ };
324
+
325
+ // Configure mock response
326
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
327
+
328
+ // Act
329
+ const result = await TFSServices.getItemContentWithHeaders(url, pat);
330
+
331
+ // Assert
332
+ expect(result).toEqual({
333
+ data: mockResponse.data,
334
+ headers: mockResponse.headers,
335
+ });
336
+ });
337
+
338
+ it('should handle spaces in URL', async () => {
339
+ // Arrange
340
+ const url = 'https://example.com/api/item with spaces';
341
+ const pat = 'token123';
342
+ const mockResponse = {
343
+ data: { id: 123 },
344
+ headers: {},
345
+ };
346
+
347
+ // Configure mock response
348
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
349
+
350
+ // Act
351
+ await TFSServices.getItemContentWithHeaders(url, pat);
352
+
353
+ // Assert
354
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
355
+ expect.objectContaining({ url: 'https://example.com/api/item%20with%20spaces' })
356
+ );
357
+ });
358
+ });
359
+
360
+ describe('getJfrogRequest', () => {
361
+ it('should make a successful GET request to JFrog', async () => {
362
+ // Arrange
363
+ const url = 'https://jfrog.example.com/api/artifacts';
364
+ const headers = { Authorization: 'Bearer token123' };
365
+ const mockResponse = { data: { artifacts: [{ name: 'artifact1' }] } };
366
+
367
+ // Configure mock response
368
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
369
+
370
+ // Act
371
+ const result = await TFSServices.getJfrogRequest(url, headers);
372
+
373
+ // Assert
374
+ expect(result).toEqual(mockResponse.data);
375
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
376
+ url,
377
+ method: 'GET',
378
+ headers,
379
+ });
380
+ });
381
+
382
+ it('should handle errors from JFrog API', async () => {
383
+ // Arrange
384
+ const url = 'https://jfrog.example.com/api/artifacts';
385
+ const headers = { Authorization: 'Bearer token123' };
386
+ const mockError = new Error('JFrog API error');
387
+
388
+ // Configure mock to throw error
389
+ mockAxiosInstance.request.mockRejectedValueOnce(mockError);
390
+
391
+ // Act & Assert
392
+ await expect(TFSServices.getJfrogRequest(url, headers)).rejects.toThrow();
393
+ expect(logger.error).toHaveBeenCalled();
394
+ });
395
+ });
396
+
397
+ describe('postRequest', () => {
398
+ it('should make a successful POST request', async () => {
399
+ // Arrange
400
+ const url = 'https://example.com/api/resource';
401
+ const pat = 'token123';
402
+ const data = { name: 'New Resource' };
403
+ const mockResponse = { data: { id: 123, name: 'New Resource' } };
404
+
405
+ // Configure mock response
406
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
407
+
408
+ // Act
409
+ const result = await TFSServices.postRequest(url, pat, 'post', data);
410
+
411
+ // Assert
412
+ expect(result).toEqual(mockResponse);
413
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
414
+ url,
415
+ method: 'post',
416
+ auth: { username: '', password: pat },
417
+ data,
418
+ headers: { headers: { 'Content-Type': 'application/json' } },
419
+ });
420
+ });
421
+
422
+ it('should use bearer Authorization header when bearer token is provided', async () => {
423
+ // Arrange
424
+ const url = 'https://example.com/api/resource';
425
+ const token = 'bearer:abc123';
426
+ const data = { name: 'New Resource' };
427
+ const mockResponse = { data: { id: 123, name: 'New Resource' } };
428
+
429
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
430
+
431
+ // Act
432
+ const result = await TFSServices.postRequest(url, token, 'post', data);
433
+
434
+ // Assert
435
+ expect(result).toEqual(mockResponse);
436
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
437
+ url,
438
+ method: 'post',
439
+ data,
440
+ headers: {
441
+ headers: {
442
+ 'Content-Type': 'application/json',
443
+ Authorization: 'Bearer abc123',
444
+ },
445
+ },
446
+ });
447
+ });
448
+
449
+ it('should work with custom headers and methods', async () => {
450
+ // Arrange
451
+ const url = 'https://example.com/api/resource';
452
+ const pat = 'token123';
453
+ const data = { name: 'Update Resource' };
454
+ const customHeaders = { 'Content-Type': 'application/xml' };
455
+ const mockResponse = { data: { id: 123, name: 'Updated Resource' } };
456
+
457
+ // Configure mock response
458
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
459
+
460
+ // Act
461
+ const result = await TFSServices.postRequest(url, pat, 'put', data, customHeaders);
462
+
463
+ // Assert
464
+ expect(result).toEqual(mockResponse);
465
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
466
+ url,
467
+ method: 'put',
468
+ auth: { username: '', password: pat },
469
+ data,
470
+ headers: customHeaders,
471
+ });
472
+ });
473
+
474
+ it('should handle errors in POST requests', async () => {
475
+ // Arrange
476
+ const url = 'https://example.com/api/resource';
477
+ const pat = 'token123';
478
+ const data = { name: 'New Resource' };
479
+ const mockError = new Error('Validation error');
480
+ (mockError as any).response = { status: 400, data: { message: 'Invalid data' } };
481
+
482
+ // Configure mock to throw error
483
+ mockAxiosInstance.request.mockRejectedValueOnce(mockError);
484
+
485
+ // Act & Assert
486
+ await expect(TFSServices.postRequest(url, pat, 'post', data)).rejects.toThrow();
487
+ expect(logger.error).toHaveBeenCalled();
488
+ });
489
+ });
490
+
491
+ describe('executeWithRetry', () => {
492
+ it('should retry on network timeouts', async () => {
493
+ // Arrange
494
+ const url = 'https://example.com/api/slow-resource';
495
+ const pat = 'token123';
496
+
497
+ // Create a timeout error
498
+ const timeoutError = new Error('timeout of 10000ms exceeded');
499
+
500
+ // Configure mock to fail with timeout then succeed
501
+ mockAxiosInstance.request
502
+ .mockRejectedValueOnce(timeoutError)
503
+ .mockResolvedValueOnce({ data: { success: true } });
504
+
505
+ // Mock setTimeout to execute immediately
506
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
507
+ fn();
508
+ return {} as any;
509
+ });
510
+
511
+ // Act
512
+ const result = await TFSServices.getItemContent(url, pat);
513
+
514
+ // Assert
515
+ expect(result).toEqual({ success: true });
516
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(2);
517
+ expect(logger.warn).toHaveBeenCalled();
518
+ });
519
+
520
+ it('should retry on server errors (5xx)', async () => {
521
+ // Arrange
522
+ const url = 'https://example.com/api/unstable-resource';
523
+ const pat = 'token123';
524
+
525
+ // Create a 503 error
526
+ const serverError = new Error('Service unavailable');
527
+ (serverError as any).response = { status: 503 };
528
+
529
+ // Configure mock to fail with server error then succeed
530
+ mockAxiosInstance.request
531
+ .mockRejectedValueOnce(serverError)
532
+ .mockResolvedValueOnce({ data: { success: true } });
533
+
534
+ // Mock setTimeout to execute immediately
535
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
536
+ fn();
537
+ return {} as any;
538
+ });
539
+
540
+ // Act
541
+ const result = await TFSServices.getItemContent(url, pat);
542
+
543
+ // Assert
544
+ expect(result).toEqual({ success: true });
545
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(2);
546
+ });
547
+
548
+ it('should give up after max retry attempts', async () => {
549
+ // Arrange
550
+ const url = 'https://example.com/api/failing-resource';
551
+ const pat = 'token123';
552
+
553
+ // Create a persistent server error
554
+ const serverError = new Error('Service unavailable');
555
+ (serverError as any).response = { status: 503 };
556
+
557
+ // Configure mock to always fail with server error
558
+ mockAxiosInstance.request.mockRejectedValue(serverError);
559
+
560
+ // Mock setTimeout to execute immediately
561
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
562
+ fn();
563
+ return {} as any;
564
+ });
565
+
566
+ // Act & Assert
567
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
568
+
569
+ // Should try original request + retries up to maxAttempts (3)
570
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(3);
571
+ });
572
+ });
573
+ // Add these test cases to the existing test file
574
+
575
+ describe('Stress testing', () => {
576
+ describe('getItemContent - Sequential large data requests', () => {
577
+ it('should handle multiple sequential requests with large datasets', async () => {
578
+ // Arrange
579
+ const url = 'https://example.com/api/large-dataset';
580
+ const pat = 'token123';
581
+
582
+ // Create 5 large responses of different sizes
583
+ const responses = Array(5)
584
+ .fill(0)
585
+ .map((_, i) => {
586
+ // Create increasingly large responses (500KB, 1MB, 1.5MB, 2MB, 2.5MB)
587
+ const size = 25000 * (i + 1);
588
+ return {
589
+ data: {
590
+ items: Array(size)
591
+ .fill(0)
592
+ .map((_, j) => ({
593
+ id: j,
594
+ name: `Item ${j}`,
595
+ description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
596
+ data: `value-${j}-${Math.random().toString(36).substring(2, 15)}`,
597
+ })),
598
+ },
599
+ };
600
+ });
601
+
602
+ // Configure the mock to return the different responses in sequence
603
+ for (const response of responses) {
604
+ mockAxiosInstance.request.mockResolvedValueOnce(response);
605
+ }
606
+
607
+ // Act - Make multiple sequential requests
608
+ const results = [];
609
+ for (let i = 0; i < responses.length; i++) {
610
+ const result = await TFSServices.getItemContent(`${url}/${i}`, pat);
611
+ results.push(result);
612
+ }
613
+
614
+ // Assert
615
+ expect(results.length).toBe(responses.length);
616
+ for (let i = 0; i < results.length; i++) {
617
+ expect(results[i].items.length).toBe(responses[i].data.items.length);
618
+ }
619
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(responses.length);
620
+ });
621
+
622
+ it('should handle sequential requests with mixed success/failure patterns', async () => {
623
+ // Arrange
624
+ const url = 'https://example.com/api/sequential';
625
+ const pat = 'token123';
626
+
627
+ // Set up a sequence of responses/errors
628
+ // 1. Success
629
+ // 2. Rate limit error (429) - should retry and succeed
630
+ // 3. Success
631
+ // 4. Server error (503) - should retry and succeed
632
+ // 5. Success
633
+
634
+ const rateLimitError = new Error('Rate limit exceeded');
635
+ (rateLimitError as any).response = { status: 429 };
636
+
637
+ const serverError = new Error('Server error');
638
+ (serverError as any).response = { status: 503 };
639
+
640
+ // Response 1
641
+ mockAxiosInstance.request.mockResolvedValueOnce({
642
+ data: { id: 1, success: true },
643
+ });
644
+
645
+ // Response 2 - fails with rate limit first, then succeeds
646
+ mockAxiosInstance.request.mockRejectedValueOnce(rateLimitError).mockResolvedValueOnce({
647
+ data: { id: 2, success: true },
648
+ });
649
+
650
+ // Response 3
651
+ mockAxiosInstance.request.mockResolvedValueOnce({
652
+ data: { id: 3, success: true },
653
+ });
654
+
655
+ // Response 4 - fails with server error first, then succeeds
656
+ mockAxiosInstance.request.mockRejectedValueOnce(serverError).mockResolvedValueOnce({
657
+ data: { id: 4, success: true },
658
+ });
659
+
660
+ // Response 5
661
+ mockAxiosInstance.request.mockResolvedValueOnce({
662
+ data: { id: 5, success: true },
663
+ });
664
+
665
+ // Mock setTimeout to execute immediately for retry delay
666
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
667
+ fn();
668
+ return {} as any;
669
+ });
670
+
671
+ // Act - Make sequential requests
672
+ const results = [];
673
+ for (let i = 1; i <= 5; i++) {
674
+ const result = await TFSServices.getItemContent(`${url}/${i}`, pat);
675
+ results.push(result);
676
+ }
677
+
678
+ // Assert
679
+ expect(results.length).toBe(5);
680
+ expect(results.map((r) => r.id)).toEqual([1, 2, 3, 4, 5]);
681
+
682
+ // Check total number of requests (5 successful responses + 2 retries)
683
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(7);
684
+ });
685
+ });
686
+
687
+ describe('postRequest - Sequential large data submissions', () => {
688
+ it('should handle multiple sequential POST requests with large payloads', async () => {
689
+ // Arrange
690
+ const url = 'https://example.com/api/resource';
691
+ const pat = 'token123';
692
+
693
+ // Create 5 increasingly large payloads
694
+ const payloads = Array(5)
695
+ .fill(0)
696
+ .map((_, i) => {
697
+ // Create payloads of increasing size (100KB, 200KB, 300KB, 400KB, 500KB)
698
+ const size = 5000 * (i + 1);
699
+ return {
700
+ name: `Resource ${i + 1}`,
701
+ description: `Large resource ${i + 1}`,
702
+ items: Array(size)
703
+ .fill(0)
704
+ .map((_, j) => ({
705
+ id: j,
706
+ value: `item-${j}-${Math.random().toString(36).substring(2, 15)}`,
707
+ timestamp: new Date().toISOString(),
708
+ })),
709
+ };
710
+ });
711
+
712
+ // Set up mock responses
713
+ for (let i = 0; i < payloads.length; i++) {
714
+ mockAxiosInstance.request.mockResolvedValueOnce({
715
+ data: { id: i + 1, status: 'created', itemCount: payloads[i].items.length },
716
+ });
717
+ }
718
+
719
+ // Act - Make multiple sequential POST requests
720
+ const results = [];
721
+ for (let i = 0; i < payloads.length; i++) {
722
+ const result = await TFSServices.postRequest(url, pat, 'post', payloads[i]);
723
+ results.push(result);
724
+ }
725
+
726
+ // Assert
727
+ expect(results.length).toBe(payloads.length);
728
+ for (let i = 0; i < results.length; i++) {
729
+ expect(results[i].data.id).toBe(i + 1);
730
+ expect(results[i].data.itemCount).toBe(payloads[i].items.length);
731
+ }
732
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(payloads.length);
733
+ });
734
+
735
+ it('should handle a mix of success and failure during sequential POST operations', async () => {
736
+ // Arrange
737
+ const url = 'https://example.com/api/resource';
738
+ const pat = 'token123';
739
+
740
+ // Create test data
741
+ const payloads = Array(5)
742
+ .fill(0)
743
+ .map((_, i) => ({ name: `Resource ${i + 1}` }));
744
+
745
+ // Validation error
746
+ const validationError = new Error('Validation error');
747
+ (validationError as any).response = {
748
+ status: 400,
749
+ data: { message: 'Invalid data', details: 'Field X is required' },
750
+ };
751
+
752
+ // Server error
753
+ const serverError = new Error('Server error');
754
+ (serverError as any).response = { status: 500 };
755
+
756
+ // Configure mock responses/errors for each request
757
+ // 1. Success
758
+ mockAxiosInstance.request.mockResolvedValueOnce({
759
+ data: { id: 1, status: 'created' },
760
+ });
761
+
762
+ // 2. Validation error
763
+ mockAxiosInstance.request.mockRejectedValueOnce(validationError);
764
+
765
+ // 3. Success
766
+ mockAxiosInstance.request.mockResolvedValueOnce({
767
+ data: { id: 3, status: 'created' },
768
+ });
769
+
770
+ // 4. Server error
771
+ mockAxiosInstance.request.mockRejectedValueOnce(serverError);
772
+
773
+ // 5. Success
774
+ mockAxiosInstance.request.mockResolvedValueOnce({
775
+ data: { id: 5, status: 'created' },
776
+ });
777
+
778
+ // Act & Assert
779
+ // Request 1 - should succeed
780
+ const result1 = await TFSServices.postRequest(url, pat, 'post', payloads[0]);
781
+ expect(result1.data.id).toBe(1);
782
+
783
+ // Request 2 - should fail with validation error
784
+ await expect(TFSServices.postRequest(url, pat, 'post', payloads[1])).rejects.toThrow(
785
+ 'Validation error'
786
+ );
787
+
788
+ // Request 3 - should succeed despite previous failure
789
+ const result3 = await TFSServices.postRequest(url, pat, 'post', payloads[2]);
790
+ expect(result3.data.id).toBe(3);
791
+
792
+ // Request 4 - should fail with server error
793
+ await expect(TFSServices.postRequest(url, pat, 'post', payloads[3])).rejects.toThrow('Server error');
794
+
795
+ // Request 5 - should succeed despite previous failure
796
+ const result5 = await TFSServices.postRequest(url, pat, 'post', payloads[4]);
797
+ expect(result5.data.id).toBe(5);
798
+
799
+ // Verify all requests were made
800
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(5);
801
+ });
802
+
803
+ it('should handle large payload with many nested objects', async () => {
804
+ // Arrange
805
+ const url = 'https://example.com/api/complex-resource';
806
+ const pat = 'token123';
807
+
808
+ // Create a deeply nested object structure
809
+ const createNestedObject = (depth: number, breadth: number, current = 0): any => {
810
+ if (current >= depth) {
811
+ return { value: `leaf-${Math.random()}` };
812
+ }
813
+
814
+ const children: Record<string, any> = {};
815
+ for (let i = 0; i < breadth; i++) {
816
+ children[`child-${current}-${i}`] = createNestedObject(depth, breadth, current + 1);
817
+ }
818
+
819
+ return {
820
+ id: `node-${current}-${Math.random()}`,
821
+ level: current,
822
+ children,
823
+ };
824
+ };
825
+
826
+ // Create a complex payload with depth 5 and breadth 5 (5^5 = 3,125 nodes)
827
+ const complexPayload = {
828
+ name: 'Complex Resource',
829
+ type: 'hierarchical',
830
+ rootNode: createNestedObject(5, 5),
831
+ };
832
+
833
+ // Configure mock response
834
+ mockAxiosInstance.request.mockResolvedValueOnce({
835
+ data: { id: 123, status: 'created', complexity: 'high' },
836
+ });
837
+
838
+ // Act
839
+ const result = await TFSServices.postRequest(url, pat, 'post', complexPayload);
840
+
841
+ // Assert
842
+ expect(result.data.id).toBe(123);
843
+ expect(result.data.status).toBe('created');
844
+
845
+ // Verify the request was made with the complex payload
846
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith(
847
+ expect.objectContaining({
848
+ url,
849
+ method: 'post',
850
+ data: complexPayload,
851
+ })
852
+ );
853
+ });
854
+ });
855
+
856
+ describe('fetchAzureDevOpsImageAsBase64 - non-image content', () => {
857
+ it('should throw error when content type is not an image', async () => {
858
+ // Arrange
859
+ const url = 'https://example.com/file.txt';
860
+ const pat = 'token123';
861
+ const mockResponse = {
862
+ data: Buffer.from('text content'),
863
+ headers: { 'content-type': 'text/plain' },
864
+ };
865
+
866
+ // Configure mock response
867
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
868
+
869
+ // Act & Assert
870
+ await expect(TFSServices.fetchAzureDevOpsImageAsBase64(url, pat)).rejects.toThrow(
871
+ "Expected image content but received 'text/plain'"
872
+ );
873
+ });
874
+
875
+ it('should handle content-type with charset', async () => {
876
+ // Arrange
877
+ const url = 'https://example.com/image.png';
878
+ const pat = 'token123';
879
+ const mockResponse = {
880
+ data: Buffer.from('image-data'),
881
+ headers: { 'content-type': 'image/png; charset=utf-8' },
882
+ };
883
+
884
+ // Configure mock response
885
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
886
+
887
+ // Act
888
+ const result = await TFSServices.fetchAzureDevOpsImageAsBase64(url, pat);
889
+
890
+ // Assert
891
+ expect(result).toEqual('data:image/png;base64,aW1hZ2UtZGF0YQ==');
892
+ });
893
+
894
+ it('should use default content type when header is missing', async () => {
895
+ // Arrange
896
+ const url = 'https://example.com/image.png';
897
+ const pat = 'token123';
898
+ const mockResponse = {
899
+ data: Buffer.from('image-data'),
900
+ headers: {},
901
+ };
902
+
903
+ // Configure mock response
904
+ mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
905
+
906
+ // Act & Assert - should throw because application/octet-stream is not an image
907
+ await expect(TFSServices.fetchAzureDevOpsImageAsBase64(url, pat)).rejects.toThrow(
908
+ "Expected image content but received 'application/octet-stream'"
909
+ );
910
+ });
911
+ });
912
+
913
+ describe('getItemContent - file not found handling', () => {
914
+ it('should throw specific error when file is not found', async () => {
915
+ // Arrange
916
+ const url = 'https://example.com/api/missing-file';
917
+ const pat = 'token123';
918
+
919
+ // Create a "not found" error
920
+ const notFoundError = new Error('The file could not be found');
921
+ (notFoundError as any).response = {
922
+ status: 404,
923
+ data: { message: 'The file could not be found' },
924
+ };
925
+
926
+ // Configure mock to throw not found error
927
+ mockAxiosInstance.request.mockRejectedValueOnce(notFoundError);
928
+
929
+ // Act & Assert
930
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow(
931
+ 'File not found or insufficient permissions'
932
+ );
933
+ });
934
+ });
935
+
936
+ describe('logDetailedError - various error formats', () => {
937
+ it('should handle error with string response data', async () => {
938
+ // Arrange
939
+ const url = 'https://example.com/api/error';
940
+ const pat = 'token123';
941
+
942
+ const errorWithStringData = new Error('Server error');
943
+ (errorWithStringData as any).response = {
944
+ status: 500,
945
+ data: 'Internal Server Error - detailed message here that is quite long',
946
+ };
947
+
948
+ // Configure mock to fail
949
+ mockAxiosInstance.request.mockRejectedValue(errorWithStringData);
950
+
951
+ // Mock setTimeout to execute immediately
952
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
953
+ fn();
954
+ return {} as any;
955
+ });
956
+
957
+ // Act & Assert
958
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
959
+ expect(logger.error).toHaveBeenCalled();
960
+ });
961
+
962
+ it('should handle error with object response data containing message', async () => {
963
+ // Arrange
964
+ const url = 'https://example.com/api/error';
965
+ const pat = 'token123';
966
+
967
+ const errorWithObjectData = new Error('Server error');
968
+ (errorWithObjectData as any).response = {
969
+ status: 500,
970
+ data: { message: 'Detailed error message' },
971
+ };
972
+
973
+ // Configure mock to fail
974
+ mockAxiosInstance.request.mockRejectedValue(errorWithObjectData);
975
+
976
+ // Mock setTimeout to execute immediately
977
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
978
+ fn();
979
+ return {} as any;
980
+ });
981
+
982
+ // Act & Assert
983
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
984
+ expect(logger.error).toHaveBeenCalled();
985
+ });
986
+
987
+ it('should handle error without response object', async () => {
988
+ // Arrange
989
+ const url = 'https://example.com/api/error';
990
+ const pat = 'token123';
991
+
992
+ const errorWithoutResponse = new Error('Network error');
993
+
994
+ // Configure mock to fail
995
+ mockAxiosInstance.request.mockRejectedValue(errorWithoutResponse);
996
+
997
+ // Mock setTimeout to execute immediately
998
+ jest.spyOn(global, 'setTimeout').mockImplementation((fn: any) => {
999
+ fn();
1000
+ return {} as any;
1001
+ });
1002
+
1003
+ // Act & Assert
1004
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
1005
+ expect(logger.error).toHaveBeenCalled();
1006
+ });
1007
+ });
1008
+
1009
+ describe('getErrorMessage - various error formats', () => {
1010
+ it('should extract message from response.data.message', async () => {
1011
+ // Arrange
1012
+ const url = 'https://example.com/api/error';
1013
+ const pat = 'token123';
1014
+
1015
+ const error = new Error('Error');
1016
+ (error as any).response = {
1017
+ status: 400,
1018
+ data: { message: 'The file could not be found' },
1019
+ };
1020
+
1021
+ mockAxiosInstance.request.mockRejectedValueOnce(error);
1022
+
1023
+ // Act & Assert
1024
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
1025
+ });
1026
+
1027
+ it('should handle error with only response status', async () => {
1028
+ // Arrange
1029
+ const url = 'https://example.com/api/error';
1030
+ const pat = 'token123';
1031
+
1032
+ const error = new Error('Error');
1033
+ (error as any).response = { status: 403 };
1034
+
1035
+ mockAxiosInstance.request.mockRejectedValueOnce(error);
1036
+
1037
+ // Act & Assert
1038
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
1039
+ });
1040
+
1041
+ it('should handle error with no message property', async () => {
1042
+ // Arrange
1043
+ const url = 'https://example.com/api/error';
1044
+ const pat = 'token123';
1045
+
1046
+ const error = {};
1047
+
1048
+ mockAxiosInstance.request.mockRejectedValueOnce(error);
1049
+
1050
+ // Act & Assert
1051
+ await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow();
1052
+ });
1053
+ });
1054
+
1055
+ describe('High volume sequential operations', () => {
1056
+ it('should handle a high volume of sequential API calls', async () => {
1057
+ // Arrange
1058
+ const url = 'https://example.com/api/items';
1059
+ const pat = 'token123';
1060
+ const requestCount = 50; // Make 50 sequential requests
1061
+
1062
+ // Configure mock responses for all requests
1063
+ for (let i = 0; i < requestCount; i++) {
1064
+ mockAxiosInstance.request.mockResolvedValueOnce({
1065
+ data: { id: i, success: true, timestamp: new Date().toISOString() },
1066
+ });
1067
+ }
1068
+
1069
+ // Act
1070
+ const results = [];
1071
+ for (let i = 0; i < requestCount; i++) {
1072
+ // Alternate between GET and POST requests
1073
+ if (i % 2 === 0) {
1074
+ const result = await TFSServices.getItemContent(`${url}/${i}`, pat);
1075
+ results.push(result);
1076
+ } else {
1077
+ const result = await TFSServices.postRequest(url, pat, 'post', { itemId: i });
1078
+ results.push(result.data);
1079
+ }
1080
+ }
1081
+
1082
+ // Assert
1083
+ expect(results.length).toBe(requestCount);
1084
+ for (let i = 0; i < requestCount; i++) {
1085
+ expect(results[i].id).toBe(i);
1086
+ expect(results[i].success).toBe(true);
1087
+ }
1088
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(requestCount);
1089
+ });
1090
+ });
1091
+ });
1092
+ });