@controlium/utils 1.0.2-alpha.1 → 1.0.2-alpha.3
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/dist/esm/apiUtils/APIUtils.js +226 -0
- package/dist/esm/detokeniser/detokeniser.js +50 -170
- package/dist/esm/index.js +1 -0
- package/dist/esm/logger/logger.js +5 -3
- package/dist/esm/mock/mock.js +519 -0
- package/dist/esm/utils/utils.js +3 -2
- package/dist/types/apiUtils/APIUtils.d.ts +90 -0
- package/dist/types/detokeniser/detokeniser.d.ts +46 -128
- package/dist/types/index.d.ts +1 -0
- package/dist/types/logger/types.d.ts +3 -1
- package/dist/types/mock/mock.d.ts +393 -0
- package/dist/types/utils/utils.d.ts +4 -4
- package/package.json +19 -55
- package/dist/cjs/detokeniser/detokeniser.js +0 -1135
- package/dist/cjs/index.js +0 -14
- package/dist/cjs/jsonUtils/jsonUtils.js +0 -460
- package/dist/cjs/logger/logger.js +0 -863
- package/dist/cjs/logger/logger.spec.js +0 -875
- package/dist/cjs/logger/types.js +0 -2
- package/dist/cjs/stringUtils/stringUtils.js +0 -294
- package/dist/cjs/utils/utils.js +0 -1050
- package/dist/esm/logger/logger.spec.js +0 -873
- package/dist/types/logger/logger.spec.d.ts +0 -1
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
* - **expression** — what to compute (type-specific)
|
|
11
11
|
* - **format** — how to format the result (type-specific, often optional)
|
|
12
12
|
*
|
|
13
|
-
* The delimiter `|` and the `[[` / `]]` endstops are configurable via {@link Detokeniser.delimiter} and
|
|
14
|
-
* {@link Detokeniser.tokenStartEndChars}, but the defaults cover the vast majority of use cases.
|
|
15
|
-
*
|
|
16
13
|
* ---
|
|
17
14
|
* ## Built-in token types
|
|
18
15
|
*
|
|
@@ -117,54 +114,26 @@
|
|
|
117
114
|
*
|
|
118
115
|
* ---
|
|
119
116
|
* ## Extending with callbacks
|
|
120
|
-
* Custom token types are registered via {@link Detokeniser.
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
117
|
+
* Custom token types are registered via {@link Detokeniser.addCallback}. Both sync and async handlers
|
|
118
|
+
* share the same registration method and callback list. Callbacks are tried in registration order;
|
|
119
|
+
* return `undefined` to pass to the next callback. If all callbacks return `undefined` and no built-in
|
|
120
|
+
* handler matched, an error is thrown.
|
|
121
|
+
*
|
|
122
|
+
* Async callbacks are silently skipped when {@link Detokeniser.do} (sync) is used — use
|
|
123
|
+
* {@link Detokeniser.doAsync} if your callback returns a Promise.
|
|
124
124
|
*
|
|
125
|
-
* @see {@link Detokeniser.
|
|
126
|
-
* @see {@link Detokeniser.addCallbackAsync}
|
|
125
|
+
* @see {@link Detokeniser.addCallback}
|
|
127
126
|
* @see {@link Detokeniser.do}
|
|
128
127
|
* @see {@link Detokeniser.doAsync}
|
|
129
128
|
*/
|
|
130
129
|
export declare class Detokeniser {
|
|
131
|
-
private static _endTokenChar;
|
|
132
|
-
private static _startTokenChar;
|
|
130
|
+
private static readonly _endTokenChar;
|
|
131
|
+
private static readonly _startTokenChar;
|
|
133
132
|
private static EscapeChar;
|
|
134
|
-
private static _delimiter;
|
|
135
|
-
private static
|
|
136
|
-
private static _syncCallbacks;
|
|
137
|
-
/**
|
|
138
|
-
* Gets the current single-character delimiter used to separate token parts.
|
|
139
|
-
* Default: `|`
|
|
140
|
-
*/
|
|
141
|
-
static get delimiter(): string;
|
|
142
|
-
/**
|
|
143
|
-
* Sets the single-character delimiter used to separate token parts.
|
|
144
|
-
* The new value applies to all subsequent {@link Detokeniser.do} / {@link Detokeniser.doAsync} calls.
|
|
145
|
-
* @param newDelimiter - Exactly one character
|
|
146
|
-
* @throws If `newDelimiter` is not exactly one character
|
|
147
|
-
* @example
|
|
148
|
-
* Detokeniser.delimiter = ':';
|
|
149
|
-
* Detokeniser.do('[[random:digits:6]]'); // → e.g. '482910'
|
|
150
|
-
* Detokeniser.reset(); // restore default '|'
|
|
151
|
-
*/
|
|
152
|
-
static set delimiter(newDelimiter: string);
|
|
153
|
-
/**
|
|
154
|
-
* Sets the start and end token sequences.
|
|
155
|
-
* @param startEndChars - An even-length string whose first half is the start sequence and second half the end sequence.
|
|
156
|
-
* @example Detokeniser.tokenStartEndChars = "[[]]"; // start = "[[", end = "]]"
|
|
157
|
-
* @remarks Start and end sequences must differ. Minimum total length is 2 (one char each).
|
|
158
|
-
*/
|
|
159
|
-
static set tokenStartEndChars(startEndChars: string);
|
|
160
|
-
/**
|
|
161
|
-
* Gets the current token start/end sequences concatenated (e.g. `"[[]]"`).
|
|
162
|
-
*/
|
|
163
|
-
static get tokenStartEndChars(): string;
|
|
133
|
+
private static readonly _delimiter;
|
|
134
|
+
private static _callbacks;
|
|
164
135
|
/**
|
|
165
136
|
* Resets the Detokeniser to factory defaults:
|
|
166
|
-
* - Token endstops restored to `[[` / `]]`
|
|
167
|
-
* - Delimiter restored to `|`
|
|
168
137
|
* - Escape char restored to `/`
|
|
169
138
|
* - All registered sync and async callbacks cleared
|
|
170
139
|
*
|
|
@@ -174,90 +143,48 @@ export declare class Detokeniser {
|
|
|
174
143
|
*/
|
|
175
144
|
static reset(): void;
|
|
176
145
|
/**
|
|
177
|
-
* Registers a
|
|
146
|
+
* Registers a custom token handler. Both sync and async handlers are registered via this method
|
|
147
|
+
* and share the same callback list.
|
|
148
|
+
*
|
|
149
|
+
* Callbacks are tried in registration order. The first to return a non-`undefined` value wins.
|
|
150
|
+
* Return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
|
|
151
|
+
* built-in handler matched, an error is thrown.
|
|
178
152
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* Return `undefined` to pass to the next callback. If all callbacks return `undefined`, an error is thrown.
|
|
153
|
+
* Async callbacks (those returning a `Promise`) are silently skipped when {@link Detokeniser.do}
|
|
154
|
+
* is used — register an async callback only if you intend to call {@link Detokeniser.doAsync}.
|
|
182
155
|
*
|
|
183
|
-
* @param callback - `(
|
|
184
|
-
* - `
|
|
185
|
-
* - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
|
|
156
|
+
* @param callback - `(token: string) => string | undefined | Promise<string | undefined>`
|
|
157
|
+
* - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`)
|
|
186
158
|
*
|
|
187
159
|
* @example
|
|
188
|
-
* //
|
|
189
|
-
* Detokeniser.
|
|
190
|
-
* const [type, name] = token.split(
|
|
191
|
-
* if (type
|
|
160
|
+
* // Sync handler for [[env|VAR_NAME]] tokens
|
|
161
|
+
* Detokeniser.addCallback((token) => {
|
|
162
|
+
* const [type, name] = token.split('|');
|
|
163
|
+
* if (type !== 'env') return undefined;
|
|
192
164
|
* return process.env[name] ?? '';
|
|
193
165
|
* });
|
|
194
166
|
* Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user'
|
|
195
167
|
*
|
|
196
168
|
* @example
|
|
197
|
-
* //
|
|
198
|
-
* Detokeniser.
|
|
199
|
-
* const [type,
|
|
200
|
-
* if (type
|
|
201
|
-
* return undefined;
|
|
202
|
-
* });
|
|
203
|
-
* Detokeniser.addCallbackSync((delimiter, token) => {
|
|
204
|
-
* const [type, value] = token.split(delimiter);
|
|
205
|
-
* if (type === 'lower') return value.toLowerCase();
|
|
206
|
-
* return undefined;
|
|
207
|
-
* });
|
|
208
|
-
* Detokeniser.do('[[upper|hello]] [[lower|WORLD]]'); // → 'HELLO world'
|
|
209
|
-
*
|
|
210
|
-
* @see {@link Detokeniser.resetSyncCallbacks} to remove all sync callbacks
|
|
211
|
-
* @see {@link Detokeniser.addCallbackAsync} for async token handlers
|
|
212
|
-
*/
|
|
213
|
-
static addCallbackSync(callback: Detokeniser.Callback_Sync): void;
|
|
214
|
-
/**
|
|
215
|
-
* Registers an asynchronous custom token handler.
|
|
216
|
-
*
|
|
217
|
-
* Works identically to {@link Detokeniser.addCallbackSync} but is invoked by {@link Detokeniser.doAsync}.
|
|
218
|
-
* Async callbacks are tried in registration order; the first to return a non-`undefined` value wins.
|
|
219
|
-
* Return `undefined` to pass to the next callback.
|
|
220
|
-
*
|
|
221
|
-
* @param asyncCallback - `(delimiter: string, token: string) => Promise<string | undefined>`
|
|
222
|
-
* - `delimiter` — current delimiter character (default `|`)
|
|
223
|
-
* - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
* // Handle [[db|table|column|whereClause]] tokens
|
|
227
|
-
* Detokeniser.addCallbackAsync(async (delimiter, token) => {
|
|
228
|
-
* const [type, table, column, where] = token.split(delimiter);
|
|
229
|
-
* if (type.toLowerCase() !== 'db') return undefined;
|
|
169
|
+
* // Async handler for [[db|table|column|where]] tokens
|
|
170
|
+
* Detokeniser.addCallback(async (token) => {
|
|
171
|
+
* const [type, table, column, where] = token.split('|');
|
|
172
|
+
* if (type !== 'db') return undefined;
|
|
230
173
|
* const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`);
|
|
231
174
|
* return String(row[column]);
|
|
232
175
|
* });
|
|
233
176
|
* const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]');
|
|
234
177
|
*
|
|
235
|
-
* @
|
|
236
|
-
*
|
|
237
|
-
* Detokeniser.addCallbackAsync(async (delimiter, token) => {
|
|
238
|
-
* const [type, key] = token.split(delimiter);
|
|
239
|
-
* if (type !== 'cache') return undefined;
|
|
240
|
-
* return await redis.get(key);
|
|
241
|
-
* });
|
|
242
|
-
* // [[random|digits|8]] resolves first, then [[cache|...]] receives the result
|
|
243
|
-
* await Detokeniser.doAsync('Val: [[cache|prefix-[[random|digits|8]]]]');
|
|
244
|
-
*
|
|
245
|
-
* @see {@link Detokeniser.resetAsyncCallbacks} to remove all async callbacks
|
|
246
|
-
* @see {@link Detokeniser.addCallbackSync} for synchronous token handlers
|
|
247
|
-
*/
|
|
248
|
-
static addCallbackAsync(asyncCallback: Detokeniser.Callback_Async): void;
|
|
249
|
-
/**
|
|
250
|
-
* Removes all registered sync callbacks. Built-in token handlers are unaffected.
|
|
251
|
-
* Use between tests or scenarios to ensure callback isolation.
|
|
252
|
-
* @see {@link Detokeniser.reset} to also clear async callbacks and restore all defaults
|
|
178
|
+
* @see {@link Detokeniser.resetCallbacks} to remove all registered callbacks
|
|
179
|
+
* @see {@link Detokeniser.doAsync} for async token resolution
|
|
253
180
|
*/
|
|
254
|
-
static
|
|
181
|
+
static addCallback(callback: Detokeniser.Callback): void;
|
|
255
182
|
/**
|
|
256
|
-
* Removes all registered
|
|
183
|
+
* Removes all registered callbacks. Built-in token handlers are unaffected.
|
|
257
184
|
* Use between tests or scenarios to ensure callback isolation.
|
|
258
|
-
* @see {@link Detokeniser.reset} to also
|
|
185
|
+
* @see {@link Detokeniser.reset} to also restore all defaults
|
|
259
186
|
*/
|
|
260
|
-
static
|
|
187
|
+
static resetCallbacks(): void;
|
|
261
188
|
/**
|
|
262
189
|
* Synchronously resolves all tokens in the given string and returns the result.
|
|
263
190
|
*
|
|
@@ -311,8 +238,8 @@ export declare class Detokeniser {
|
|
|
311
238
|
*
|
|
312
239
|
* @example
|
|
313
240
|
* // Async callback for database-driven tokens
|
|
314
|
-
* Detokeniser.addCallbackAsync(async (
|
|
315
|
-
* const [type, key] = token.split(
|
|
241
|
+
* Detokeniser.addCallbackAsync(async (token) => {
|
|
242
|
+
* const [type, key] = token.split('|');
|
|
316
243
|
* if (type !== 'db') return undefined;
|
|
317
244
|
* return await fetchFromDatabase(key);
|
|
318
245
|
* });
|
|
@@ -365,24 +292,16 @@ export declare class Detokeniser {
|
|
|
365
292
|
private static doRandomFloat;
|
|
366
293
|
}
|
|
367
294
|
/**
|
|
368
|
-
* Signature for
|
|
295
|
+
* Signature for a custom token handler registered via {@link Detokeniser.addCallback}.
|
|
369
296
|
*
|
|
370
|
-
*
|
|
371
|
-
* @
|
|
372
|
-
* @returns Promise resolving to the replacement string, or `undefined` to pass to the next handler
|
|
373
|
-
*/
|
|
374
|
-
export interface Detokeniser_Callback_Async {
|
|
375
|
-
(delimiter: string, token: string): Promise<string | undefined>;
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Signature for a synchronous custom token handler registered via {@link Detokeniser.addCallbackSync}.
|
|
297
|
+
* May be synchronous or asynchronous. Async callbacks (returning a `Promise`) are silently skipped
|
|
298
|
+
* by {@link Detokeniser.do} and only invoked by {@link Detokeniser.doAsync}.
|
|
379
299
|
*
|
|
380
|
-
* @param
|
|
381
|
-
* @
|
|
382
|
-
* @returns The replacement string, or `undefined` to pass to the next handler
|
|
300
|
+
* @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`)
|
|
301
|
+
* @returns The replacement string, a Promise resolving to it, or `undefined` to pass to the next handler
|
|
383
302
|
*/
|
|
384
|
-
export interface
|
|
385
|
-
(
|
|
303
|
+
export interface Detokeniser_Callback {
|
|
304
|
+
(token: string): string | undefined | Promise<string | undefined>;
|
|
386
305
|
}
|
|
387
306
|
/**
|
|
388
307
|
* Options passed to {@link Detokeniser.do} and {@link Detokeniser.doAsync}.
|
|
@@ -396,7 +315,6 @@ export interface Detokeniser_DoOptions {
|
|
|
396
315
|
contextParameters?: Record<string, unknown>;
|
|
397
316
|
}
|
|
398
317
|
export declare namespace Detokeniser {
|
|
399
|
-
type
|
|
400
|
-
type Callback_Sync = Detokeniser_Callback_Sync;
|
|
318
|
+
type Callback = Detokeniser_Callback;
|
|
401
319
|
type DoOptions = Detokeniser_DoOptions;
|
|
402
320
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,3 +16,4 @@ export { JsonUtils } from "./jsonUtils/jsonUtils";
|
|
|
16
16
|
export { StringUtils } from "./stringUtils/stringUtils";
|
|
17
17
|
export { Utils, ExistingFileWriteActions } from "./utils/utils";
|
|
18
18
|
export type { AssertTypeMap, ActionAndParams } from "./utils/utils";
|
|
19
|
+
export { Mock } from "./mock/mock";
|
|
@@ -130,7 +130,9 @@ export interface WriteLineOptions {
|
|
|
130
130
|
* // "MyTest.someStep (myTest.ts:45:3)"
|
|
131
131
|
* Logger.writeLine(Logger.Levels.TestInformation, message, { stackOffset: 1 });
|
|
132
132
|
*
|
|
133
|
-
*
|
|
133
|
+
* Values less than 1 are treated as 0 (no offset). Values that would exceed
|
|
134
|
+
* the available stack depth are clipped to the outermost frame.
|
|
135
|
+
*
|
|
134
136
|
*/
|
|
135
137
|
stackOffset?: number;
|
|
136
138
|
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static HTTP request interception and mocking utility for test suites.
|
|
3
|
+
*
|
|
4
|
+
* `Mock` is designed to sit between a test framework's network interceptor
|
|
5
|
+
* (e.g. Playwright's `page.route`) and the application under test. Every
|
|
6
|
+
* outgoing request from the AUT must be forwarded to {@link Mock.processInterceptedRequest}, which
|
|
7
|
+
* decides how to handle it based on the registered listeners.
|
|
8
|
+
*
|
|
9
|
+
* **Default-deny**: any request that does not match a listener is blocked and
|
|
10
|
+
* recorded as an `unmatched` transaction. The test suite has full control —
|
|
11
|
+
* nothing reaches the network unless explicitly permitted.
|
|
12
|
+
*
|
|
13
|
+
* **All transactions are stored** regardless of outcome, so test steps can
|
|
14
|
+
* later inspect real and mocked traffic via {@link Mock.getTransactions}.
|
|
15
|
+
*
|
|
16
|
+
* All methods are static — no instantiation is required. Call {@link Mock.reset}
|
|
17
|
+
* in `beforeEach` / `afterEach` hooks to start each test with a clean slate.
|
|
18
|
+
*
|
|
19
|
+
* ---
|
|
20
|
+
*
|
|
21
|
+
* ### Local mode (default)
|
|
22
|
+
*
|
|
23
|
+
* `Mock` runs in the same process as the test framework. When a `passthrough`
|
|
24
|
+
* listener matches, `Mock` fetches the real response itself and returns it
|
|
25
|
+
* directly. {@link Mock.processInterceptedRequest} always resolves to a {@link Mock.InterceptResult}
|
|
26
|
+
* with `action: 'fulfill'` or `action: 'block'`.
|
|
27
|
+
*
|
|
28
|
+
* ### Delegate mode
|
|
29
|
+
*
|
|
30
|
+
* Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote
|
|
31
|
+
* server (e.g. a mock proxy) and the caller — not `Mock` — is better placed
|
|
32
|
+
* to perform the real network fetch. In this mode, when a `passthrough`
|
|
33
|
+
* listener matches, {@link Mock.processInterceptedRequest} returns `action: 'passthrough'`
|
|
34
|
+
* plus a `correlationId`. The caller fetches the real response and reports it
|
|
35
|
+
* back via {@link Mock.completePassthrough}, allowing `Mock` to record the
|
|
36
|
+
* full transaction. Use {@link Mock.getPendingTransactions} to detect
|
|
37
|
+
* passthrough calls that were never completed.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // Local mode — Playwright adapter
|
|
42
|
+
* Mock.reset();
|
|
43
|
+
* Mock.addListener('get-users',
|
|
44
|
+
* ['$[?(@.url =~ /api\\/users/)]', '$[?(@.method == "GET")]'],
|
|
45
|
+
* { status: 200, body: [{ id: 1, name: 'Alice' }] }
|
|
46
|
+
* );
|
|
47
|
+
*
|
|
48
|
+
* await page.route('**\/*', async (route) => {
|
|
49
|
+
* const result = await Mock.intercept({
|
|
50
|
+
* url: route.request().url(),
|
|
51
|
+
* method: route.request().method(),
|
|
52
|
+
* headers: await route.request().allHeaders(),
|
|
53
|
+
* });
|
|
54
|
+
* if (result.action === 'fulfill') {
|
|
55
|
+
* await route.fulfill({ status: result.response.status });
|
|
56
|
+
* } else {
|
|
57
|
+
* await route.abort();
|
|
58
|
+
* }
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // Delegate mode — remote mock server
|
|
65
|
+
* Mock.delegateMode = true;
|
|
66
|
+
*
|
|
67
|
+
* // In the interceptor callback:
|
|
68
|
+
* const result = await Mock.intercept(request);
|
|
69
|
+
* if (result.action === 'passthrough') {
|
|
70
|
+
* const real = await myFetch(request);
|
|
71
|
+
* Mock.completePassthrough(result.correlationId, real);
|
|
72
|
+
* return real;
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare class Mock {
|
|
77
|
+
private static listeners;
|
|
78
|
+
private static transactions;
|
|
79
|
+
private static transactionCounter;
|
|
80
|
+
private static pendingTransactions;
|
|
81
|
+
private static _delegateMode;
|
|
82
|
+
/**
|
|
83
|
+
* When `true`, passthrough requests are delegated to the caller rather than
|
|
84
|
+
* fetched by `Mock` itself. {@link Mock.processInterceptedRequest} returns
|
|
85
|
+
* `action: 'passthrough'` with a `correlationId` for the caller to use when
|
|
86
|
+
* reporting the real response back via {@link Mock.completePassthrough}.
|
|
87
|
+
*
|
|
88
|
+
* Defaults to `false` (local mode — `Mock` fetches passthroughs itself).
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* Mock.delegateMode = true;
|
|
92
|
+
*/
|
|
93
|
+
static set delegateMode(value: boolean);
|
|
94
|
+
static get delegateMode(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Registers a named listener that matches intercepted requests and defines
|
|
97
|
+
* how `Mock` should respond to them.
|
|
98
|
+
*
|
|
99
|
+
* Listeners are evaluated in registration order. The first listener whose
|
|
100
|
+
* every matcher returns a result wins. If a listener with the same `name`
|
|
101
|
+
* already exists it is replaced and a warning is logged.
|
|
102
|
+
*
|
|
103
|
+
* @param name - Unique name for this listener. Used as the key in the
|
|
104
|
+
* listener store and appears in transaction records and log output.
|
|
105
|
+
* Must be a non-empty string.
|
|
106
|
+
*
|
|
107
|
+
* @param matchers - One or more JSONPath expressions evaluated against the
|
|
108
|
+
* {@link Mock.Request} object. All expressions must return at least one
|
|
109
|
+
* result for the listener to match (AND logic). Each expression is
|
|
110
|
+
* validated for syntactic correctness at registration time — an invalid
|
|
111
|
+
* expression throws immediately rather than silently failing at intercept
|
|
112
|
+
* time. Must be a non-empty array of non-empty strings.
|
|
113
|
+
*
|
|
114
|
+
* @param action - What to do when this listener matches:
|
|
115
|
+
* - `'block'` — abort the request and return `action: 'block'` to the caller.
|
|
116
|
+
* - `'passthrough'` — forward the request to the real endpoint (or delegate
|
|
117
|
+
* to the caller in {@link Mock.delegateMode}) and return the real response.
|
|
118
|
+
* - {@link Mock.Response} — return the supplied response object directly
|
|
119
|
+
* without touching the network. `status` must be a valid HTTP status
|
|
120
|
+
* code (100–599).
|
|
121
|
+
*
|
|
122
|
+
* @param delayMs - Optional delay in milliseconds applied before the
|
|
123
|
+
* response is returned, whether mocked, real, or blocked. Useful for
|
|
124
|
+
* simulating slow networks. Must be a non-negative finite number.
|
|
125
|
+
* Defaults to `0` (no delay).
|
|
126
|
+
*
|
|
127
|
+
* @throws {Error} If any argument fails validation.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* // Block all requests to an analytics endpoint
|
|
131
|
+
* Mock.addListener('block-analytics', ['$[?(@.url =~ /analytics/)]'], 'block');
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* // Return a mock response with a 2-second simulated delay
|
|
135
|
+
* Mock.addListener('slow-login',
|
|
136
|
+
* ['$[?(@.url =~ /api\\/login/)]'],
|
|
137
|
+
* { status: 200, headers: { 'content-type': 'application/json' }, body: { token: 'abc' } },
|
|
138
|
+
* 2000
|
|
139
|
+
* );
|
|
140
|
+
*/
|
|
141
|
+
static addListener(name: string, matchers: string[], action: Mock.ListenerAction, delayMs?: number): void;
|
|
142
|
+
/**
|
|
143
|
+
* Removes a previously registered listener by name.
|
|
144
|
+
*
|
|
145
|
+
* If no listener with the given name exists, a warning is logged and the
|
|
146
|
+
* call is a no-op.
|
|
147
|
+
*
|
|
148
|
+
* @param name - Name of the listener to remove. Must be a non-empty string.
|
|
149
|
+
* @throws {Error} If `name` is not a non-empty string.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* Mock.removeListener('get-users');
|
|
153
|
+
*/
|
|
154
|
+
static removeListener(name: string): void;
|
|
155
|
+
/**
|
|
156
|
+
* Removes all registered listeners.
|
|
157
|
+
*
|
|
158
|
+
* Transaction history and pending transactions are unaffected. Use
|
|
159
|
+
* {@link Mock.reset} to clear everything in a single call.
|
|
160
|
+
*/
|
|
161
|
+
static clearListeners(): void;
|
|
162
|
+
/**
|
|
163
|
+
* Processes an intercepted HTTP request and returns a {@link Mock.InterceptResult}
|
|
164
|
+
* describing what the caller should do.
|
|
165
|
+
*
|
|
166
|
+
* Listeners are evaluated in registration order; the first full match wins.
|
|
167
|
+
* Every request — matched or not — is recorded in the transaction history.
|
|
168
|
+
* If the matching listener has a `delayMs`, the delay is applied before the
|
|
169
|
+
* result is returned.
|
|
170
|
+
*
|
|
171
|
+
* | Situation | `action` returned | Transaction type |
|
|
172
|
+
* |---|---|---|
|
|
173
|
+
* | Matched → mock response | `'fulfill'` | `'mocked'` |
|
|
174
|
+
* | Matched → passthrough, local mode | `'fulfill'` | `'passthrough'` |
|
|
175
|
+
* | Matched → passthrough, delegate mode | `'passthrough'` | pending until {@link Mock.completePassthrough} |
|
|
176
|
+
* | Matched → block | `'block'` | `'blocked'` |
|
|
177
|
+
* | No listener matched | `'block'` | `'unmatched'` |
|
|
178
|
+
*
|
|
179
|
+
* @param interceptedRequest - The intercepted request. Must be a non-null object with
|
|
180
|
+
* non-empty `url` and `method` string properties.
|
|
181
|
+
*
|
|
182
|
+
* @returns A promise resolving to a {@link Mock.InterceptResult}.
|
|
183
|
+
*
|
|
184
|
+
* @throws {Error} If `request` fails validation.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const result = await Mock.intercept(request);
|
|
188
|
+
* if (result.action === 'fulfill') {
|
|
189
|
+
* respondWith(result.response);
|
|
190
|
+
* } else if (result.action === 'passthrough') {
|
|
191
|
+
* // delegate mode only
|
|
192
|
+
* const real = await fetch(request.url);
|
|
193
|
+
* Mock.completePassthrough(result.correlationId, real);
|
|
194
|
+
* respondWith(real);
|
|
195
|
+
* } else {
|
|
196
|
+
* abortRequest();
|
|
197
|
+
* }
|
|
198
|
+
*/
|
|
199
|
+
static processInterceptedRequest(interceptedRequest: Mock.Request): Promise<Mock.InterceptResult>;
|
|
200
|
+
/**
|
|
201
|
+
* Completes a delegated passthrough by supplying the real response the caller
|
|
202
|
+
* fetched. Records the full transaction and removes the pending entry.
|
|
203
|
+
*
|
|
204
|
+
* Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId`
|
|
205
|
+
* must match one previously returned by {@link Mock.processInterceptedRequest}.
|
|
206
|
+
*
|
|
207
|
+
* @param correlationId - The UUID returned in the `action: 'passthrough'`
|
|
208
|
+
* result from {@link Mock.processInterceptedRequest}. Must be a non-empty string that
|
|
209
|
+
* matches a pending transaction.
|
|
210
|
+
*
|
|
211
|
+
* @param response - The real response the caller received from the endpoint.
|
|
212
|
+
* Must be a valid {@link Mock.Response} with a status code of 100–599.
|
|
213
|
+
*
|
|
214
|
+
* @throws {Error} If `correlationId` is invalid, unknown, or already completed.
|
|
215
|
+
* @throws {Error} If `response` fails validation.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* const result = await Mock.intercept(request);
|
|
219
|
+
* if (result.action === 'passthrough') {
|
|
220
|
+
* const real = await myFetch(request);
|
|
221
|
+
* Mock.completePassthrough(result.correlationId, real);
|
|
222
|
+
* }
|
|
223
|
+
*/
|
|
224
|
+
static completePassthrough(correlationId: string, response: Mock.Response): void;
|
|
225
|
+
/**
|
|
226
|
+
* Returns all completed transactions in chronological order.
|
|
227
|
+
*
|
|
228
|
+
* Includes mocked, passthrough (completed), blocked, and unmatched entries.
|
|
229
|
+
* Delegated passthroughs awaiting {@link Mock.completePassthrough} appear in
|
|
230
|
+
* {@link Mock.getPendingTransactions} instead.
|
|
231
|
+
*
|
|
232
|
+
* @returns A read-only array of {@link Mock.Transaction} objects.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* const blocked = Mock.getTransactions().filter(t => t.type === 'blocked');
|
|
236
|
+
*/
|
|
237
|
+
static getTransactions(): readonly Mock.Transaction[];
|
|
238
|
+
/**
|
|
239
|
+
* Clears all completed transactions and resets the transaction counter.
|
|
240
|
+
*
|
|
241
|
+
* Pending passthrough transactions and registered listeners are unaffected.
|
|
242
|
+
* Use {@link Mock.reset} to clear everything in a single call.
|
|
243
|
+
*/
|
|
244
|
+
static clearTransactions(): void;
|
|
245
|
+
/**
|
|
246
|
+
* Returns all delegated passthrough transactions that have not yet been
|
|
247
|
+
* completed via {@link Mock.completePassthrough}.
|
|
248
|
+
*
|
|
249
|
+
* Use this in test assertions to verify no passthrough calls were abandoned,
|
|
250
|
+
* or to diagnose remote fetch failures.
|
|
251
|
+
*
|
|
252
|
+
* Only populated when {@link Mock.delegateMode} is `true`.
|
|
253
|
+
*
|
|
254
|
+
* @returns A read-only array of {@link Mock.PendingTransaction} objects.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* // Assert no orphaned passthroughs after a test
|
|
258
|
+
* expect(Mock.getPendingTransactions()).toHaveLength(0);
|
|
259
|
+
*/
|
|
260
|
+
static getPendingTransactions(): readonly Mock.PendingTransaction[];
|
|
261
|
+
/**
|
|
262
|
+
* Clears all listeners, completed transactions, pending transactions, and
|
|
263
|
+
* resets delegate mode to `false`.
|
|
264
|
+
*
|
|
265
|
+
* Call this in `beforeEach` or `afterEach` hooks to guarantee a clean slate
|
|
266
|
+
* between tests.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* beforeEach(() => {
|
|
270
|
+
* Mock.reset();
|
|
271
|
+
* Mock.addListener('static-assets', [...], 'passthrough');
|
|
272
|
+
* });
|
|
273
|
+
*/
|
|
274
|
+
static reset(): void;
|
|
275
|
+
private static throwError;
|
|
276
|
+
private static findMatch;
|
|
277
|
+
private static fetchReal;
|
|
278
|
+
private static record;
|
|
279
|
+
}
|
|
280
|
+
export declare namespace Mock {
|
|
281
|
+
/**
|
|
282
|
+
* Represents an HTTP request as seen by the interceptor.
|
|
283
|
+
*
|
|
284
|
+
* The `url`, `method`, and `headers` fields are required. Additional
|
|
285
|
+
* properties (e.g. framework-specific metadata) may be included and are
|
|
286
|
+
* available to JSONPath matchers.
|
|
287
|
+
*/
|
|
288
|
+
interface Request {
|
|
289
|
+
/** The fully-qualified request URL. */
|
|
290
|
+
url: string;
|
|
291
|
+
/** HTTP method in uppercase, e.g. `'GET'`, `'POST'`. */
|
|
292
|
+
method: string;
|
|
293
|
+
/** Request headers as a plain key/value object. */
|
|
294
|
+
headers: Record<string, string>;
|
|
295
|
+
/** Parsed request body, if present. */
|
|
296
|
+
body?: unknown;
|
|
297
|
+
/** Any additional properties provided by the framework adapter. */
|
|
298
|
+
[key: string]: unknown;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Represents an HTTP response returned to the AUT, whether real or mocked.
|
|
302
|
+
*/
|
|
303
|
+
interface Response {
|
|
304
|
+
/** HTTP status code, e.g. `200`, `404`. Must be 100–599. */
|
|
305
|
+
status: number;
|
|
306
|
+
/** HTTP status text, eg. 'ok' or 'Internal Server Error' etc */
|
|
307
|
+
statusText?: string;
|
|
308
|
+
/** Response headers as a plain key/value object. */
|
|
309
|
+
headers?: Record<string, string>;
|
|
310
|
+
/** Response body. Objects are serialised to JSON by the framework adapter. */
|
|
311
|
+
body?: unknown;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Defines what a listener does when it matches a request.
|
|
315
|
+
*
|
|
316
|
+
* - `'block'` — abort the request; the AUT receives no response.
|
|
317
|
+
* - `'passthrough'` — forward to the real endpoint (or delegate to the caller
|
|
318
|
+
* in {@link Mock.delegateMode}) and return the actual response.
|
|
319
|
+
* - {@link Response} — return the supplied mock response without hitting the network.
|
|
320
|
+
*/
|
|
321
|
+
type ListenerAction = 'block' | 'passthrough' | Response;
|
|
322
|
+
/**
|
|
323
|
+
* The result returned by {@link Mock.processInterceptedRequest} to the framework adapter.
|
|
324
|
+
*
|
|
325
|
+
* - `action: 'fulfill'` — return `response` to the AUT.
|
|
326
|
+
* - `action: 'block'` — abort the request; the AUT receives nothing.
|
|
327
|
+
* - `action: 'passthrough'` — only in {@link Mock.delegateMode}. Fetch the
|
|
328
|
+
* real response using `request` details and report it back via
|
|
329
|
+
* {@link Mock.completePassthrough} using the supplied `correlationId`.
|
|
330
|
+
*/
|
|
331
|
+
type InterceptResult = {
|
|
332
|
+
action: 'fulfill';
|
|
333
|
+
response: Response;
|
|
334
|
+
} | {
|
|
335
|
+
action: 'block';
|
|
336
|
+
} | {
|
|
337
|
+
action: 'passthrough';
|
|
338
|
+
correlationId: string;
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Describes the outcome of a single completed intercepted request.
|
|
342
|
+
*
|
|
343
|
+
* - `'mocked'` — a mock {@link Response} was returned.
|
|
344
|
+
* - `'passthrough'` — the request was forwarded and the real response returned.
|
|
345
|
+
* - `'blocked'` — the request was explicitly blocked by a listener.
|
|
346
|
+
* - `'unmatched'` — no listener matched; the request was blocked by default.
|
|
347
|
+
*/
|
|
348
|
+
type TransactionType = 'mocked' | 'passthrough' | 'blocked' | 'unmatched';
|
|
349
|
+
/**
|
|
350
|
+
* A record of a single completed intercepted request and its outcome.
|
|
351
|
+
*
|
|
352
|
+
* Delegated passthroughs awaiting completion appear in
|
|
353
|
+
* {@link Mock.getPendingTransactions} as {@link PendingTransaction} until
|
|
354
|
+
* {@link Mock.completePassthrough} is called.
|
|
355
|
+
*/
|
|
356
|
+
interface Transaction {
|
|
357
|
+
/** Auto-incremented identifier, e.g. `'txn-1'`. Resets on {@link Mock.reset}. */
|
|
358
|
+
id: string;
|
|
359
|
+
/** Wall-clock time at which the request was intercepted. */
|
|
360
|
+
timestamp: Date;
|
|
361
|
+
/** Name of the listener that matched, if any. Absent for `'unmatched'` transactions. */
|
|
362
|
+
listenerName?: string;
|
|
363
|
+
/** How the request was handled. */
|
|
364
|
+
type: TransactionType;
|
|
365
|
+
/** The intercepted request. */
|
|
366
|
+
request: Request;
|
|
367
|
+
/** The response returned to the AUT. Absent for `'blocked'` and `'unmatched'` transactions. */
|
|
368
|
+
response?: Response;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* A delegated passthrough that has been issued by {@link Mock.processInterceptedRequest} but
|
|
372
|
+
* not yet completed via {@link Mock.completePassthrough}.
|
|
373
|
+
*
|
|
374
|
+
* Only created when {@link Mock.delegateMode} is `true`.
|
|
375
|
+
*/
|
|
376
|
+
interface PendingTransaction {
|
|
377
|
+
/** The UUID issued by {@link Mock.processInterceptedRequest} to identify this passthrough. */
|
|
378
|
+
correlationId: string;
|
|
379
|
+
/** Wall-clock time at which the passthrough was issued. */
|
|
380
|
+
timestamp: Date;
|
|
381
|
+
/** Name of the listener that triggered the passthrough. */
|
|
382
|
+
listenerName: string;
|
|
383
|
+
/** The original intercepted request. */
|
|
384
|
+
request: Request;
|
|
385
|
+
}
|
|
386
|
+
/** @internal */
|
|
387
|
+
interface Listener {
|
|
388
|
+
name: string;
|
|
389
|
+
matchers: string[];
|
|
390
|
+
action: ListenerAction;
|
|
391
|
+
delayMs: number;
|
|
392
|
+
}
|
|
393
|
+
}
|