@forwardimpact/libutil 0.1.60
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/LICENSE +201 -0
- package/bin/fit-download-bundle.js +24 -0
- package/downloader.js +108 -0
- package/extractor.js +330 -0
- package/finder.js +148 -0
- package/http.js +21 -0
- package/index.js +154 -0
- package/package.json +24 -0
- package/processor.js +125 -0
- package/retry.js +126 -0
- package/test/downloader.test.js +223 -0
- package/test/extractor.test.js +338 -0
- package/test/finder.test.js +285 -0
- package/test/fixtures/sample.tar.gz +0 -0
- package/test/fixtures/sample.zip +0 -0
- package/test/http.test.js +93 -0
- package/test/libutil.test.js +38 -0
- package/test/logger.test.js +322 -0
- package/test/processor.test.js +140 -0
- package/test/retry.test.js +194 -0
- package/test/tokenizer.test.js +123 -0
- package/test/wait.test.js +89 -0
- package/tokenizer.js +89 -0
- package/wait.js +28 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
// Module under test
|
|
5
|
+
import { Logger, createLogger } from "@forwardimpact/libtelemetry";
|
|
6
|
+
|
|
7
|
+
describe("Logger", () => {
|
|
8
|
+
let originalDebug;
|
|
9
|
+
let consoleOutput;
|
|
10
|
+
let originalConsoleError;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
originalDebug = process.env.DEBUG;
|
|
14
|
+
consoleOutput = [];
|
|
15
|
+
originalConsoleError = console.error;
|
|
16
|
+
console.error = (message) => consoleOutput.push(message);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.env.DEBUG = originalDebug;
|
|
21
|
+
console.error = originalConsoleError;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("creates Logger with domain", () => {
|
|
25
|
+
const logger = new Logger("test");
|
|
26
|
+
|
|
27
|
+
assert.ok(logger instanceof Logger);
|
|
28
|
+
assert.strictEqual(logger.domain, "test");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("validates constructor parameters", () => {
|
|
32
|
+
assert.throws(() => new Logger(), {
|
|
33
|
+
message: /domain must be a non-empty string/,
|
|
34
|
+
});
|
|
35
|
+
assert.throws(() => new Logger(""), {
|
|
36
|
+
message: /domain must be a non-empty string/,
|
|
37
|
+
});
|
|
38
|
+
assert.throws(() => new Logger(null), {
|
|
39
|
+
message: /domain must be a non-empty string/,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("enables logging when DEBUG=*", () => {
|
|
44
|
+
process.env.DEBUG = "*";
|
|
45
|
+
const logger = new Logger("test");
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(logger.enabled, true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("disables logging when DEBUG is empty", () => {
|
|
51
|
+
process.env.DEBUG = "";
|
|
52
|
+
const logger = new Logger("test");
|
|
53
|
+
|
|
54
|
+
assert.strictEqual(logger.enabled, false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("enables logging for exact domain match", () => {
|
|
58
|
+
process.env.DEBUG = "test,other";
|
|
59
|
+
const logger = new Logger("test");
|
|
60
|
+
|
|
61
|
+
assert.strictEqual(logger.enabled, true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("enables logging for wildcard pattern match", () => {
|
|
65
|
+
process.env.DEBUG = "test*";
|
|
66
|
+
const logger = new Logger("test:service");
|
|
67
|
+
|
|
68
|
+
assert.strictEqual(logger.enabled, true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("disables logging for non-matching domain", () => {
|
|
72
|
+
process.env.DEBUG = "other";
|
|
73
|
+
const logger = new Logger("test");
|
|
74
|
+
|
|
75
|
+
assert.strictEqual(logger.enabled, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("logs debug message when enabled", () => {
|
|
79
|
+
process.env.DEBUG = "test";
|
|
80
|
+
const logger = new Logger("test");
|
|
81
|
+
|
|
82
|
+
logger.debug("TestApp", "Test message");
|
|
83
|
+
|
|
84
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
85
|
+
assert.ok(consoleOutput[0].includes("DEBUG"));
|
|
86
|
+
assert.ok(consoleOutput[0].includes("test"));
|
|
87
|
+
assert.ok(consoleOutput[0].includes("TestApp"));
|
|
88
|
+
assert.ok(consoleOutput[0].includes("Test message"));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("does not log when disabled", () => {
|
|
92
|
+
process.env.DEBUG = "other";
|
|
93
|
+
const logger = new Logger("test");
|
|
94
|
+
|
|
95
|
+
logger.debug("TestApp", "Test message");
|
|
96
|
+
|
|
97
|
+
assert.strictEqual(consoleOutput.length, 0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test(
|
|
101
|
+
"logs message with data object",
|
|
102
|
+
{ skip: "Future PR will fix this" },
|
|
103
|
+
() => {
|
|
104
|
+
process.env.DEBUG = "test";
|
|
105
|
+
const logger = new Logger("test");
|
|
106
|
+
|
|
107
|
+
logger.debug("ProcessMethod", "Processing", {
|
|
108
|
+
items: "50/200",
|
|
109
|
+
retry: "2/3",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
113
|
+
assert.ok(consoleOutput[0].includes("DEBUG"));
|
|
114
|
+
assert.ok(consoleOutput[0].includes("test"));
|
|
115
|
+
assert.ok(consoleOutput[0].includes("ProcessMethod"));
|
|
116
|
+
assert.ok(consoleOutput[0].includes("Processing"));
|
|
117
|
+
assert.ok(consoleOutput[0].includes('items="50/200"'));
|
|
118
|
+
assert.ok(consoleOutput[0].includes('retry="2/3"'));
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
test("handles empty data object", () => {
|
|
123
|
+
process.env.DEBUG = "test";
|
|
124
|
+
const logger = new Logger("test");
|
|
125
|
+
|
|
126
|
+
logger.debug("TestApp", "Test message", {});
|
|
127
|
+
|
|
128
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
129
|
+
assert.ok(consoleOutput[0].includes("DEBUG"));
|
|
130
|
+
assert.ok(consoleOutput[0].includes("Test message"));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("includes timestamp in log output", () => {
|
|
134
|
+
process.env.DEBUG = "test";
|
|
135
|
+
const logger = new Logger("test");
|
|
136
|
+
|
|
137
|
+
logger.debug("TestApp", "Test message");
|
|
138
|
+
|
|
139
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
140
|
+
assert.ok(consoleOutput[0].match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test(
|
|
144
|
+
"extracts trace context from error objects",
|
|
145
|
+
{ skip: "Future PR will fix this" },
|
|
146
|
+
() => {
|
|
147
|
+
process.env.DEBUG = "test";
|
|
148
|
+
const logger = new Logger("test");
|
|
149
|
+
|
|
150
|
+
// Create error with trace context (as added by Tracer)
|
|
151
|
+
const error = new Error("Test error");
|
|
152
|
+
Object.defineProperties(error, {
|
|
153
|
+
trace_id: {
|
|
154
|
+
value: "abc123def456",
|
|
155
|
+
enumerable: false,
|
|
156
|
+
writable: false,
|
|
157
|
+
},
|
|
158
|
+
span_id: {
|
|
159
|
+
value: "789xyz012",
|
|
160
|
+
enumerable: false,
|
|
161
|
+
writable: false,
|
|
162
|
+
},
|
|
163
|
+
service_name: {
|
|
164
|
+
value: "test-service",
|
|
165
|
+
enumerable: false,
|
|
166
|
+
writable: false,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
logger.error("TestMethod", error);
|
|
171
|
+
|
|
172
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
173
|
+
assert.ok(consoleOutput[0].includes("ERROR"));
|
|
174
|
+
assert.ok(consoleOutput[0].includes("Test error"));
|
|
175
|
+
assert.ok(
|
|
176
|
+
consoleOutput[0].includes("trace_id=abc123def456"),
|
|
177
|
+
"Should include trace_id in structured data",
|
|
178
|
+
);
|
|
179
|
+
assert.ok(
|
|
180
|
+
consoleOutput[0].includes("span_id=789xyz012"),
|
|
181
|
+
"Should include span_id in structured data",
|
|
182
|
+
);
|
|
183
|
+
assert.ok(
|
|
184
|
+
consoleOutput[0].includes("service_name=test-service"),
|
|
185
|
+
"Should include service_name in structured data",
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
test(
|
|
191
|
+
"merges trace context with provided attributes",
|
|
192
|
+
{ skip: "Future PR will fix this" },
|
|
193
|
+
() => {
|
|
194
|
+
process.env.DEBUG = "test";
|
|
195
|
+
const logger = new Logger("test");
|
|
196
|
+
|
|
197
|
+
const error = new Error("Test error");
|
|
198
|
+
Object.defineProperty(error, "trace_id", {
|
|
199
|
+
value: "trace123",
|
|
200
|
+
enumerable: false,
|
|
201
|
+
writable: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
logger.error("TestMethod", error, { retry: "1/3", status: "500" });
|
|
205
|
+
|
|
206
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
207
|
+
assert.ok(consoleOutput[0].includes("ERROR"));
|
|
208
|
+
assert.ok(consoleOutput[0].includes("Test error"));
|
|
209
|
+
assert.ok(
|
|
210
|
+
consoleOutput[0].includes('trace_id="abc123def456"'),
|
|
211
|
+
"Should include trace_id in structured data",
|
|
212
|
+
);
|
|
213
|
+
assert.ok(
|
|
214
|
+
consoleOutput[0].includes('span_id="789xyz012"'),
|
|
215
|
+
"Should include span_id in structured data",
|
|
216
|
+
);
|
|
217
|
+
assert.ok(
|
|
218
|
+
consoleOutput[0].includes('service_name="test-service"'),
|
|
219
|
+
"Should include service_name in structured data",
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
test("merges trace context with provided attributes", () => {
|
|
225
|
+
process.env.DEBUG = "test";
|
|
226
|
+
const logger = new Logger("test");
|
|
227
|
+
|
|
228
|
+
const error = new Error("Test error");
|
|
229
|
+
Object.defineProperty(error, "trace_id", {
|
|
230
|
+
value: "trace123",
|
|
231
|
+
enumerable: false,
|
|
232
|
+
writable: false,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
logger.error("TestMethod", error, { retry: "1/3", status: "500" });
|
|
236
|
+
|
|
237
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
238
|
+
assert.ok(consoleOutput[0].includes('trace_id="trace123"'));
|
|
239
|
+
assert.ok(consoleOutput[0].includes('retry="1/3"'));
|
|
240
|
+
assert.ok(consoleOutput[0].includes('status="500"'));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("exception logs message when disabled", () => {
|
|
244
|
+
process.env.DEBUG = "other";
|
|
245
|
+
const logger = new Logger("test");
|
|
246
|
+
|
|
247
|
+
const error = new Error("Test error");
|
|
248
|
+
|
|
249
|
+
logger.exception("TestMethod", error);
|
|
250
|
+
|
|
251
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
252
|
+
assert.ok(consoleOutput[0].includes("ERROR"));
|
|
253
|
+
assert.ok(consoleOutput[0].includes("Test error"));
|
|
254
|
+
assert.ok(
|
|
255
|
+
!consoleOutput[0].includes("at "),
|
|
256
|
+
"Should not include stack trace when disabled",
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("exception logs message with stack trace when enabled", () => {
|
|
261
|
+
process.env.DEBUG = "test";
|
|
262
|
+
const logger = new Logger("test");
|
|
263
|
+
|
|
264
|
+
const error = new Error("Test error");
|
|
265
|
+
|
|
266
|
+
logger.exception("TestMethod", error);
|
|
267
|
+
|
|
268
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
269
|
+
assert.ok(consoleOutput[0].includes("ERROR"));
|
|
270
|
+
assert.ok(consoleOutput[0].includes("Test error"));
|
|
271
|
+
assert.ok(
|
|
272
|
+
consoleOutput[0].includes("at "),
|
|
273
|
+
"Should include stack trace when enabled",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("exception extracts trace context from error", () => {
|
|
278
|
+
process.env.DEBUG = "test";
|
|
279
|
+
const logger = new Logger("test");
|
|
280
|
+
|
|
281
|
+
const error = new Error("Test error");
|
|
282
|
+
error.trace_id = "trace456";
|
|
283
|
+
error.span_id = "span789";
|
|
284
|
+
error.service_name = "my-service";
|
|
285
|
+
|
|
286
|
+
logger.exception("TestMethod", error);
|
|
287
|
+
|
|
288
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
289
|
+
assert.ok(consoleOutput[0].includes('trace_id="trace456"'));
|
|
290
|
+
assert.ok(consoleOutput[0].includes('span_id="span789"'));
|
|
291
|
+
assert.ok(consoleOutput[0].includes('service_name="my-service"'));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("exception merges trace context with provided attributes", () => {
|
|
295
|
+
process.env.DEBUG = "test";
|
|
296
|
+
const logger = new Logger("test");
|
|
297
|
+
|
|
298
|
+
const error = new Error("Test error");
|
|
299
|
+
error.trace_id = "trace123";
|
|
300
|
+
|
|
301
|
+
logger.exception("TestMethod", error, { retry: "2/3" });
|
|
302
|
+
|
|
303
|
+
assert.strictEqual(consoleOutput.length, 1);
|
|
304
|
+
assert.ok(consoleOutput[0].includes('trace_id="trace123"'));
|
|
305
|
+
assert.ok(consoleOutput[0].includes('retry="2/3"'));
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("createLogger", () => {
|
|
310
|
+
test("creates Logger instance", () => {
|
|
311
|
+
const logger = createLogger("test");
|
|
312
|
+
|
|
313
|
+
assert.ok(logger instanceof Logger);
|
|
314
|
+
assert.strictEqual(logger.domain, "test");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("passes through domain validation", () => {
|
|
318
|
+
assert.throws(() => createLogger(""), {
|
|
319
|
+
message: /domain must be a non-empty string/,
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { test, describe, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
import { createSilentLogger } from "@forwardimpact/libharness";
|
|
5
|
+
|
|
6
|
+
// Module under test
|
|
7
|
+
import { ProcessorBase } from "../processor.js";
|
|
8
|
+
|
|
9
|
+
describe("ProcessorBase", () => {
|
|
10
|
+
let mockLogger;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockLogger = createSilentLogger();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("constructor", () => {
|
|
17
|
+
test("creates ProcessorBase with logger and batch size", () => {
|
|
18
|
+
const processor = new ProcessorBase(mockLogger, 5);
|
|
19
|
+
|
|
20
|
+
assert.ok(processor instanceof ProcessorBase);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("validates logger parameter", () => {
|
|
24
|
+
assert.throws(() => new ProcessorBase(), {
|
|
25
|
+
message: /logger is required/,
|
|
26
|
+
});
|
|
27
|
+
assert.throws(() => new ProcessorBase(null), {
|
|
28
|
+
message: /logger is required/,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("validates batch size parameter", () => {
|
|
33
|
+
assert.throws(() => new ProcessorBase(mockLogger, 0), {
|
|
34
|
+
message: /batchSize must be a positive number/,
|
|
35
|
+
});
|
|
36
|
+
assert.throws(() => new ProcessorBase(mockLogger, -1), {
|
|
37
|
+
message: /batchSize must be a positive number/,
|
|
38
|
+
});
|
|
39
|
+
assert.throws(() => new ProcessorBase(mockLogger, "invalid"), {
|
|
40
|
+
message: /batchSize must be a positive number/,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("uses default batch size when not provided", () => {
|
|
45
|
+
const processor = new ProcessorBase(mockLogger);
|
|
46
|
+
// Test passes if no error is thrown
|
|
47
|
+
assert.ok(processor instanceof ProcessorBase);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("process", () => {
|
|
52
|
+
test("validates items parameter", async () => {
|
|
53
|
+
const processor = new ProcessorBase(mockLogger, 2);
|
|
54
|
+
|
|
55
|
+
await assert.rejects(() => processor.process("not-array"), {
|
|
56
|
+
message: /items must be an array/,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("handles empty array", async () => {
|
|
61
|
+
const processor = new ProcessorBase(mockLogger, 2);
|
|
62
|
+
|
|
63
|
+
// Should not throw
|
|
64
|
+
await processor.process([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("calls processItem for each item", async () => {
|
|
68
|
+
/** Test processor that tracks processed items */
|
|
69
|
+
class TestProcessor extends ProcessorBase {
|
|
70
|
+
/**
|
|
71
|
+
* Creates a test processor
|
|
72
|
+
* @param {object} logger - Logger instance
|
|
73
|
+
*/
|
|
74
|
+
constructor(logger) {
|
|
75
|
+
super(logger, 2);
|
|
76
|
+
this.processedItems = [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Processes a single item
|
|
81
|
+
* @param {any} item - Item to process
|
|
82
|
+
* @returns {Promise<string>} Processed result
|
|
83
|
+
*/
|
|
84
|
+
async processItem(item) {
|
|
85
|
+
this.processedItems.push(item);
|
|
86
|
+
return `processed-${item}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const processor = new TestProcessor(mockLogger);
|
|
91
|
+
await processor.process(["a", "b", "c", "d"]);
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(processor.processedItems.length, 4);
|
|
94
|
+
assert.deepStrictEqual(processor.processedItems, ["a", "b", "c", "d"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("continues processing when individual items fail", async () => {
|
|
98
|
+
/** Test processor that simulates failures */
|
|
99
|
+
class TestProcessor extends ProcessorBase {
|
|
100
|
+
/**
|
|
101
|
+
* Creates a test processor
|
|
102
|
+
* @param {object} logger - Logger instance
|
|
103
|
+
*/
|
|
104
|
+
constructor(logger) {
|
|
105
|
+
super(logger, 3);
|
|
106
|
+
this.processedItems = [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Processes a single item with failure simulation
|
|
111
|
+
* @param {any} item - Item to process
|
|
112
|
+
* @returns {Promise<string>} Processed result
|
|
113
|
+
*/
|
|
114
|
+
async processItem(item) {
|
|
115
|
+
if (item === "fail") {
|
|
116
|
+
throw new Error("Simulated failure");
|
|
117
|
+
}
|
|
118
|
+
this.processedItems.push(item);
|
|
119
|
+
return `processed-${item}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const processor = new TestProcessor(mockLogger);
|
|
124
|
+
await processor.process(["a", "fail", "b", "c"]);
|
|
125
|
+
|
|
126
|
+
// Should have processed all items except the failing one
|
|
127
|
+
assert.deepStrictEqual(processor.processedItems, ["a", "b", "c"]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("processItem", () => {
|
|
132
|
+
test("throws error when not implemented", async () => {
|
|
133
|
+
const processor = new ProcessorBase(mockLogger, 2);
|
|
134
|
+
|
|
135
|
+
await assert.rejects(() => processor.processItem("item"), {
|
|
136
|
+
message: /processItem must be implemented by subclass/,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { test, describe, beforeEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
|
|
4
|
+
// Module under test
|
|
5
|
+
import { Retry } from "../retry.js";
|
|
6
|
+
|
|
7
|
+
describe("Retry", () => {
|
|
8
|
+
let retry;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Use very short delay for testing to speed up retry tests
|
|
12
|
+
retry = new Retry({ delay: 1 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("creates retry instance with default config", () => {
|
|
16
|
+
const defaultRetry = new Retry();
|
|
17
|
+
assert.ok(defaultRetry instanceof Retry);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("creates retry instance with custom config", () => {
|
|
21
|
+
const customRetry = new Retry({ retries: 5, delay: 500 });
|
|
22
|
+
assert.ok(customRetry instanceof Retry);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("retry mechanism works with exhausted retries on 429", async () => {
|
|
26
|
+
const retryResponse = {
|
|
27
|
+
ok: false,
|
|
28
|
+
status: 429,
|
|
29
|
+
statusText: "Too Many Requests",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mockFetch = mock.fn(() => Promise.resolve(retryResponse));
|
|
33
|
+
|
|
34
|
+
const response = await retry.execute(mockFetch);
|
|
35
|
+
|
|
36
|
+
// Should exhaust all retries and return the 429 response
|
|
37
|
+
assert.strictEqual(mockFetch.mock.callCount(), 11); // Initial + 10 retries
|
|
38
|
+
assert.strictEqual(response.status, 429);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("retries on 502 Bad Gateway errors", async () => {
|
|
42
|
+
const retryResponse = {
|
|
43
|
+
ok: false,
|
|
44
|
+
status: 502,
|
|
45
|
+
statusText: "Bad Gateway",
|
|
46
|
+
};
|
|
47
|
+
const successResponse = {
|
|
48
|
+
ok: true,
|
|
49
|
+
status: 200,
|
|
50
|
+
json: mock.fn(() => Promise.resolve({ data: "success" })),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let callCount = 0;
|
|
54
|
+
const mockFetch = mock.fn(() => {
|
|
55
|
+
callCount++;
|
|
56
|
+
if (callCount === 1) {
|
|
57
|
+
return Promise.resolve(retryResponse);
|
|
58
|
+
}
|
|
59
|
+
return Promise.resolve(successResponse);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await retry.execute(mockFetch);
|
|
63
|
+
|
|
64
|
+
// Should retry once and succeed
|
|
65
|
+
assert(mockFetch.mock.callCount() >= 2);
|
|
66
|
+
assert.strictEqual(result.ok, true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("retries on 503 Service Unavailable errors", async () => {
|
|
70
|
+
const retryResponse = {
|
|
71
|
+
ok: false,
|
|
72
|
+
status: 503,
|
|
73
|
+
statusText: "Service Unavailable",
|
|
74
|
+
};
|
|
75
|
+
const successResponse = {
|
|
76
|
+
ok: true,
|
|
77
|
+
status: 200,
|
|
78
|
+
json: mock.fn(() => Promise.resolve({ data: "success" })),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let callCount = 0;
|
|
82
|
+
const mockFetch = mock.fn(() => {
|
|
83
|
+
callCount++;
|
|
84
|
+
if (callCount === 1) {
|
|
85
|
+
return Promise.resolve(retryResponse);
|
|
86
|
+
}
|
|
87
|
+
return Promise.resolve(successResponse);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await retry.execute(mockFetch);
|
|
91
|
+
|
|
92
|
+
// Should retry once and succeed
|
|
93
|
+
assert(mockFetch.mock.callCount() >= 2);
|
|
94
|
+
assert.strictEqual(result.ok, true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("retries on 504 Gateway Timeout errors", async () => {
|
|
98
|
+
const retryResponse = {
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 504,
|
|
101
|
+
statusText: "Gateway Timeout",
|
|
102
|
+
};
|
|
103
|
+
const successResponse = {
|
|
104
|
+
ok: true,
|
|
105
|
+
status: 200,
|
|
106
|
+
json: mock.fn(() => Promise.resolve({ data: "success" })),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let callCount = 0;
|
|
110
|
+
const mockFetch = mock.fn(() => {
|
|
111
|
+
callCount++;
|
|
112
|
+
if (callCount <= 2) {
|
|
113
|
+
return Promise.resolve(retryResponse);
|
|
114
|
+
}
|
|
115
|
+
return Promise.resolve(successResponse);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = await retry.execute(mockFetch);
|
|
119
|
+
|
|
120
|
+
// Should retry twice and succeed
|
|
121
|
+
assert(mockFetch.mock.callCount() >= 3);
|
|
122
|
+
assert.strictEqual(result.ok, true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("retries on network errors", async () => {
|
|
126
|
+
const successResponse = {
|
|
127
|
+
ok: true,
|
|
128
|
+
status: 200,
|
|
129
|
+
json: mock.fn(() => Promise.resolve({ data: "success" })),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let callCount = 0;
|
|
133
|
+
const mockFetch = mock.fn(() => {
|
|
134
|
+
callCount++;
|
|
135
|
+
if (callCount === 1) {
|
|
136
|
+
return Promise.reject(new Error("Network error: Connection refused"));
|
|
137
|
+
}
|
|
138
|
+
return Promise.resolve(successResponse);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = await retry.execute(mockFetch);
|
|
142
|
+
|
|
143
|
+
// Should retry once and succeed
|
|
144
|
+
assert(mockFetch.mock.callCount() >= 2);
|
|
145
|
+
assert.strictEqual(result.ok, true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("non-retryable errors do not trigger retries", async () => {
|
|
149
|
+
const errorResponse = {
|
|
150
|
+
ok: false,
|
|
151
|
+
status: 400,
|
|
152
|
+
statusText: "Bad Request",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const mockFetch = mock.fn(() => Promise.resolve(errorResponse));
|
|
156
|
+
|
|
157
|
+
const response = await retry.execute(mockFetch);
|
|
158
|
+
|
|
159
|
+
// Should only make one call for non-retryable errors
|
|
160
|
+
assert.strictEqual(mockFetch.mock.callCount(), 1);
|
|
161
|
+
assert.strictEqual(response.status, 400);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("500 Internal Server Error triggers retries", async () => {
|
|
165
|
+
const errorResponse = {
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 500,
|
|
168
|
+
statusText: "Internal Server Error",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const mockFetch = mock.fn(() => Promise.resolve(errorResponse));
|
|
172
|
+
|
|
173
|
+
const response = await retry.execute(mockFetch);
|
|
174
|
+
|
|
175
|
+
// Should retry for 500 errors (retries + 1 initial attempt = 11 calls)
|
|
176
|
+
assert.strictEqual(mockFetch.mock.callCount(), 11);
|
|
177
|
+
assert.strictEqual(response.status, 500);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("executes successfully without validation function", async () => {
|
|
181
|
+
const successResponse = {
|
|
182
|
+
ok: true,
|
|
183
|
+
status: 200,
|
|
184
|
+
json: mock.fn(() => Promise.resolve({ data: "success" })),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const mockFetch = mock.fn(() => Promise.resolve(successResponse));
|
|
188
|
+
|
|
189
|
+
const result = await retry.execute(mockFetch);
|
|
190
|
+
|
|
191
|
+
assert.strictEqual(mockFetch.mock.callCount(), 1);
|
|
192
|
+
assert.strictEqual(result.ok, true);
|
|
193
|
+
});
|
|
194
|
+
});
|