@elisra-devops/docgen-data-provider 1.63.13 → 1.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +26 -9
- package/.github/workflows/release.yml +9 -10
- package/bin/helpers/tfs.d.ts +3 -0
- package/bin/helpers/tfs.js +44 -7
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.d.ts +10 -0
- package/bin/modules/GitDataProvider.js +10 -0
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +0 -1
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +63 -24
- package/bin/modules/TicketsDataProvider.js +216 -114
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +279 -0
- package/bin/tests/helpers/helper.test.js.map +1 -0
- package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
- package/bin/tests/helpers/tfs.test.js.map +1 -0
- package/bin/tests/index.test.js +25 -0
- package/bin/tests/index.test.js.map +1 -0
- package/bin/tests/models/tfs-data.test.js +160 -0
- package/bin/tests/models/tfs-data.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
- package/bin/tests/modules/gitDataProvider.test.js +1888 -0
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +13 -1
- package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
- package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/testDataProvider.test.js +717 -0
- package/bin/tests/modules/testDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
- package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
- package/bin/tests/utils/DataProviderUtils.test.js +61 -0
- package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
- package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
- package/bin/tests/utils/testStepParserHelper.test.js +359 -0
- package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
- package/package.json +9 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +298 -141
- package/src/tests/helpers/helper.test.ts +337 -0
- package/src/tests/helpers/tfs.test.ts +1092 -0
- package/src/tests/index.test.ts +28 -0
- package/src/tests/models/tfs-data.test.ts +203 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
- package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
- package/src/tests/modules/gitDataProvider.test.ts +2628 -0
- package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +33 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
- package/src/tests/modules/testDataProvider.test.ts +1046 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
- package/src/tests/utils/DataProviderUtils.test.ts +76 -0
- package/src/tests/utils/testStepParserHelper.test.ts +437 -0
- package/tsconfig.json +1 -0
- package/bin/helpers/test/tfs.test.js.map +0 -1
- package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
- package/bin/modules/test/ResultDataProvider.test.js +0 -444
- package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
- package/bin/modules/test/gitDataProvider.test.js +0 -428
- package/bin/modules/test/gitDataProvider.test.js.map +0 -1
- package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
- package/bin/modules/test/pipelineDataProvider.test.js +0 -237
- package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
- package/bin/modules/test/testDataProvider.test.js +0 -234
- package/bin/modules/test/testDataProvider.test.js.map +0 -1
- package/bin/modules/test/ticketsDataProvider.test.js +0 -348
- package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
- package/src/helpers/test/tfs.test.ts +0 -748
- package/src/modules/test/JfrogDataProvider.test.ts +0 -171
- package/src/modules/test/ResultDataProvider.test.ts +0 -542
- package/src/modules/test/gitDataProvider.test.ts +0 -645
- package/src/modules/test/pipelineDataProvider.test.ts +0 -292
- package/src/modules/test/testDataProvider.test.ts +0 -318
- package/src/modules/test/ticketsDataProvider.test.ts +0 -462
- /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
- /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
- /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
- /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
- /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
|
@@ -0,0 +1,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
|
+
});
|