@controlium/utils 1.0.2-alpha.1 → 1.0.2-alpha.2

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.
@@ -0,0 +1,509 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Mock = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const jsonpath_plus_1 = require("jsonpath-plus");
6
+ const utils_1 = require("../utils/utils");
7
+ const __1 = require("..");
8
+ /**
9
+ * Static HTTP request interception and mocking utility for test suites.
10
+ *
11
+ * `Mock` is designed to sit between a test framework's network interceptor
12
+ * (e.g. Playwright's `page.route`) and the application under test. Every
13
+ * outgoing request from the AUT is forwarded to {@link Mock.intercept}, which
14
+ * decides how to handle it based on the registered listeners.
15
+ *
16
+ * **Default-deny**: any request that does not match a listener is blocked and
17
+ * recorded as an `unmatched` transaction. The test suite has full control —
18
+ * nothing reaches the network unless explicitly permitted.
19
+ *
20
+ * **All transactions are stored** regardless of outcome, so test steps can
21
+ * later inspect real and mocked traffic via {@link Mock.getTransactions}.
22
+ *
23
+ * All methods are static — no instantiation is required. Call {@link Mock.reset}
24
+ * in `beforeEach` / `afterEach` hooks to start each test with a clean slate.
25
+ *
26
+ * ---
27
+ *
28
+ * ### Local mode (default)
29
+ *
30
+ * `Mock` runs in the same process as the test framework. When a `passthrough`
31
+ * listener matches, `Mock` fetches the real response itself and returns it
32
+ * directly. {@link Mock.intercept} always resolves to a {@link Mock.InterceptResult}
33
+ * with `action: 'fulfill'` or `action: 'block'`.
34
+ *
35
+ * ### Delegate mode
36
+ *
37
+ * Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote
38
+ * server (e.g. a mock proxy) and the caller — not `Mock` — is better placed
39
+ * to perform the real network fetch. In this mode, when a `passthrough`
40
+ * listener matches, {@link Mock.intercept} returns `action: 'passthrough'`
41
+ * plus a `correlationId`. The caller fetches the real response and reports it
42
+ * back via {@link Mock.completePassthrough}, allowing `Mock` to record the
43
+ * full transaction. Use {@link Mock.getPendingTransactions} to detect
44
+ * passthrough calls that were never completed.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // Local mode — Playwright adapter
49
+ * Mock.reset();
50
+ * Mock.addListener('get-users',
51
+ * ['$[?(@.url =~ /api\\/users/)]', '$[?(@.method == "GET")]'],
52
+ * { status: 200, body: [{ id: 1, name: 'Alice' }] }
53
+ * );
54
+ *
55
+ * await page.route('**\/*', async (route) => {
56
+ * const result = await Mock.intercept({
57
+ * url: route.request().url(),
58
+ * method: route.request().method(),
59
+ * headers: await route.request().allHeaders(),
60
+ * });
61
+ * if (result.action === 'fulfill') {
62
+ * await route.fulfill({ status: result.response.status });
63
+ * } else {
64
+ * await route.abort();
65
+ * }
66
+ * });
67
+ * ```
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * // Delegate mode — remote mock server
72
+ * Mock.delegateMode = true;
73
+ *
74
+ * // In the interceptor callback:
75
+ * const result = await Mock.intercept(request);
76
+ * if (result.action === 'passthrough') {
77
+ * const real = await myFetch(request);
78
+ * Mock.completePassthrough(result.correlationId, real);
79
+ * return real;
80
+ * }
81
+ * ```
82
+ */
83
+ class Mock {
84
+ // ── Configuration ──────────────────────────────────────────────────────────
85
+ /**
86
+ * When `true`, passthrough requests are delegated to the caller rather than
87
+ * fetched by `Mock` itself. {@link Mock.intercept} returns
88
+ * `action: 'passthrough'` with a `correlationId` for the caller to use when
89
+ * reporting the real response back via {@link Mock.completePassthrough}.
90
+ *
91
+ * Defaults to `false` (local mode — `Mock` fetches passthroughs itself).
92
+ *
93
+ * @example
94
+ * Mock.delegateMode = true;
95
+ */
96
+ static set delegateMode(value) {
97
+ utils_1.Utils.assertType(value, 'boolean', 'delegateMode', 'value');
98
+ __1.Log.writeLine(__1.LogLevels.FrameworkInformation, `Mock: delegate mode ${value ? 'enabled' : 'disabled'}`);
99
+ Mock._delegateMode = value;
100
+ }
101
+ static get delegateMode() {
102
+ return Mock._delegateMode;
103
+ }
104
+ // ── Listener management ────────────────────────────────────────────────────
105
+ /**
106
+ * Registers a named listener that matches intercepted requests and defines
107
+ * how `Mock` should respond to them.
108
+ *
109
+ * Listeners are evaluated in registration order. The first listener whose
110
+ * every matcher returns a result wins. If a listener with the same `name`
111
+ * already exists it is replaced and a warning is logged.
112
+ *
113
+ * @param name - Unique name for this listener. Used as the key in the
114
+ * listener store and appears in transaction records and log output.
115
+ * Must be a non-empty string.
116
+ *
117
+ * @param matchers - One or more JSONPath expressions evaluated against the
118
+ * {@link Mock.Request} object. All expressions must return at least one
119
+ * result for the listener to match (AND logic). Each expression is
120
+ * validated for syntactic correctness at registration time — an invalid
121
+ * expression throws immediately rather than silently failing at intercept
122
+ * time. Must be a non-empty array of non-empty strings.
123
+ *
124
+ * @param action - What to do when this listener matches:
125
+ * - `'block'` — abort the request and return `action: 'block'` to the caller.
126
+ * - `'passthrough'` — forward the request to the real endpoint (or delegate
127
+ * to the caller in {@link Mock.delegateMode}) and return the real response.
128
+ * - {@link Mock.Response} — return the supplied response object directly
129
+ * without touching the network. `status` must be a valid HTTP status
130
+ * code (100–599).
131
+ *
132
+ * @param delayMs - Optional delay in milliseconds applied before the
133
+ * response is returned, whether mocked, real, or blocked. Useful for
134
+ * simulating slow networks. Must be a non-negative finite number.
135
+ * Defaults to `0` (no delay).
136
+ *
137
+ * @throws {Error} If any argument fails validation.
138
+ *
139
+ * @example
140
+ * // Block all requests to an analytics endpoint
141
+ * Mock.addListener('block-analytics', ['$[?(@.url =~ /analytics/)]'], 'block');
142
+ *
143
+ * @example
144
+ * // Return a mock response with a 2-second simulated delay
145
+ * Mock.addListener('slow-login',
146
+ * ['$[?(@.url =~ /api\\/login/)]'],
147
+ * { status: 200, headers: { 'content-type': 'application/json' }, body: { token: 'abc' } },
148
+ * 2000
149
+ * );
150
+ */
151
+ static addListener(name, matchers, action, delayMs = 0) {
152
+ utils_1.Utils.assertType(name, 'string', 'addListener', 'name');
153
+ if (name.trim().length === 0)
154
+ Mock.throwError('addListener', '[name] must not be empty');
155
+ if (!Array.isArray(matchers)) {
156
+ Mock.throwError('addListener', `[matchers] must be a string array, got [${typeof matchers}]`);
157
+ }
158
+ if (matchers.length === 0) {
159
+ Mock.throwError('addListener', '[matchers] array must not be empty');
160
+ }
161
+ matchers.forEach((matcher, index) => {
162
+ if (typeof matcher !== 'string') {
163
+ Mock.throwError('addListener', `[matchers[${index}]] must be a string, got [${typeof matcher}]`);
164
+ }
165
+ if (matcher.trim().length === 0) {
166
+ Mock.throwError('addListener', `[matchers[${index}]] must not be empty`);
167
+ }
168
+ try {
169
+ (0, jsonpath_plus_1.JSONPath)({ path: matcher, json: {}, wrap: true });
170
+ }
171
+ catch (e) {
172
+ Mock.throwError('addListener', `[matchers[${index}]] is not valid JSONPath syntax: "${matcher}". ${e.message}`);
173
+ }
174
+ });
175
+ if (action !== 'block' && action !== 'passthrough') {
176
+ if (action === null || typeof action !== 'object' || Array.isArray(action)) {
177
+ Mock.throwError('addListener', `[action] must be 'block', 'passthrough', or a Mock.Response object, got [${action === null ? 'null' : typeof action}]`);
178
+ }
179
+ const response = action;
180
+ if (typeof response.status !== 'number' || !Number.isInteger(response.status) || response.status < 100 || response.status > 599) {
181
+ Mock.throwError('addListener', `[action.status] must be a valid HTTP status code (100-599), got [${response.status}]`);
182
+ }
183
+ if (response.headers !== undefined && (typeof response.headers !== 'object' || response.headers === null || Array.isArray(response.headers))) {
184
+ Mock.throwError('addListener', `[action.headers] must be a plain object if provided`);
185
+ }
186
+ }
187
+ utils_1.Utils.assertType(delayMs, 'number', 'addListener', 'delayMs');
188
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
189
+ Mock.throwError('addListener', `[delayMs] must be a non-negative finite number, got [${delayMs}]`);
190
+ }
191
+ if (Mock.listeners.has(name)) {
192
+ __1.Log.writeLine(__1.LogLevels.Warning, `Mock.addListener: replacing existing listener <${name}>`);
193
+ }
194
+ const isBlockOrPassthrough = action === 'block' || action === 'passthrough';
195
+ const actionText = (() => {
196
+ if (action === 'block')
197
+ return 'Block';
198
+ if (action === 'passthrough')
199
+ return 'Passthrough';
200
+ return JSON.stringify(action, null, 2);
201
+ })();
202
+ __1.Log.writeLine(__1.LogLevels.FrameworkInformation, `Add mock listener <${name}${delayMs > 0 ? ` (Delay by ${delayMs}mS)` : ''}>:\nMatcher: ${matchers.join('\nMatcher: ')}\nAction:${isBlockOrPassthrough ? ` ${actionText}` : `\n${actionText}`}`, { suppressMultilinePreamble: true });
203
+ Mock.listeners.set(name, { name, matchers, action, delayMs });
204
+ }
205
+ /**
206
+ * Removes a previously registered listener by name.
207
+ *
208
+ * If no listener with the given name exists, a warning is logged and the
209
+ * call is a no-op.
210
+ *
211
+ * @param name - Name of the listener to remove. Must be a non-empty string.
212
+ * @throws {Error} If `name` is not a non-empty string.
213
+ *
214
+ * @example
215
+ * Mock.removeListener('get-users');
216
+ */
217
+ static removeListener(name) {
218
+ utils_1.Utils.assertType(name, 'string', 'removeListener', 'name');
219
+ if (name.trim().length === 0)
220
+ Mock.throwError('removeListener', '[name] must not be empty');
221
+ if (!Mock.listeners.has(name)) {
222
+ __1.Log.writeLine(__1.LogLevels.Warning, `Mock.removeListener: no listener named <${name}> exists — nothing removed`);
223
+ return;
224
+ }
225
+ __1.Log.writeLine(__1.LogLevels.FrameworkInformation, `Mock: Remove listener <${name}>`);
226
+ Mock.listeners.delete(name);
227
+ }
228
+ /**
229
+ * Removes all registered listeners.
230
+ *
231
+ * Transaction history and pending transactions are unaffected. Use
232
+ * {@link Mock.reset} to clear everything in a single call.
233
+ */
234
+ static clearListeners() {
235
+ __1.Log.writeLine(__1.LogLevels.FrameworkInformation, `Mock: Clear all listeners (${Mock.listeners.size} removed)`);
236
+ Mock.listeners.clear();
237
+ }
238
+ // ── Intercept ──────────────────────────────────────────────────────────────
239
+ /**
240
+ * Processes an intercepted HTTP request and returns a {@link Mock.InterceptResult}
241
+ * describing what the caller should do.
242
+ *
243
+ * Listeners are evaluated in registration order; the first full match wins.
244
+ * Every request — matched or not — is recorded in the transaction history.
245
+ * If the matching listener has a `delayMs`, the delay is applied before the
246
+ * result is returned.
247
+ *
248
+ * | Situation | `action` returned | Transaction type |
249
+ * |---|---|---|
250
+ * | Matched → mock response | `'fulfill'` | `'mocked'` |
251
+ * | Matched → passthrough, local mode | `'fulfill'` | `'passthrough'` |
252
+ * | Matched → passthrough, delegate mode | `'passthrough'` | pending until {@link Mock.completePassthrough} |
253
+ * | Matched → block | `'block'` | `'blocked'` |
254
+ * | No listener matched | `'block'` | `'unmatched'` |
255
+ *
256
+ * @param request - The intercepted request. Must be a non-null object with
257
+ * non-empty `url` and `method` string properties.
258
+ *
259
+ * @returns A promise resolving to a {@link Mock.InterceptResult}.
260
+ *
261
+ * @throws {Error} If `request` fails validation.
262
+ *
263
+ * @example
264
+ * const result = await Mock.intercept(request);
265
+ * if (result.action === 'fulfill') {
266
+ * respondWith(result.response);
267
+ * } else if (result.action === 'passthrough') {
268
+ * // delegate mode only
269
+ * const real = await fetch(request.url);
270
+ * Mock.completePassthrough(result.correlationId, real);
271
+ * respondWith(real);
272
+ * } else {
273
+ * abortRequest();
274
+ * }
275
+ */
276
+ static async intercept(request) {
277
+ if (request === null || request === undefined || typeof request !== 'object' || Array.isArray(request)) {
278
+ Mock.throwError('intercept', `[request] must be a non-null object, got [${request === null ? 'null' : typeof request}]`);
279
+ }
280
+ if (typeof request.url !== 'string' || request.url.trim().length === 0) {
281
+ Mock.throwError('intercept', `[request.url] must be a non-empty string, got [${typeof request.url}]`);
282
+ }
283
+ if (typeof request.method !== 'string' || request.method.trim().length === 0) {
284
+ Mock.throwError('intercept', `[request.method] must be a non-empty string, got [${typeof request.method}]`);
285
+ }
286
+ if (request.headers !== undefined && request.headers !== null && (typeof request.headers !== 'object' || Array.isArray(request.headers))) {
287
+ Mock.throwError('intercept', `[request.headers] must be a plain object if provided`);
288
+ }
289
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: ${request.method} ${request.url}`);
290
+ const listener = Mock.findMatch(request);
291
+ if (!listener) {
292
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: no listener matched — blocking`);
293
+ Mock.record({ type: 'unmatched', request });
294
+ return { action: 'block' };
295
+ }
296
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: matched listener <${listener.name}>`);
297
+ if (listener.delayMs > 0) {
298
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: delaying ${listener.delayMs}ms`);
299
+ await utils_1.Utils.sleep(listener.delayMs);
300
+ }
301
+ if (listener.action === 'block') {
302
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: blocking request`);
303
+ Mock.record({ type: 'blocked', listenerName: listener.name, request });
304
+ return { action: 'block' };
305
+ }
306
+ if (listener.action === 'passthrough') {
307
+ if (Mock._delegateMode) {
308
+ const correlationId = (0, node_crypto_1.randomUUID)();
309
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: delegating passthrough (correlationId: ${correlationId})`);
310
+ Mock.pendingTransactions.set(correlationId, {
311
+ correlationId,
312
+ timestamp: new Date(),
313
+ listenerName: listener.name,
314
+ request,
315
+ });
316
+ return { action: 'passthrough', correlationId };
317
+ }
318
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: passing through to real endpoint`);
319
+ const response = await Mock.fetchReal(request);
320
+ Mock.record({ type: 'passthrough', listenerName: listener.name, request, response });
321
+ return { action: 'fulfill', response };
322
+ }
323
+ const response = listener.action;
324
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.intercept: returning mocked response (status ${response.status})`);
325
+ Mock.record({ type: 'mocked', listenerName: listener.name, request, response });
326
+ return { action: 'fulfill', response };
327
+ }
328
+ /**
329
+ * Completes a delegated passthrough by supplying the real response the caller
330
+ * fetched. Records the full transaction and removes the pending entry.
331
+ *
332
+ * Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId`
333
+ * must match one previously returned by {@link Mock.intercept}.
334
+ *
335
+ * @param correlationId - The UUID returned in the `action: 'passthrough'`
336
+ * result from {@link Mock.intercept}. Must be a non-empty string that
337
+ * matches a pending transaction.
338
+ *
339
+ * @param response - The real response the caller received from the endpoint.
340
+ * Must be a valid {@link Mock.Response} with a status code of 100–599.
341
+ *
342
+ * @throws {Error} If `correlationId` is invalid, unknown, or already completed.
343
+ * @throws {Error} If `response` fails validation.
344
+ *
345
+ * @example
346
+ * const result = await Mock.intercept(request);
347
+ * if (result.action === 'passthrough') {
348
+ * const real = await myFetch(request);
349
+ * Mock.completePassthrough(result.correlationId, real);
350
+ * }
351
+ */
352
+ static completePassthrough(correlationId, response) {
353
+ utils_1.Utils.assertType(correlationId, 'string', 'completePassthrough', 'correlationId');
354
+ if (correlationId.trim().length === 0) {
355
+ Mock.throwError('completePassthrough', '[correlationId] must not be empty');
356
+ }
357
+ if (!Mock.pendingTransactions.has(correlationId)) {
358
+ Mock.throwError('completePassthrough', `[correlationId] "${correlationId}" does not match any pending passthrough — it may be unknown, already completed, or cleared by reset()`);
359
+ }
360
+ if (response === null || response === undefined || typeof response !== 'object' || Array.isArray(response)) {
361
+ Mock.throwError('completePassthrough', `[response] must be a non-null object, got [${response === null ? 'null' : typeof response}]`);
362
+ }
363
+ if (typeof response.status !== 'number' || !Number.isInteger(response.status) || response.status < 100 || response.status > 599) {
364
+ Mock.throwError('completePassthrough', `[response.status] must be a valid HTTP status code (100-599), got [${response.status}]`);
365
+ }
366
+ if (response.headers !== undefined && (typeof response.headers !== 'object' || response.headers === null || Array.isArray(response.headers))) {
367
+ Mock.throwError('completePassthrough', `[response.headers] must be a plain object if provided`);
368
+ }
369
+ const pending = Mock.pendingTransactions.get(correlationId);
370
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.completePassthrough: completing passthrough for correlationId ${correlationId} (status ${response.status})`);
371
+ Mock.pendingTransactions.delete(correlationId);
372
+ Mock.record({ type: 'passthrough', listenerName: pending.listenerName, request: pending.request, response });
373
+ }
374
+ // ── Transaction history ────────────────────────────────────────────────────
375
+ /**
376
+ * Returns all completed transactions in chronological order.
377
+ *
378
+ * Includes mocked, passthrough (completed), blocked, and unmatched entries.
379
+ * Delegated passthroughs awaiting {@link Mock.completePassthrough} appear in
380
+ * {@link Mock.getPendingTransactions} instead.
381
+ *
382
+ * @returns A read-only array of {@link Mock.Transaction} objects.
383
+ *
384
+ * @example
385
+ * const blocked = Mock.getTransactions().filter(t => t.type === 'blocked');
386
+ */
387
+ static getTransactions() {
388
+ return Mock.transactions;
389
+ }
390
+ /**
391
+ * Clears all completed transactions and resets the transaction counter.
392
+ *
393
+ * Pending passthrough transactions and registered listeners are unaffected.
394
+ * Use {@link Mock.reset} to clear everything in a single call.
395
+ */
396
+ static clearTransactions() {
397
+ __1.Log.writeLine(__1.LogLevels.FrameworkInformation, `Mock: Clear transaction history (${Mock.transactions.length} removed)`);
398
+ Mock.transactions = [];
399
+ Mock.transactionCounter = 0;
400
+ }
401
+ /**
402
+ * Returns all delegated passthrough transactions that have not yet been
403
+ * completed via {@link Mock.completePassthrough}.
404
+ *
405
+ * Use this in test assertions to verify no passthrough calls were abandoned,
406
+ * or to diagnose remote fetch failures.
407
+ *
408
+ * Only populated when {@link Mock.delegateMode} is `true`.
409
+ *
410
+ * @returns A read-only array of {@link Mock.PendingTransaction} objects.
411
+ *
412
+ * @example
413
+ * // Assert no orphaned passthroughs after a test
414
+ * expect(Mock.getPendingTransactions()).toHaveLength(0);
415
+ */
416
+ static getPendingTransactions() {
417
+ return [...Mock.pendingTransactions.values()];
418
+ }
419
+ // ── Reset ──────────────────────────────────────────────────────────────────
420
+ /**
421
+ * Clears all listeners, completed transactions, pending transactions, and
422
+ * resets delegate mode to `false`.
423
+ *
424
+ * Call this in `beforeEach` or `afterEach` hooks to guarantee a clean slate
425
+ * between tests.
426
+ *
427
+ * @example
428
+ * beforeEach(() => {
429
+ * Mock.reset();
430
+ * Mock.addListener('static-assets', [...], 'passthrough');
431
+ * });
432
+ */
433
+ static reset() {
434
+ __1.Log.writeLine(__1.LogLevels.FrameworkInformation, `Mock: Reset (${Mock.listeners.size} listeners, ${Mock.transactions.length} transactions, ${Mock.pendingTransactions.size} pending cleared)`);
435
+ Mock.listeners.clear();
436
+ Mock.transactions = [];
437
+ Mock.transactionCounter = 0;
438
+ Mock.pendingTransactions.clear();
439
+ Mock._delegateMode = false;
440
+ }
441
+ // ── Private helpers ────────────────────────────────────────────────────────
442
+ static throwError(funcName, message) {
443
+ const errorText = `Mock.${funcName}: ${message}`;
444
+ __1.Log.writeLine(__1.LogLevels.Error, errorText, { stackOffset: 1 });
445
+ throw new Error(errorText);
446
+ }
447
+ static findMatch(request) {
448
+ for (const listener of Mock.listeners.values()) {
449
+ try {
450
+ const allMatch = listener.matchers.every((path) => {
451
+ try {
452
+ const results = (0, jsonpath_plus_1.JSONPath)({ path, json: request, wrap: true });
453
+ return Array.isArray(results) && results.length > 0;
454
+ }
455
+ catch (e) {
456
+ __1.Log.writeLine(__1.LogLevels.Warning, `Mock: JSONPath evaluation error in listener <${listener.name}> for path "${path}": ${e.message} — treating as non-match`);
457
+ return false;
458
+ }
459
+ });
460
+ if (allMatch)
461
+ return listener;
462
+ }
463
+ catch (e) {
464
+ __1.Log.writeLine(__1.LogLevels.Warning, `Mock: Unexpected error evaluating listener <${listener.name}>: ${e.message} — skipping`);
465
+ }
466
+ }
467
+ return undefined;
468
+ }
469
+ static async fetchReal(request) {
470
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.fetchReal: ${request.method} ${request.url}`);
471
+ try {
472
+ const response = await fetch(request.url, {
473
+ method: request.method,
474
+ headers: request.headers,
475
+ body: request.body != null ? JSON.stringify(request.body) : undefined,
476
+ });
477
+ const body = await response.text().then((text) => {
478
+ try {
479
+ return JSON.parse(text);
480
+ }
481
+ catch {
482
+ return text;
483
+ }
484
+ });
485
+ const headers = {};
486
+ response.headers.forEach((value, key) => { headers[key] = value; });
487
+ __1.Log.writeLine(__1.LogLevels.FrameworkDebug, `Mock.fetchReal: received ${response.status} from ${request.url}`);
488
+ return { status: response.status, headers, body };
489
+ }
490
+ catch (e) {
491
+ const message = e.message;
492
+ __1.Log.writeLine(__1.LogLevels.Error, `Mock.fetchReal: network error fetching ${request.url}: ${message}`);
493
+ return { status: 502, headers: {}, body: `Mock passthrough network error: ${message}` };
494
+ }
495
+ }
496
+ static record(entry) {
497
+ Mock.transactions.push({
498
+ id: `txn-${++Mock.transactionCounter}`,
499
+ timestamp: new Date(),
500
+ ...entry,
501
+ });
502
+ }
503
+ }
504
+ exports.Mock = Mock;
505
+ Mock.listeners = new Map();
506
+ Mock.transactions = [];
507
+ Mock.transactionCounter = 0;
508
+ Mock.pendingTransactions = new Map();
509
+ Mock._delegateMode = false;