@controlium/utils 1.0.2-alpha.2 → 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.
@@ -0,0 +1,226 @@
1
+ import { Agent, Headers, ProxyAgent, fetch } from 'undici';
2
+ import { Log, LogLevels, Utils } from '../index';
3
+ export class EAAPIUtils {
4
+ /**
5
+ * Verify if HTTP server listening
6
+ * @param url
7
+ * Protocol and domain of HTTP Server (IE. http://localhost:4200)
8
+ * @param timeoutMS
9
+ * Maximum time (in Milliseconds to wait for response)
10
+ * @returns boolean
11
+ * true if Server alive and responding
12
+ * false if no response with timeout
13
+ * @abstract
14
+ * A fetch 'HEAD' request is used to obtain a header from the server. If no
15
+ * response then it is assumed nothing listening
16
+ */
17
+ static async isWebServerListening(url, timeoutMS) {
18
+ try {
19
+ await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(timeoutMS) });
20
+ return true;
21
+ }
22
+ catch (err) {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ if (err && typeof err === 'object' && 'errors' in err && Array.isArray(err.errors)) {
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const agg = err;
27
+ for (const e of agg.errors) {
28
+ Log.writeLine(LogLevels.FrameworkInformation, `Error:\n${e.code} (${e.message})`);
29
+ if (e.message.includes('ECONNREFUSED')) {
30
+ return false;
31
+ }
32
+ }
33
+ }
34
+ else {
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ Log.writeLine(LogLevels.FrameworkInformation, `Error:\n${err['code'] ?? 'no code'} (${err.message})`);
37
+ if (err.message.includes('ECONNREFUSED')) {
38
+ return false;
39
+ }
40
+ }
41
+ // We are only interested in if ECONNREFUSED. All else means there be _something_ listening...
42
+ return true;
43
+ }
44
+ }
45
+ /**
46
+ * Waits for coherent response from given HTTP url
47
+ * @param url
48
+ * Protocol and domain of HTTP Server (IE. http://localhost:4200)
49
+ * @param maxSecondsToWait
50
+ * Maximum time to wait (in seconds) for a coherent response from given url
51
+ * @param maxResponseTimeMS (optional, default 1000)
52
+ * Maximum time for a response (in milliseconds) to a http HEAD request
53
+ * @returns
54
+ * Promise of boolean
55
+ * true - Webserver on given url is alive and responding
56
+ * false - No response from given url within timeout.
57
+ * @abstract
58
+ * URL is polled
59
+ */
60
+ static async waitForWebServerListening(url, maxSecondsToWait, { maxResponseTimeMS = 1000 } = {}) {
61
+ const pollIntervalMs = 500;
62
+ Log.writeLine(LogLevels.TestInformation, `Waiting for AUT at ${url} to become available (overall timeout: ${maxSecondsToWait} seconds)...`);
63
+ const startTime = Date.now();
64
+ let elapsed = 0;
65
+ while (!await this.isWebServerListening(url, maxResponseTimeMS)) {
66
+ const oldElapsed = elapsed;
67
+ elapsed = Math.floor((Date.now() - startTime) / 1000);
68
+ if (elapsed >= maxSecondsToWait) {
69
+ Log.writeLine(LogLevels.Error, `Timeout reached: Server did not respond within ${maxSecondsToWait} seconds.`);
70
+ return false;
71
+ }
72
+ // Stick a confidence message out every 4 seconds
73
+ if ((oldElapsed != elapsed) && (elapsed % 4 == 0)) {
74
+ Log.writeLine(LogLevels.TestInformation, `Waiting for AUT: Waited ${elapsed} seconds so far (Max wait ${maxSecondsToWait} seconds)`);
75
+ }
76
+ await Utils.sleep(pollIntervalMs, false);
77
+ }
78
+ return true;
79
+ }
80
+ /**
81
+ * Perform a single HTTP/HTTPS operation based on the details of the Request envelope
82
+ * @param httpRequest - Details of Request to be performed
83
+ * @returns Full response
84
+ * @throws Error if there is any fail that results in a Response not being received.
85
+ * The caller-supplied timeout (or the default 10s) is enforced as a hard failsafe via AbortSignal.
86
+ */
87
+ static async performHTTPOperation(httpRequest) {
88
+ const API_DEFAULT_TIMEOUT = 10000;
89
+ let dispatcher;
90
+ try {
91
+ if (Utils.isNullOrUndefined(httpRequest.timeout)) {
92
+ Log.writeLine(LogLevels.FrameworkDebug, `No API Timeout defined. Setting to ${Utils.msToHMS(API_DEFAULT_TIMEOUT)}`);
93
+ httpRequest.timeout = API_DEFAULT_TIMEOUT;
94
+ }
95
+ const builtUrl = this.buildURL(httpRequest);
96
+ Log.writeLine(LogLevels.FrameworkInformation, `Built URL: [${builtUrl}]`);
97
+ dispatcher = this.buildDispatcher(httpRequest);
98
+ const headers = this.buildHeaders(httpRequest.headers);
99
+ this.doRequestLogging(httpRequest.method ?? 'GET', builtUrl, headers, httpRequest.body);
100
+ const body = Utils.isNullOrUndefined(httpRequest.body)
101
+ ? undefined
102
+ : typeof httpRequest.body === 'string'
103
+ ? httpRequest.body
104
+ : JSON.stringify(httpRequest.body);
105
+ const fetchResponse = await fetch(builtUrl, {
106
+ method: httpRequest.method ?? 'GET',
107
+ headers,
108
+ body,
109
+ redirect: 'follow',
110
+ signal: AbortSignal.timeout(httpRequest.timeout),
111
+ dispatcher,
112
+ });
113
+ const responseBody = await fetchResponse.text();
114
+ this.doResponseLogging(fetchResponse.status, fetchResponse.statusText, responseBody);
115
+ return {
116
+ status: fetchResponse.status,
117
+ statusMessage: fetchResponse.statusText,
118
+ body: responseBody,
119
+ };
120
+ }
121
+ catch (err) {
122
+ Log.writeLine(LogLevels.Error, `HTTP OPERATION ERROR: ${err}`);
123
+ throw err;
124
+ }
125
+ finally {
126
+ await dispatcher?.close();
127
+ }
128
+ }
129
+ static buildURL(httpRequest) {
130
+ let builtUrl = httpRequest.protocol;
131
+ builtUrl += '://';
132
+ builtUrl += httpRequest.host.endsWith('/')
133
+ ? httpRequest.host.substring(0, httpRequest.host.length - 1)
134
+ : httpRequest.host;
135
+ builtUrl += '/';
136
+ builtUrl += httpRequest.resourcePath.startsWith('/')
137
+ ? httpRequest.resourcePath.substring(1, httpRequest.resourcePath.length)
138
+ : httpRequest.resourcePath;
139
+ if (!Utils.isNullOrUndefined(httpRequest.queryString)) {
140
+ const queryString = httpRequest.queryString;
141
+ builtUrl += '?';
142
+ builtUrl += queryString.startsWith('?')
143
+ ? queryString.substring(1, queryString.length)
144
+ : queryString;
145
+ }
146
+ return builtUrl;
147
+ }
148
+ static buildDispatcher(httpRequest) {
149
+ if (!Utils.isNullOrUndefined(httpRequest.proxy)) {
150
+ const proxyAgent = new ProxyAgent({
151
+ uri: httpRequest.proxy,
152
+ connect: { timeout: httpRequest.timeout, rejectUnauthorized: false },
153
+ });
154
+ Log.writeLine(LogLevels.FrameworkInformation, `Proxy configured: [${httpRequest.proxy}]`);
155
+ return proxyAgent;
156
+ }
157
+ return new Agent({ connect: { timeout: httpRequest.timeout } });
158
+ }
159
+ static buildHeaders(httpHeaders) {
160
+ const headers = new Headers();
161
+ for (const [key, value] of Object.entries(httpHeaders)) {
162
+ if (!Utils.isNull(value)) {
163
+ headers.append(key, String(value));
164
+ }
165
+ }
166
+ return headers;
167
+ }
168
+ static doRequestLogging(method, url, headers, body) {
169
+ Log.writeLine(LogLevels.FrameworkInformation, `HTTP [${method}] to [${url}]:-`);
170
+ Log.writeLine(LogLevels.FrameworkInformation, ' Headers;');
171
+ let headersStr = '';
172
+ headers.forEach((value, key) => {
173
+ headersStr += `${headersStr === '' ? '' : '\n'} "${key}": "${value}"`;
174
+ });
175
+ Log.writeLine(LogLevels.FrameworkInformation, headersStr === '' ? ' <No headers!>' : headersStr);
176
+ if (Log.loggingLevel >= LogLevels.FrameworkDebug) {
177
+ Log.writeLine(LogLevels.FrameworkDebug, ' Request (full body);');
178
+ const bodyStr = Utils.isNullOrUndefined(body)
179
+ ? ' <No body!>'
180
+ : typeof body === 'string' ? body : JSON.stringify(body, null, 2);
181
+ Log.writeLine(LogLevels.FrameworkDebug, bodyStr, { maxLines: 1024, suppressMultilinePreamble: true });
182
+ }
183
+ else {
184
+ Log.writeLine(LogLevels.FrameworkInformation, ' Body;');
185
+ const bodyStr = Utils.isNullOrUndefined(body)
186
+ ? ''
187
+ : typeof body === 'string' ? body : JSON.stringify(body);
188
+ if (!bodyStr) {
189
+ Log.writeLine(LogLevels.FrameworkInformation, ' <No body!>');
190
+ }
191
+ else {
192
+ let indented = '';
193
+ bodyStr.split(/\r?\n/).forEach((line) => {
194
+ indented += `${indented === '' ? '' : '\n'} ${line}`;
195
+ });
196
+ Log.writeLine(LogLevels.FrameworkInformation, indented);
197
+ }
198
+ }
199
+ }
200
+ static doResponseLogging(status, statusText, body) {
201
+ Log.writeLine(LogLevels.FrameworkInformation, 'HTTP Response:-');
202
+ Log.writeLine(LogLevels.FrameworkInformation, ` Status [${status}] - [${statusText}]`);
203
+ Log.writeLine(LogLevels.FrameworkInformation, ' Body;');
204
+ let indented = '';
205
+ if (body) {
206
+ body.split(/\r?\n/).forEach((line) => {
207
+ indented += `${indented === '' ? '' : '\n'} ${line}`;
208
+ });
209
+ }
210
+ Log.writeLine(LogLevels.FrameworkInformation, indented === '' ? ' <No body!>' : indented);
211
+ Log.writeLine(LogLevels.FrameworkInformation, 'HTTP Response end');
212
+ }
213
+ }
214
+ // eslint-disable-next-line @typescript-eslint/no-namespace
215
+ (function (EAAPIUtils) {
216
+ /**
217
+ * Generic Http call methods
218
+ */
219
+ let HttpMethods;
220
+ (function (HttpMethods) {
221
+ HttpMethods["POST"] = "POST";
222
+ HttpMethods["GET"] = "GET";
223
+ HttpMethods["PUT"] = "PUT";
224
+ })(HttpMethods = EAAPIUtils.HttpMethods || (EAAPIUtils.HttpMethods = {}));
225
+ EAAPIUtils.APPLICATION_JSON = 'application/json';
226
+ })(EAAPIUtils || (EAAPIUtils = {}));
@@ -1,13 +1,13 @@
1
+ import { STATUS_CODES } from 'node:http';
1
2
  import { randomUUID } from 'node:crypto';
2
- import { JSONPath } from 'jsonpath-plus';
3
3
  import { Utils } from '../utils/utils';
4
- import { Log, LogLevels } from '..';
4
+ import { JsonUtils, Log, Logger, LogLevels } from '..';
5
5
  /**
6
6
  * Static HTTP request interception and mocking utility for test suites.
7
7
  *
8
8
  * `Mock` is designed to sit between a test framework's network interceptor
9
9
  * (e.g. Playwright's `page.route`) and the application under test. Every
10
- * outgoing request from the AUT is forwarded to {@link Mock.intercept}, which
10
+ * outgoing request from the AUT must be forwarded to {@link Mock.processInterceptedRequest}, which
11
11
  * decides how to handle it based on the registered listeners.
12
12
  *
13
13
  * **Default-deny**: any request that does not match a listener is blocked and
@@ -26,7 +26,7 @@ import { Log, LogLevels } from '..';
26
26
  *
27
27
  * `Mock` runs in the same process as the test framework. When a `passthrough`
28
28
  * listener matches, `Mock` fetches the real response itself and returns it
29
- * directly. {@link Mock.intercept} always resolves to a {@link Mock.InterceptResult}
29
+ * directly. {@link Mock.processInterceptedRequest} always resolves to a {@link Mock.InterceptResult}
30
30
  * with `action: 'fulfill'` or `action: 'block'`.
31
31
  *
32
32
  * ### Delegate mode
@@ -34,7 +34,7 @@ import { Log, LogLevels } from '..';
34
34
  * Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote
35
35
  * server (e.g. a mock proxy) and the caller — not `Mock` — is better placed
36
36
  * to perform the real network fetch. In this mode, when a `passthrough`
37
- * listener matches, {@link Mock.intercept} returns `action: 'passthrough'`
37
+ * listener matches, {@link Mock.processInterceptedRequest} returns `action: 'passthrough'`
38
38
  * plus a `correlationId`. The caller fetches the real response and reports it
39
39
  * back via {@link Mock.completePassthrough}, allowing `Mock` to record the
40
40
  * full transaction. Use {@link Mock.getPendingTransactions} to detect
@@ -81,7 +81,7 @@ export class Mock {
81
81
  // ── Configuration ──────────────────────────────────────────────────────────
82
82
  /**
83
83
  * When `true`, passthrough requests are delegated to the caller rather than
84
- * fetched by `Mock` itself. {@link Mock.intercept} returns
84
+ * fetched by `Mock` itself. {@link Mock.processInterceptedRequest} returns
85
85
  * `action: 'passthrough'` with a `correlationId` for the caller to use when
86
86
  * reporting the real response back via {@link Mock.completePassthrough}.
87
87
  *
@@ -163,10 +163,10 @@ export class Mock {
163
163
  Mock.throwError('addListener', `[matchers[${index}]] must not be empty`);
164
164
  }
165
165
  try {
166
- JSONPath({ path: matcher, json: {}, wrap: true });
166
+ JsonUtils.getMatchingJSONPropertyCount({}, matcher);
167
167
  }
168
168
  catch (e) {
169
- Mock.throwError('addListener', `[matchers[${index}]] is not valid JSONPath syntax: "${matcher}". ${e.message}`);
169
+ Mock.throwError('addListener', `Matcher item [${index}] is not valid JSONPath syntax: "${matcher}". ${e.message}`);
170
170
  }
171
171
  });
172
172
  if (action !== 'block' && action !== 'passthrough') {
@@ -180,6 +180,10 @@ export class Mock {
180
180
  if (response.headers !== undefined && (typeof response.headers !== 'object' || response.headers === null || Array.isArray(response.headers))) {
181
181
  Mock.throwError('addListener', `[action.headers] must be a plain object if provided`);
182
182
  }
183
+ if (Utils.isUndefined(action.statusText))
184
+ action.statusText = STATUS_CODES[action.status] ?? undefined;
185
+ else if (action.statusText === '_undefined')
186
+ action.statusText = undefined;
183
187
  }
184
188
  Utils.assertType(delayMs, 'number', 'addListener', 'delayMs');
185
189
  if (!Number.isFinite(delayMs) || delayMs < 0) {
@@ -250,7 +254,7 @@ export class Mock {
250
254
  * | Matched → block | `'block'` | `'blocked'` |
251
255
  * | No listener matched | `'block'` | `'unmatched'` |
252
256
  *
253
- * @param request - The intercepted request. Must be a non-null object with
257
+ * @param interceptedRequest - The intercepted request. Must be a non-null object with
254
258
  * non-empty `url` and `method` string properties.
255
259
  *
256
260
  * @returns A promise resolving to a {@link Mock.InterceptResult}.
@@ -270,24 +274,24 @@ export class Mock {
270
274
  * abortRequest();
271
275
  * }
272
276
  */
273
- static async intercept(request) {
274
- if (request === null || request === undefined || typeof request !== 'object' || Array.isArray(request)) {
275
- Mock.throwError('intercept', `[request] must be a non-null object, got [${request === null ? 'null' : typeof request}]`);
277
+ static async processInterceptedRequest(interceptedRequest) {
278
+ if (interceptedRequest === null || interceptedRequest === undefined || typeof interceptedRequest !== 'object' || Array.isArray(interceptedRequest)) {
279
+ Mock.throwError('intercept', `[request] must be a non-null object, got [${interceptedRequest === null ? 'null' : typeof interceptedRequest}]`);
276
280
  }
277
- if (typeof request.url !== 'string' || request.url.trim().length === 0) {
278
- Mock.throwError('intercept', `[request.url] must be a non-empty string, got [${typeof request.url}]`);
281
+ if (typeof interceptedRequest.url !== 'string' || interceptedRequest.url.trim().length === 0) {
282
+ Mock.throwError('intercept', `[request.url] must be a non-empty string, got [${typeof interceptedRequest.url}]`);
279
283
  }
280
- if (typeof request.method !== 'string' || request.method.trim().length === 0) {
281
- Mock.throwError('intercept', `[request.method] must be a non-empty string, got [${typeof request.method}]`);
284
+ if (typeof interceptedRequest.method !== 'string' || interceptedRequest.method.trim().length === 0) {
285
+ Mock.throwError('intercept', `[request.method] must be a non-empty string, got [${typeof interceptedRequest.method}]`);
282
286
  }
283
- if (request.headers !== undefined && request.headers !== null && (typeof request.headers !== 'object' || Array.isArray(request.headers))) {
287
+ if (interceptedRequest.headers !== undefined && interceptedRequest.headers !== null && (typeof interceptedRequest.headers !== 'object' || Array.isArray(interceptedRequest.headers))) {
284
288
  Mock.throwError('intercept', `[request.headers] must be a plain object if provided`);
285
289
  }
286
- Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: ${request.method} ${request.url}`);
287
- const listener = Mock.findMatch(request);
290
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: ${interceptedRequest.method} ${interceptedRequest.url}`);
291
+ const listener = Mock.findMatch(interceptedRequest);
288
292
  if (!listener) {
289
293
  Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: no listener matched — blocking`);
290
- Mock.record({ type: 'unmatched', request });
294
+ Mock.record({ type: 'unmatched', request: interceptedRequest });
291
295
  return { action: 'block' };
292
296
  }
293
297
  Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: matched listener <${listener.name}>`);
@@ -297,7 +301,7 @@ export class Mock {
297
301
  }
298
302
  if (listener.action === 'block') {
299
303
  Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: blocking request`);
300
- Mock.record({ type: 'blocked', listenerName: listener.name, request });
304
+ Mock.record({ type: 'blocked', listenerName: listener.name, request: interceptedRequest });
301
305
  return { action: 'block' };
302
306
  }
303
307
  if (listener.action === 'passthrough') {
@@ -308,18 +312,18 @@ export class Mock {
308
312
  correlationId,
309
313
  timestamp: new Date(),
310
314
  listenerName: listener.name,
311
- request,
315
+ request: interceptedRequest,
312
316
  });
313
317
  return { action: 'passthrough', correlationId };
314
318
  }
315
319
  Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: passing through to real endpoint`);
316
- const response = await Mock.fetchReal(request);
317
- Mock.record({ type: 'passthrough', listenerName: listener.name, request, response });
320
+ const response = await Mock.fetchReal(interceptedRequest);
321
+ Mock.record({ type: 'passthrough', listenerName: listener.name, request: interceptedRequest, response });
318
322
  return { action: 'fulfill', response };
319
323
  }
320
324
  const response = listener.action;
321
325
  Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: returning mocked response (status ${response.status})`);
322
- Mock.record({ type: 'mocked', listenerName: listener.name, request, response });
326
+ Mock.record({ type: 'mocked', listenerName: listener.name, request: interceptedRequest, response });
323
327
  return { action: 'fulfill', response };
324
328
  }
325
329
  /**
@@ -327,10 +331,10 @@ export class Mock {
327
331
  * fetched. Records the full transaction and removes the pending entry.
328
332
  *
329
333
  * Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId`
330
- * must match one previously returned by {@link Mock.intercept}.
334
+ * must match one previously returned by {@link Mock.processInterceptedRequest}.
331
335
  *
332
336
  * @param correlationId - The UUID returned in the `action: 'passthrough'`
333
- * result from {@link Mock.intercept}. Must be a non-empty string that
337
+ * result from {@link Mock.processInterceptedRequest}. Must be a non-empty string that
334
338
  * matches a pending transaction.
335
339
  *
336
340
  * @param response - The real response the caller received from the endpoint.
@@ -442,20 +446,30 @@ export class Mock {
442
446
  throw new Error(errorText);
443
447
  }
444
448
  static findMatch(request) {
449
+ Logger.writeLine(LogLevels.FrameworkInformation, `Checking if any listeners for [${request.url} (${request.method})]`);
445
450
  for (const listener of Mock.listeners.values()) {
446
451
  try {
447
- const allMatch = listener.matchers.every((path) => {
452
+ const allMatch = listener.matchers.every((matcher) => {
448
453
  try {
449
- const results = JSONPath({ path, json: request, wrap: true });
450
- return Array.isArray(results) && results.length > 0;
454
+ const x = JSON.stringify(request, null, 2);
455
+ Logger.writeLine(LogLevels.TestInformation, x);
456
+ const matched = JsonUtils.getMatchingJSONPropertyCount([request], matcher) > 0;
457
+ if (matched) {
458
+ Logger.writeLine(LogLevels.FrameworkDebug, `Matched on [${matcher}]`);
459
+ return true;
460
+ }
461
+ return false;
451
462
  }
452
463
  catch (e) {
453
- Log.writeLine(LogLevels.Warning, `Mock: JSONPath evaluation error in listener <${listener.name}> for path "${path}": ${e.message} — treating as non-match`);
464
+ Log.writeLine(LogLevels.Warning, `Mock: JSONPath evaluation error in listener <${listener.name}> for path "${matcher}": ${e.message} — treating as non-match`);
454
465
  return false;
455
466
  }
456
467
  });
457
468
  if (allMatch)
458
469
  return listener;
470
+ else {
471
+ Logger.writeLine(LogLevels.FrameworkDebug, 'No matches');
472
+ }
459
473
  }
460
474
  catch (e) {
461
475
  Log.writeLine(LogLevels.Warning, `Mock: Unexpected error evaluating listener <${listener.name}>: ${e.message} — skipping`);
@@ -2,7 +2,8 @@ import { exec, spawn } from 'child_process';
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import path from "path";
4
4
  import { decodeHTML } from "entities";
5
- import { decode as jwtDecode, sign as jwtSign } from "jsonwebtoken";
5
+ import { createRequire } from 'module';
6
+ const { sign: jwtSign, decode: jwtDecode } = createRequire(import.meta.url)('jsonwebtoken');
6
7
  import psTree from "ps-tree";
7
8
  // import { Detokeniser } from "./Detokeniser"; Claude, just masking this out for now...
8
9
  import { JsonUtils } from "../index";
@@ -98,7 +99,7 @@ export class Utils {
98
99
  return true;
99
100
  }
100
101
  }
101
- /**
102
+ /**2
102
103
  * Safely checks if a value is `null`.
103
104
  *
104
105
  * @param obj - Value to check.
@@ -0,0 +1,90 @@
1
+ export declare class EAAPIUtils {
2
+ /**
3
+ * Verify if HTTP server listening
4
+ * @param url
5
+ * Protocol and domain of HTTP Server (IE. http://localhost:4200)
6
+ * @param timeoutMS
7
+ * Maximum time (in Milliseconds to wait for response)
8
+ * @returns boolean
9
+ * true if Server alive and responding
10
+ * false if no response with timeout
11
+ * @abstract
12
+ * A fetch 'HEAD' request is used to obtain a header from the server. If no
13
+ * response then it is assumed nothing listening
14
+ */
15
+ static isWebServerListening(url: string, timeoutMS: number): Promise<boolean>;
16
+ /**
17
+ * Waits for coherent response from given HTTP url
18
+ * @param url
19
+ * Protocol and domain of HTTP Server (IE. http://localhost:4200)
20
+ * @param maxSecondsToWait
21
+ * Maximum time to wait (in seconds) for a coherent response from given url
22
+ * @param maxResponseTimeMS (optional, default 1000)
23
+ * Maximum time for a response (in milliseconds) to a http HEAD request
24
+ * @returns
25
+ * Promise of boolean
26
+ * true - Webserver on given url is alive and responding
27
+ * false - No response from given url within timeout.
28
+ * @abstract
29
+ * URL is polled
30
+ */
31
+ static waitForWebServerListening(url: string, maxSecondsToWait: number, { maxResponseTimeMS }?: {
32
+ maxResponseTimeMS?: number;
33
+ }): Promise<boolean>;
34
+ /**
35
+ * Perform a single HTTP/HTTPS operation based on the details of the Request envelope
36
+ * @param httpRequest - Details of Request to be performed
37
+ * @returns Full response
38
+ * @throws Error if there is any fail that results in a Response not being received.
39
+ * The caller-supplied timeout (or the default 10s) is enforced as a hard failsafe via AbortSignal.
40
+ */
41
+ static performHTTPOperation(httpRequest: EAAPIUtils.HTTPRequest): Promise<EAAPIUtils.HTTPResponse>;
42
+ private static buildURL;
43
+ private static buildDispatcher;
44
+ private static buildHeaders;
45
+ private static doRequestLogging;
46
+ private static doResponseLogging;
47
+ }
48
+ export declare namespace EAAPIUtils {
49
+ /**
50
+ * Generic HTTP call Header items
51
+ */
52
+ type HTTPHeaders = {
53
+ [key: string]: string | string[] | number | boolean | null;
54
+ };
55
+ /**
56
+ * Generic HTTP call Request envelope
57
+ */
58
+ type HTTPRequest = {
59
+ proxy?: string;
60
+ method?: string;
61
+ protocol: 'http' | 'https';
62
+ host: string;
63
+ resourcePath: string;
64
+ queryString?: string;
65
+ headers: HTTPHeaders;
66
+ body?: string | object;
67
+ timeout?: number;
68
+ };
69
+ /**
70
+ * Generic Http call methods
71
+ */
72
+ enum HttpMethods {
73
+ POST = "POST",
74
+ GET = "GET",
75
+ PUT = "PUT"
76
+ }
77
+ const APPLICATION_JSON = "application/json";
78
+ /**
79
+ * Generic HTTP call Response envelope
80
+ */
81
+ type HTTPResponse = {
82
+ status: number;
83
+ statusMessage: string;
84
+ body: string;
85
+ };
86
+ type HTTPInteraction = {
87
+ request: HTTPRequest;
88
+ response: HTTPResponse;
89
+ };
90
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `Mock` is designed to sit between a test framework's network interceptor
5
5
  * (e.g. Playwright's `page.route`) and the application under test. Every
6
- * outgoing request from the AUT is forwarded to {@link Mock.intercept}, which
6
+ * outgoing request from the AUT must be forwarded to {@link Mock.processInterceptedRequest}, which
7
7
  * decides how to handle it based on the registered listeners.
8
8
  *
9
9
  * **Default-deny**: any request that does not match a listener is blocked and
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * `Mock` runs in the same process as the test framework. When a `passthrough`
24
24
  * listener matches, `Mock` fetches the real response itself and returns it
25
- * directly. {@link Mock.intercept} always resolves to a {@link Mock.InterceptResult}
25
+ * directly. {@link Mock.processInterceptedRequest} always resolves to a {@link Mock.InterceptResult}
26
26
  * with `action: 'fulfill'` or `action: 'block'`.
27
27
  *
28
28
  * ### Delegate mode
@@ -30,7 +30,7 @@
30
30
  * Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote
31
31
  * server (e.g. a mock proxy) and the caller — not `Mock` — is better placed
32
32
  * to perform the real network fetch. In this mode, when a `passthrough`
33
- * listener matches, {@link Mock.intercept} returns `action: 'passthrough'`
33
+ * listener matches, {@link Mock.processInterceptedRequest} returns `action: 'passthrough'`
34
34
  * plus a `correlationId`. The caller fetches the real response and reports it
35
35
  * back via {@link Mock.completePassthrough}, allowing `Mock` to record the
36
36
  * full transaction. Use {@link Mock.getPendingTransactions} to detect
@@ -81,7 +81,7 @@ export declare class Mock {
81
81
  private static _delegateMode;
82
82
  /**
83
83
  * When `true`, passthrough requests are delegated to the caller rather than
84
- * fetched by `Mock` itself. {@link Mock.intercept} returns
84
+ * fetched by `Mock` itself. {@link Mock.processInterceptedRequest} returns
85
85
  * `action: 'passthrough'` with a `correlationId` for the caller to use when
86
86
  * reporting the real response back via {@link Mock.completePassthrough}.
87
87
  *
@@ -176,7 +176,7 @@ export declare class Mock {
176
176
  * | Matched → block | `'block'` | `'blocked'` |
177
177
  * | No listener matched | `'block'` | `'unmatched'` |
178
178
  *
179
- * @param request - The intercepted request. Must be a non-null object with
179
+ * @param interceptedRequest - The intercepted request. Must be a non-null object with
180
180
  * non-empty `url` and `method` string properties.
181
181
  *
182
182
  * @returns A promise resolving to a {@link Mock.InterceptResult}.
@@ -196,16 +196,16 @@ export declare class Mock {
196
196
  * abortRequest();
197
197
  * }
198
198
  */
199
- static intercept(request: Mock.Request): Promise<Mock.InterceptResult>;
199
+ static processInterceptedRequest(interceptedRequest: Mock.Request): Promise<Mock.InterceptResult>;
200
200
  /**
201
201
  * Completes a delegated passthrough by supplying the real response the caller
202
202
  * fetched. Records the full transaction and removes the pending entry.
203
203
  *
204
204
  * Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId`
205
- * must match one previously returned by {@link Mock.intercept}.
205
+ * must match one previously returned by {@link Mock.processInterceptedRequest}.
206
206
  *
207
207
  * @param correlationId - The UUID returned in the `action: 'passthrough'`
208
- * result from {@link Mock.intercept}. Must be a non-empty string that
208
+ * result from {@link Mock.processInterceptedRequest}. Must be a non-empty string that
209
209
  * matches a pending transaction.
210
210
  *
211
211
  * @param response - The real response the caller received from the endpoint.
@@ -303,6 +303,8 @@ export declare namespace Mock {
303
303
  interface Response {
304
304
  /** HTTP status code, e.g. `200`, `404`. Must be 100–599. */
305
305
  status: number;
306
+ /** HTTP status text, eg. 'ok' or 'Internal Server Error' etc */
307
+ statusText?: string;
306
308
  /** Response headers as a plain key/value object. */
307
309
  headers?: Record<string, string>;
308
310
  /** Response body. Objects are serialised to JSON by the framework adapter. */
@@ -318,7 +320,7 @@ export declare namespace Mock {
318
320
  */
319
321
  type ListenerAction = 'block' | 'passthrough' | Response;
320
322
  /**
321
- * The result returned by {@link Mock.intercept} to the framework adapter.
323
+ * The result returned by {@link Mock.processInterceptedRequest} to the framework adapter.
322
324
  *
323
325
  * - `action: 'fulfill'` — return `response` to the AUT.
324
326
  * - `action: 'block'` — abort the request; the AUT receives nothing.
@@ -366,13 +368,13 @@ export declare namespace Mock {
366
368
  response?: Response;
367
369
  }
368
370
  /**
369
- * A delegated passthrough that has been issued by {@link Mock.intercept} but
371
+ * A delegated passthrough that has been issued by {@link Mock.processInterceptedRequest} but
370
372
  * not yet completed via {@link Mock.completePassthrough}.
371
373
  *
372
374
  * Only created when {@link Mock.delegateMode} is `true`.
373
375
  */
374
376
  interface PendingTransaction {
375
- /** The UUID issued by {@link Mock.intercept} to identify this passthrough. */
377
+ /** The UUID issued by {@link Mock.processInterceptedRequest} to identify this passthrough. */
376
378
  correlationId: string;
377
379
  /** Wall-clock time at which the passthrough was issued. */
378
380
  timestamp: Date;
@@ -70,21 +70,21 @@ export declare class Utils {
70
70
  * @param obj - Value to check.
71
71
  * @returns `true` if `null` or `undefined`, otherwise `false`.
72
72
  */
73
- static isNullOrUndefined(obj?: unknown): boolean;
74
- /**
73
+ static isNullOrUndefined(obj?: unknown): obj is null | undefined;
74
+ /**2
75
75
  * Safely checks if a value is `null`.
76
76
  *
77
77
  * @param obj - Value to check.
78
78
  * @returns `true` if `null`, otherwise `false`.
79
79
  */
80
- static isNull(obj?: unknown): boolean;
80
+ static isNull(obj?: unknown): obj is null;
81
81
  /**
82
82
  * Safely checks if a value is `undefined`.
83
83
  *
84
84
  * @param obj - Value to check.
85
85
  * @returns `true` if `undefined`, otherwise `false`.
86
86
  */
87
- static isUndefined(obj?: unknown): boolean;
87
+ static isUndefined(obj?: unknown): obj is undefined;
88
88
  /**
89
89
  * Checks whether a value evaluates to `true` according to common conventions.
90
90
  *