@gcoredev/fastedge-test 0.1.7 → 0.2.1
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/fastedge-cli/METADATA.json +1 -1
- package/dist/fastedge-cli/fastedge-run-darwin-arm64 +0 -0
- package/dist/fastedge-cli/fastedge-run-linux-x64 +0 -0
- package/dist/fastedge-cli/fastedge-run.exe +0 -0
- package/dist/frontend/assets/{index-BCXfEMSq.js → index-CiqeJ9rz.js} +24 -24
- package/dist/frontend/index.html +1 -1
- package/dist/lib/index.cjs +292 -140
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +292 -140
- package/dist/lib/runner/HeaderManager.d.ts +7 -4
- package/dist/lib/runner/HostFunctions.d.ts +5 -5
- package/dist/lib/runner/HttpWasmRunner.d.ts +13 -4
- package/dist/lib/runner/IStateManager.d.ts +7 -7
- package/dist/lib/runner/IWasmRunner.d.ts +17 -9
- package/dist/lib/runner/PropertyResolver.d.ts +3 -3
- package/dist/lib/runner/ProxyWasmRunner.d.ts +6 -3
- package/dist/lib/runner/standalone.d.ts +1 -1
- package/dist/lib/runner/types.d.ts +17 -8
- package/dist/lib/schemas/api.d.ts +0 -8
- package/dist/lib/schemas/config.d.ts +0 -13
- package/dist/lib/schemas/index.d.ts +2 -2
- package/dist/lib/test-framework/assertions.d.ts +18 -4
- package/dist/lib/test-framework/index.cjs +18754 -189
- package/dist/lib/test-framework/index.d.ts +2 -0
- package/dist/lib/test-framework/index.js +18771 -178
- package/dist/lib/test-framework/mock-origins.d.ts +56 -0
- package/dist/lib/test-framework/types.d.ts +1 -5
- package/dist/server.js +33 -33
- package/docs/API.md +23 -53
- package/docs/DEBUGGER.md +7 -7
- package/docs/INDEX.md +4 -1
- package/docs/RUNNER.md +79 -64
- package/docs/TEST_CONFIG.md +28 -41
- package/docs/TEST_FRAMEWORK.md +205 -32
- package/docs/WEBSOCKET.md +25 -21
- package/docs/quickstart.md +1 -13
- package/package.json +4 -1
- package/schemas/api-config.schema.json +0 -24
- package/schemas/api-send.schema.json +0 -20
- package/schemas/fastedge-config.test.schema.json +0 -24
- package/schemas/full-flow-result.schema.json +17 -7
- package/schemas/hook-call.schema.json +16 -6
- package/schemas/hook-result.schema.json +16 -6
- package/schemas/http-response.schema.json +227 -5
package/docs/TEST_CONFIG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Test Configuration
|
|
2
2
|
|
|
3
|
-
Configuration file reference for `fastedge-config.test.json` — the per-test JSON file that defines the WASM binary, request,
|
|
3
|
+
Configuration file reference for `fastedge-config.test.json` — the per-test JSON file that defines the WASM binary, request, CDN properties, and environment variable loading for a single test scenario.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -8,8 +8,8 @@ Each test scenario is described by a `fastedge-config.test.json` file. The file
|
|
|
8
8
|
|
|
9
9
|
The config schema is a union of two variants selected by `appType`:
|
|
10
10
|
|
|
11
|
-
- **`proxy-wasm`** (CDN mode, default): The WASM module intercepts an upstream HTTP request. Uses `request.url` (full URL).
|
|
12
|
-
- **`http-wasm`**: The WASM module acts as an origin HTTP server. Uses `request.path` (path only).
|
|
11
|
+
- **`proxy-wasm`** (CDN mode, default): The WASM module intercepts an upstream HTTP request. Uses `request.url` (full URL). The upstream response is generated at runtime — either by a real fetch against `request.url`, or by the built-in responder when `request.url === "built-in"`.
|
|
12
|
+
- **`http-wasm`**: The WASM module acts as an origin HTTP server. Uses `request.path` (path only).
|
|
13
13
|
|
|
14
14
|
**Required fields** (per JSON Schema `required` arrays):
|
|
15
15
|
- Top-level: `properties`, `appType`, and `request`
|
|
@@ -22,28 +22,25 @@ The config schema is a union of two variants selected by `appType`:
|
|
|
22
22
|
|
|
23
23
|
### Top-Level Fields
|
|
24
24
|
|
|
25
|
-
| JSON Path
|
|
26
|
-
|
|
|
27
|
-
| `$schema`
|
|
28
|
-
| `description`
|
|
29
|
-
| `wasm`
|
|
30
|
-
| `wasm.path`
|
|
31
|
-
| `wasm.description`
|
|
32
|
-
| `appType`
|
|
33
|
-
| `request`
|
|
34
|
-
| `request.method`
|
|
35
|
-
| `request.url`
|
|
36
|
-
| `request.path`
|
|
37
|
-
| `request.headers`
|
|
38
|
-
| `request.body`
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `dotenv.enabled` | `boolean` | No | — | Whether to load a `.env` file before execution. |
|
|
45
|
-
| `dotenv.path` | `string` | No | — | Path to the `.env` file. If omitted, resolves `.env` relative to the config file directory. |
|
|
46
|
-
| `httpPort` | `integer` | No | — | HTTP-WASM only. Pin the subprocess to a specific port (1024–65535) instead of dynamic allocation from the 8100–8199 pool. Throws if the port is busy. |
|
|
25
|
+
| JSON Path | Type | Required (Schema) | Default | Description |
|
|
26
|
+
| ------------------ | --------- | --------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
27
|
+
| `$schema` | `string` | No | — | URI pointing to the JSON Schema file for IDE autocompletion and validation. |
|
|
28
|
+
| `description` | `string` | No | — | Human-readable label for this test scenario. |
|
|
29
|
+
| `wasm` | `object` | No | — | WASM binary configuration. Required when running without a programmatic `wasmBuffer`. |
|
|
30
|
+
| `wasm.path` | `string` | Yes (if `wasm` present) | — | Path to the compiled `.wasm` binary, relative to the config file or absolute. |
|
|
31
|
+
| `wasm.description` | `string` | No | — | Human-readable label for the WASM binary. |
|
|
32
|
+
| `appType` | `string` | Yes (schema) / CDN has runtime default | `"proxy-wasm"` | App variant. `"proxy-wasm"` for CDN mode; `"http-wasm"` for HTTP mode. HTTP-WASM has no default. |
|
|
33
|
+
| `request` | `object` | **Yes** | — | Incoming HTTP request to simulate. |
|
|
34
|
+
| `request.method` | `string` | Yes (schema) / runtime default | `"GET"` | HTTP method (e.g. `"GET"`, `"POST"`). |
|
|
35
|
+
| `request.url` | `string` | **Yes** (CDN only) | — | Full URL for the simulated upstream request (e.g. `"https://example.com/api"`). CDN mode only. |
|
|
36
|
+
| `request.path` | `string` | **Yes** (HTTP-WASM only) | — | Request path (e.g. `"/api/submit"`). HTTP-WASM mode only. The WASM module acts as the origin server and receives only the path portion of the request. |
|
|
37
|
+
| `request.headers` | `object` | Yes (schema) / runtime default | `{}` | Key/value map of request headers. All keys and values must be strings. |
|
|
38
|
+
| `request.body` | `string` | Yes (schema) / runtime default | `""` | Request body as a plain string. Use an empty string for requests with no body. |
|
|
39
|
+
| `properties` | `object` | **Yes** (schema) / runtime default | `{}` | CDN property key/value pairs passed to the WASM execution context. Values may be any JSON type. |
|
|
40
|
+
| `dotenv` | `object` | No | — | Dotenv file loading configuration. |
|
|
41
|
+
| `dotenv.enabled` | `boolean` | No | — | Whether to load a `.env` file before execution. |
|
|
42
|
+
| `dotenv.path` | `string` | No | — | Path to the `.env` file. If omitted, resolves `.env` relative to the config file directory. |
|
|
43
|
+
| `httpPort` | `integer` | No | — | HTTP-WASM only. Pin the subprocess to a specific port (1024–65535) instead of dynamic allocation from the 8100–8199 pool. Throws if the port is busy. |
|
|
47
44
|
|
|
48
45
|
### Required vs. Default Distinction
|
|
49
46
|
|
|
@@ -203,37 +200,32 @@ An HTTP-WASM scenario simulating a `POST` request with a JSON body. `appType` mu
|
|
|
203
200
|
}
|
|
204
201
|
```
|
|
205
202
|
|
|
206
|
-
###
|
|
203
|
+
### Built-In Responder (No Network)
|
|
207
204
|
|
|
208
|
-
A CDN scenario
|
|
205
|
+
A CDN scenario that uses the test runner's built-in responder instead of reaching out to a real origin. Set `request.url` to `"built-in"` — the runner generates a local response and skips the upstream fetch. This is the fastest way to exercise a CDN WASM app without depending on network reachability.
|
|
209
206
|
|
|
210
207
|
```json
|
|
211
208
|
{
|
|
212
209
|
"$schema": "./node_modules/@gcoredev/fastedge-test/schemas/fastedge-config.test.schema.json",
|
|
213
|
-
"description": "CDN handler
|
|
210
|
+
"description": "CDN handler against the built-in responder",
|
|
214
211
|
"appType": "proxy-wasm",
|
|
215
212
|
"wasm": {
|
|
216
213
|
"path": "./dist/handler.wasm"
|
|
217
214
|
},
|
|
218
215
|
"request": {
|
|
219
216
|
"method": "GET",
|
|
220
|
-
"url": "
|
|
217
|
+
"url": "built-in",
|
|
221
218
|
"headers": {},
|
|
222
219
|
"body": ""
|
|
223
220
|
},
|
|
224
|
-
"response": {
|
|
225
|
-
"headers": {
|
|
226
|
-
"content-type": "application/json",
|
|
227
|
-
"cache-control": "max-age=86400"
|
|
228
|
-
},
|
|
229
|
-
"body": "{\"status\": \"ok\", \"data\": []}"
|
|
230
|
-
},
|
|
231
221
|
"properties": {
|
|
232
222
|
"CACHE_TTL": 86400
|
|
233
223
|
}
|
|
234
224
|
}
|
|
235
225
|
```
|
|
236
226
|
|
|
227
|
+
The default built-in response is a full JSON echo of the request. Override via control headers on the request: `x-debugger-status` sets the HTTP status; `x-debugger-content: status-only` omits the body; `x-debugger-content: body-only` echoes only the request body (with its inferred content-type).
|
|
228
|
+
|
|
237
229
|
## IDE Integration
|
|
238
230
|
|
|
239
231
|
Adding `$schema` to your config file enables JSON Schema validation and autocompletion in VSCode and any editor that supports the JSON Language Server.
|
|
@@ -290,10 +282,6 @@ type CdnConfig = {
|
|
|
290
282
|
headers: Record<string, string>; // default: {}
|
|
291
283
|
body: string; // default: ""
|
|
292
284
|
};
|
|
293
|
-
response?: {
|
|
294
|
-
headers: Record<string, string>; // default: {}
|
|
295
|
-
body: string; // default: ""
|
|
296
|
-
};
|
|
297
285
|
properties: Record<string, unknown>; // default: {}
|
|
298
286
|
dotenv?: {
|
|
299
287
|
enabled?: boolean;
|
|
@@ -333,7 +321,6 @@ const config = await loadConfigFile("./fastedge-config.test.json");
|
|
|
333
321
|
|
|
334
322
|
if (config.appType === "proxy-wasm") {
|
|
335
323
|
console.log(config.request.url); // string — CDN full URL
|
|
336
|
-
console.log(config.response); // ResponseConfig | undefined
|
|
337
324
|
} else {
|
|
338
325
|
console.log(config.request.path); // string — HTTP-WASM path
|
|
339
326
|
console.log(config.httpPort); // number | undefined
|
package/docs/TEST_FRAMEWORK.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Test Framework API
|
|
2
2
|
|
|
3
|
-
High-level API for defining and running
|
|
3
|
+
High-level test framework API for defining and running WASM test suites with `@gcoredev/fastedge-test`.
|
|
4
4
|
|
|
5
5
|
## Import
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
runFlow,
|
|
13
13
|
runHttpRequest,
|
|
14
14
|
loadConfigFile,
|
|
15
|
+
mockOrigins,
|
|
15
16
|
assertRequestHeader,
|
|
16
17
|
assertNoRequestHeader,
|
|
17
18
|
assertResponseHeader,
|
|
@@ -44,6 +45,8 @@ import type {
|
|
|
44
45
|
FlowOptions,
|
|
45
46
|
HttpRequestOptions,
|
|
46
47
|
RunnerConfig,
|
|
48
|
+
MockOriginsHandle,
|
|
49
|
+
MockOriginsOptions,
|
|
47
50
|
} from "@gcoredev/fastedge-test/test";
|
|
48
51
|
```
|
|
49
52
|
|
|
@@ -111,10 +114,6 @@ interface FlowOptions {
|
|
|
111
114
|
method?: string; // Default: "GET"
|
|
112
115
|
requestHeaders?: Record<string, string>;
|
|
113
116
|
requestBody?: string; // Default: ""
|
|
114
|
-
responseStatus?: number; // Default: 200
|
|
115
|
-
responseStatusText?: string; // Default: "OK"
|
|
116
|
-
responseHeaders?: Record<string, string>; // Default: {}
|
|
117
|
-
responseBody?: string; // Default: ""
|
|
118
117
|
properties?: Record<string, unknown>; // Default: {}
|
|
119
118
|
enforceProductionPropertyRules?: boolean; // Default: true
|
|
120
119
|
}
|
|
@@ -122,17 +121,40 @@ interface FlowOptions {
|
|
|
122
121
|
|
|
123
122
|
| Field | Default | Description |
|
|
124
123
|
| -------------------------------- | ------- | --------------------------------------------------------------------------- |
|
|
125
|
-
| `url` | — | Full URL including scheme and host
|
|
124
|
+
| `url` | — | Full URL including scheme and host, or `"built-in"` for the local responder |
|
|
126
125
|
| `method` | `"GET"` | HTTP method |
|
|
127
126
|
| `requestHeaders` | `{}` | Additional request headers; pseudo-headers here override derived values |
|
|
128
127
|
| `requestBody` | `""` | Request body string |
|
|
129
|
-
| `responseStatus` | `200` | Simulated upstream response status code |
|
|
130
|
-
| `responseStatusText` | `"OK"` | Simulated upstream response status text |
|
|
131
|
-
| `responseHeaders` | `{}` | Simulated upstream response headers |
|
|
132
|
-
| `responseBody` | `""` | Simulated upstream response body |
|
|
133
128
|
| `properties` | `{}` | Proxy-wasm properties to inject |
|
|
134
129
|
| `enforceProductionPropertyRules` | `true` | When true, denies access to properties not available in production FastEdge |
|
|
135
130
|
|
|
131
|
+
The upstream response is generated at runtime by a real fetch against `url`, or by the built-in responder when `url === "built-in"`. See the [Origin Mocking](#origin-mocking) section for controlling responses in tests.
|
|
132
|
+
|
|
133
|
+
### MockOriginsHandle & MockOriginsOptions
|
|
134
|
+
|
|
135
|
+
Returned by `mockOrigins()` to scope an undici `MockAgent` to a single test (see [Origin Mocking](#origin-mocking) for full usage).
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface MockOriginsOptions {
|
|
139
|
+
allowNetConnect?: boolean | (string | RegExp)[]; // Default: false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface MockOriginsHandle {
|
|
143
|
+
origin(url: string): MockPool;
|
|
144
|
+
readonly agent: MockAgent;
|
|
145
|
+
close(): Promise<void>;
|
|
146
|
+
assertAllCalled(): void;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Field | Description |
|
|
151
|
+
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
152
|
+
| `options.allowNetConnect` | Opt requests out of the default `disableNetConnect` block. `true` allows all; an array allow-lists origins or patterns |
|
|
153
|
+
| `handle.origin(url)` | Get or create a `MockPool` for an origin; chain `.intercept({path, method}).reply(...)` on it |
|
|
154
|
+
| `handle.agent` | Raw `MockAgent` escape hatch for `.persist()` / `.times()` / `.delay()` / body matchers |
|
|
155
|
+
| `handle.close()` | Restore the previous global dispatcher and close the agent; idempotent |
|
|
156
|
+
| `handle.assertAllCalled()` | Throw if any registered interceptor was never matched by a real request |
|
|
157
|
+
|
|
136
158
|
### HttpRequestOptions
|
|
137
159
|
|
|
138
160
|
Object-based options for `runHttpRequest()`. Used with HTTP WASM apps (as opposed to CDN proxy-wasm filter apps tested with `runFlow`).
|
|
@@ -161,6 +183,26 @@ Re-exported from the runner. Controls WASM execution behaviour. See [RUNNER.md](
|
|
|
161
183
|
import type { RunnerConfig } from "@gcoredev/fastedge-test/test";
|
|
162
184
|
```
|
|
163
185
|
|
|
186
|
+
```typescript
|
|
187
|
+
interface RunnerConfig {
|
|
188
|
+
dotenv?: {
|
|
189
|
+
enabled?: boolean;
|
|
190
|
+
path?: string;
|
|
191
|
+
};
|
|
192
|
+
enforceProductionPropertyRules?: boolean;
|
|
193
|
+
runnerType?: "http-wasm" | "proxy-wasm";
|
|
194
|
+
httpPort?: number;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Field | Type | Description |
|
|
199
|
+
| -------------------------------- | ------------------------------ | ------------------------------------------------------------------------- |
|
|
200
|
+
| `dotenv.enabled` | `boolean` | Enable dotenv loading |
|
|
201
|
+
| `dotenv.path` | `string` | Directory to load dotenv files from; defaults to process CWD when omitted |
|
|
202
|
+
| `enforceProductionPropertyRules` | `boolean` | Override production property enforcement for the runner; default `true` |
|
|
203
|
+
| `runnerType` | `"http-wasm" \| "proxy-wasm"` | Override automatic WASM type detection |
|
|
204
|
+
| `httpPort` | `number` | Pin the HTTP server to a specific port (HTTP WASM only; throws if in use) |
|
|
205
|
+
|
|
164
206
|
## Functions
|
|
165
207
|
|
|
166
208
|
### defineTestSuite
|
|
@@ -244,11 +286,11 @@ The returned `FullFlowResult` has this shape:
|
|
|
244
286
|
|
|
245
287
|
```typescript
|
|
246
288
|
type FullFlowResult = {
|
|
247
|
-
hookResults: Record<string, HookResult>;
|
|
289
|
+
hookResults: Record<string, HookResult>;
|
|
248
290
|
finalResponse: {
|
|
249
291
|
status: number;
|
|
250
292
|
statusText: string;
|
|
251
|
-
headers: Record<string, string>;
|
|
293
|
+
headers: Record<string, string | string[]>;
|
|
252
294
|
body: string;
|
|
253
295
|
contentType: string;
|
|
254
296
|
isBase64?: boolean;
|
|
@@ -259,12 +301,12 @@ type FullFlowResult = {
|
|
|
259
301
|
|
|
260
302
|
Hook results are accessed by camelCase key:
|
|
261
303
|
|
|
262
|
-
| Key
|
|
263
|
-
|
|
|
264
|
-
| `onRequestHeaders`
|
|
265
|
-
| `onRequestBody`
|
|
266
|
-
| `onResponseHeaders`
|
|
267
|
-
| `onResponseBody`
|
|
304
|
+
| Key | Hook |
|
|
305
|
+
| ------------------- | -------------------------- |
|
|
306
|
+
| `onRequestHeaders` | `on_request_headers` hook |
|
|
307
|
+
| `onRequestBody` | `on_request_body` hook |
|
|
308
|
+
| `onResponseHeaders` | `on_response_headers` hook |
|
|
309
|
+
| `onResponseBody` | `on_response_body` hook |
|
|
268
310
|
|
|
269
311
|
```typescript
|
|
270
312
|
const result = await runFlow(runner, {
|
|
@@ -272,8 +314,6 @@ const result = await runFlow(runner, {
|
|
|
272
314
|
method: "POST",
|
|
273
315
|
requestHeaders: { "content-type": "application/json" },
|
|
274
316
|
requestBody: '{"key":"value"}',
|
|
275
|
-
responseStatus: 201,
|
|
276
|
-
responseHeaders: { "x-upstream": "backend-1" },
|
|
277
317
|
});
|
|
278
318
|
|
|
279
319
|
// Access hook results
|
|
@@ -281,8 +321,8 @@ const reqHook = result.hookResults.onRequestHeaders;
|
|
|
281
321
|
const resHook = result.hookResults.onResponseHeaders;
|
|
282
322
|
|
|
283
323
|
// Access final response
|
|
284
|
-
console.log(result.finalResponse.status);
|
|
285
|
-
console.log(result.finalResponse.contentType);
|
|
324
|
+
console.log(result.finalResponse.status);
|
|
325
|
+
console.log(result.finalResponse.contentType);
|
|
286
326
|
```
|
|
287
327
|
|
|
288
328
|
### runHttpRequest
|
|
@@ -307,7 +347,7 @@ const response = await runHttpRequest(runner, { path: "/login" });
|
|
|
307
347
|
assertHttpStatus(response, 302);
|
|
308
348
|
assertHttpHeader(response, "location", "/dashboard");
|
|
309
349
|
|
|
310
|
-
const redirected = await runHttpRequest(runner, { path: response.headers["location"] });
|
|
350
|
+
const redirected = await runHttpRequest(runner, { path: response.headers["location"] as string });
|
|
311
351
|
assertHttpStatus(redirected, 200);
|
|
312
352
|
```
|
|
313
353
|
|
|
@@ -336,6 +376,23 @@ Reads and validates a `fastedge-config.test.json` file. Returns the parsed `Test
|
|
|
336
376
|
const config = await loadConfigFile("./fastedge-config.test.json");
|
|
337
377
|
```
|
|
338
378
|
|
|
379
|
+
### mockOrigins
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
function mockOrigins(options?: MockOriginsOptions): MockOriginsHandle
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Install an undici `MockAgent` as the global fetch dispatcher for the duration of a test. Every origin fetch and every `proxy_http_call` upstream the runner makes routes through it, so interceptors registered on the returned handle match all of them. Blocks unmocked requests by default.
|
|
386
|
+
|
|
387
|
+
See [Origin Mocking](#origin-mocking) for the full usage pattern including lifecycle, multi-upstream setup, and the HTTP-WASM `allowNetConnect` caveat.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
const mocks = mockOrigins();
|
|
391
|
+
mocks.origin("https://api.example.com").intercept({ path: "/users" }).reply(200, "[]");
|
|
392
|
+
// ... run your test ...
|
|
393
|
+
await mocks.close();
|
|
394
|
+
```
|
|
395
|
+
|
|
339
396
|
## Assertion Helpers
|
|
340
397
|
|
|
341
398
|
All assertion helpers throw an `Error` on failure, making them compatible with any test framework (vitest, jest, node:assert) or plain try/catch.
|
|
@@ -343,11 +400,11 @@ All assertion helpers throw an `Error` on failure, making them compatible with a
|
|
|
343
400
|
### Request Headers
|
|
344
401
|
|
|
345
402
|
```typescript
|
|
346
|
-
function assertRequestHeader(result: HookResult, name: string, expected?: string): void
|
|
403
|
+
function assertRequestHeader(result: HookResult, name: string, expected?: string | string[]): void
|
|
347
404
|
function assertNoRequestHeader(result: HookResult, name: string): void
|
|
348
405
|
```
|
|
349
406
|
|
|
350
|
-
`assertRequestHeader` asserts the named header exists in the hook's output request headers.
|
|
407
|
+
`assertRequestHeader` asserts the named header exists (case-insensitive) in the hook's output request headers. When `expected` is a `string` and the header is multi-valued, passes if any value matches (`.includes()` semantics). When `expected` is a `string[]`, requires an exact array match.
|
|
351
408
|
|
|
352
409
|
`assertNoRequestHeader` asserts the named header is absent.
|
|
353
410
|
|
|
@@ -362,7 +419,7 @@ assertNoRequestHeader(hookResult, "x-internal-secret"); // absent
|
|
|
362
419
|
### Response Headers
|
|
363
420
|
|
|
364
421
|
```typescript
|
|
365
|
-
function assertResponseHeader(result: HookResult, name: string, expected?: string): void
|
|
422
|
+
function assertResponseHeader(result: HookResult, name: string, expected?: string | string[]): void
|
|
366
423
|
function assertNoResponseHeader(result: HookResult, name: string): void
|
|
367
424
|
```
|
|
368
425
|
|
|
@@ -380,12 +437,12 @@ assertNoResponseHeader(hookResult, "server");
|
|
|
380
437
|
|
|
381
438
|
```typescript
|
|
382
439
|
function assertFinalStatus(result: FullFlowResult, expected: number): void
|
|
383
|
-
function assertFinalHeader(result: FullFlowResult, name: string, expected?: string): void
|
|
440
|
+
function assertFinalHeader(result: FullFlowResult, name: string, expected?: string | string[]): void
|
|
384
441
|
```
|
|
385
442
|
|
|
386
443
|
`assertFinalStatus` asserts the final response status code after the full flow completes.
|
|
387
444
|
|
|
388
|
-
`assertFinalHeader` asserts a header in `result.finalResponse.headers`. If `expected` is provided, also asserts the value.
|
|
445
|
+
`assertFinalHeader` asserts a header in `result.finalResponse.headers`. If `expected` is provided, also asserts the value. Multi-value semantics match `assertRequestHeader`: a `string` expected matches any value when the actual header is multi-valued; a `string[]` expected requires an exact array match. This preserves the RFC 6265 contract for `Set-Cookie` and any other legitimately-repeatable headers.
|
|
389
446
|
|
|
390
447
|
```typescript
|
|
391
448
|
assertFinalStatus(result, 200);
|
|
@@ -464,7 +521,7 @@ These assertions operate on an `HttpResponse` returned by `runHttpRequest`. Use
|
|
|
464
521
|
|
|
465
522
|
```typescript
|
|
466
523
|
function assertHttpStatus(response: HttpResponse, expected: number): void
|
|
467
|
-
function assertHttpHeader(response: HttpResponse, name: string, expected?: string): void
|
|
524
|
+
function assertHttpHeader(response: HttpResponse, name: string, expected?: string | string[]): void
|
|
468
525
|
function assertHttpNoHeader(response: HttpResponse, name: string): void
|
|
469
526
|
function assertHttpBody(response: HttpResponse, expected: string): void
|
|
470
527
|
function assertHttpBodyContains(response: HttpResponse, substring: string): void
|
|
@@ -476,7 +533,18 @@ function assertHttpNoLog(response: HttpResponse, messageSubstring: string): void
|
|
|
476
533
|
|
|
477
534
|
`assertHttpStatus` — asserts the response status code.
|
|
478
535
|
|
|
479
|
-
`assertHttpHeader` — asserts the named header exists (case-insensitive). If `expected` is
|
|
536
|
+
`assertHttpHeader` — asserts the named header exists (case-insensitive). If `expected` is a `string` and the header is multi-valued (e.g. `set-cookie`), passes if any value matches (`.includes()` semantics). If `expected` is a `string[]`, requires an exact array match.
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// Single-valued header — exact match
|
|
540
|
+
assertHttpHeader(response, "content-type", "application/json");
|
|
541
|
+
|
|
542
|
+
// Multi-valued header — one-of-many match
|
|
543
|
+
assertHttpHeader(response, "set-cookie", "sid=abc; Path=/");
|
|
544
|
+
|
|
545
|
+
// Multi-valued header — exact array
|
|
546
|
+
assertHttpHeader(response, "set-cookie", ["sid=abc; Path=/", "theme=dark; Path=/"]);
|
|
547
|
+
```
|
|
480
548
|
|
|
481
549
|
`assertHttpNoHeader` — asserts the named header is absent (case-insensitive).
|
|
482
550
|
|
|
@@ -507,6 +575,113 @@ const data = assertHttpJson<{ items: unknown[] }>(response);
|
|
|
507
575
|
console.log(data.items.length);
|
|
508
576
|
```
|
|
509
577
|
|
|
578
|
+
## Origin Mocking
|
|
579
|
+
|
|
580
|
+
The runner's origin fetch inside `callFullFlow` and every `proxy_http_call` upstream fetch both go through Node's global `fetch`, which routes through undici's global dispatcher. `mockOrigins()` installs a [`MockAgent`](https://undici.nodejs.org/#/docs/api/MockAgent) as that dispatcher, so interceptors registered on the returned handle match every request the runner makes — origin fetches in full-flow mode, upstream calls from WASM via `proxy_http_call`, anything else the runner eventually emits.
|
|
581
|
+
|
|
582
|
+
### Basic Usage
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import { mockOrigins } from "@gcoredev/fastedge-test/test";
|
|
586
|
+
|
|
587
|
+
let mocks: MockOriginsHandle | null = null;
|
|
588
|
+
|
|
589
|
+
beforeEach(() => {
|
|
590
|
+
mocks = mockOrigins();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
afterEach(async () => {
|
|
594
|
+
await mocks?.close();
|
|
595
|
+
mocks = null;
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("renders a retry UI when the origin returns 503", async () => {
|
|
599
|
+
mocks!
|
|
600
|
+
.origin("https://origin.example.com")
|
|
601
|
+
.intercept({ path: "/api/resource" })
|
|
602
|
+
.reply(503, "upstream down");
|
|
603
|
+
|
|
604
|
+
const result = await runFlow(runner, {
|
|
605
|
+
url: "https://origin.example.com/api/resource",
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
assertFinalStatus(result, 503);
|
|
609
|
+
mocks!.assertAllCalled();
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
`handle.origin(url)` returns an undici [`MockPool`](https://undici.nodejs.org/#/docs/api/MockPool) for that origin. Despite reading like "HTTP GET", `MockAgent.get` is a `Map.get`-style lookup — the HTTP method lives on the subsequent `.intercept({ method })` call and defaults to `GET`. The method field accepts any HTTP verb as a string, a `RegExp`, or a predicate function.
|
|
614
|
+
|
|
615
|
+
### Multi-Upstream with `proxy_http_call`
|
|
616
|
+
|
|
617
|
+
Every upstream the WASM initiates via `proxy_http_call` goes through the same global dispatcher, so multiple origins can be stacked in one setup:
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
mocks!
|
|
621
|
+
.origin("https://auth.example.com")
|
|
622
|
+
.intercept({ path: "/token", method: "POST" })
|
|
623
|
+
.reply(200, '{"jwt":"xyz"}');
|
|
624
|
+
|
|
625
|
+
mocks!
|
|
626
|
+
.origin("https://analytics.example.com")
|
|
627
|
+
.intercept({ path: "/event", method: "POST" })
|
|
628
|
+
.reply(204);
|
|
629
|
+
|
|
630
|
+
mocks!
|
|
631
|
+
.origin("https://origin.example.com")
|
|
632
|
+
.intercept({ path: "/" })
|
|
633
|
+
.reply(200, "hello");
|
|
634
|
+
|
|
635
|
+
const result = await runFlow(runner, {
|
|
636
|
+
url: "https://origin.example.com/",
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Fails if any registered interceptor was never hit
|
|
640
|
+
mocks!.assertAllCalled();
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Lifecycle and `assertAllCalled`
|
|
644
|
+
|
|
645
|
+
The handle installs the MockAgent as the global dispatcher on construction and restores the previous dispatcher on `close()`. One handle per test is the expected pattern; `beforeEach` / `afterEach` keeps each test isolated. Calling `close()` more than once is safe — later calls are no-ops.
|
|
646
|
+
|
|
647
|
+
`handle.assertAllCalled()` throws if any registered interceptor was never matched. Use it at the end of a test (or in `afterEach`) to catch setup drift — mocks that were registered but never exercised because the WASM under test took a different code path.
|
|
648
|
+
|
|
649
|
+
### HTTP-WASM `allowNetConnect` caveat
|
|
650
|
+
|
|
651
|
+
By default, `mockOrigins()` calls `MockAgent.disableNetConnect()` — every request that doesn't match a registered interceptor is rejected. This is the safer default: missing mocks become loud errors instead of silent live network calls in CI.
|
|
652
|
+
|
|
653
|
+
**HTTP-WASM tests do not compose with the default.** `HttpWasmRunner.execute()` forwards requests to a spawned `fastedge-run` subprocess on `localhost:<port>`, and that localhost fetch is also blocked by `disableNetConnect()`. Use `allowNetConnect` to exempt localhost:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
mocks = mockOrigins({
|
|
657
|
+
allowNetConnect: [/^127\.0\.0\.1/, /^localhost/],
|
|
658
|
+
});
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
This preserves the block-by-default safety for all real origins while allowing the runner's own process-to-process fetch through. For pure-CDN test suites using `runFlow` only, the default is correct and this option is not needed.
|
|
662
|
+
|
|
663
|
+
### Advanced: the raw `MockAgent`
|
|
664
|
+
|
|
665
|
+
`handle.agent` exposes the underlying `MockAgent` unchanged. Use it for features the wrapper doesn't re-export — `.persist()` (match repeatedly), `.times(n)` (match exactly N times), `.delay(ms)` (simulate latency), custom body matchers, request body predicates, etc. See the [undici MockAgent docs](https://undici.nodejs.org/#/docs/api/MockAgent) for the full DSL.
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
mocks!.agent
|
|
669
|
+
.get("https://flaky.example.com")
|
|
670
|
+
.intercept({ path: "/api" })
|
|
671
|
+
.reply(503, "down")
|
|
672
|
+
.times(2);
|
|
673
|
+
|
|
674
|
+
mocks!.agent
|
|
675
|
+
.get("https://flaky.example.com")
|
|
676
|
+
.intercept({ path: "/api" })
|
|
677
|
+
.reply(200, "ok")
|
|
678
|
+
.persist();
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Pseudo-headers and the outbound fetch
|
|
682
|
+
|
|
683
|
+
The runner strips HTTP/2 pseudo-headers (`:method`, `:path`, `:authority`, `:scheme`) from the outbound fetch before it leaves the process. WASM hooks still see them via `proxy_get_header_map_value` during hook execution; the HTTP/1.1 fetch that actually reaches the origin does not carry them. This mirrors production FastEdge behaviour and means `runFlow` — which derives and injects the pseudo-headers from `url` and `method` — composes with `mockOrigins()` out of the box: interceptors match on path and method only, exactly as undici expects.
|
|
684
|
+
|
|
510
685
|
## CI Integration
|
|
511
686
|
|
|
512
687
|
`runAndExit` is the primary entry point for CI pipelines. It exits with code `0` on full pass and `1` on any failure, compatible with standard CI exit-code conventions.
|
|
@@ -590,8 +765,6 @@ const suite = defineTestSuite({
|
|
|
590
765
|
async run(runner) {
|
|
591
766
|
const result = await runFlow(runner, {
|
|
592
767
|
url: "https://cdn.example.com/static/app.js",
|
|
593
|
-
responseStatus: 200,
|
|
594
|
-
responseHeaders: { "content-type": "application/javascript" },
|
|
595
768
|
});
|
|
596
769
|
|
|
597
770
|
const res = result.hookResults.onResponseHeaders;
|
package/docs/WEBSOCKET.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Real-time event stream from the `@gcoredev/fastedge-test` server to connected clients.
|
|
4
4
|
|
|
5
|
+
> **Note on header values.** All header fields in this protocol use `Record<string, string | string[]>` — single-valued headers are a `string`, multi-valued headers (notably `Set-Cookie` per RFC 6265) are a `string[]`. HTTP-wasm response headers additionally allow `undefined` values (`Record<string, string | string[] | undefined>`), though `undefined` entries are dropped during JSON serialization. JSON examples below use `Record<string, string>` for brevity.
|
|
6
|
+
|
|
5
7
|
## Connection
|
|
6
8
|
|
|
7
9
|
Connect to the WebSocket server at:
|
|
@@ -107,16 +109,16 @@ interface RequestStartedEvent {
|
|
|
107
109
|
data: {
|
|
108
110
|
url: string;
|
|
109
111
|
method: string;
|
|
110
|
-
headers: Record<string, string>;
|
|
112
|
+
headers: Record<string, string | string[]>;
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
```
|
|
114
116
|
|
|
115
|
-
| Field | Type
|
|
116
|
-
| --------- |
|
|
117
|
-
| `url` | `string`
|
|
118
|
-
| `method` | `string`
|
|
119
|
-
| `headers` | `Record<string, string>` | Request headers |
|
|
117
|
+
| Field | Type | Description |
|
|
118
|
+
| --------- | ------------------------------------ | --------------------------------- |
|
|
119
|
+
| `url` | `string` | Full request URL |
|
|
120
|
+
| `method` | `string` | HTTP method (`GET`, `POST`, etc.) |
|
|
121
|
+
| `headers` | `Record<string, string \| string[]>` | Request headers |
|
|
120
122
|
|
|
121
123
|
**Example:**
|
|
122
124
|
|
|
@@ -154,12 +156,12 @@ interface HookExecutedEvent {
|
|
|
154
156
|
returnCode: number | null;
|
|
155
157
|
logCount: number;
|
|
156
158
|
input: {
|
|
157
|
-
request: { headers: Record<string, string>; body: string };
|
|
158
|
-
response: { headers: Record<string, string>; body: string };
|
|
159
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
160
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
159
161
|
};
|
|
160
162
|
output: {
|
|
161
|
-
request: { headers: Record<string, string>; body: string };
|
|
162
|
-
response: { headers: Record<string, string>; body: string };
|
|
163
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
164
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
163
165
|
};
|
|
164
166
|
};
|
|
165
167
|
}
|
|
@@ -226,7 +228,7 @@ interface RequestCompletedEvent {
|
|
|
226
228
|
finalResponse: {
|
|
227
229
|
status: number;
|
|
228
230
|
statusText: string;
|
|
229
|
-
headers: Record<string, string>;
|
|
231
|
+
headers: Record<string, string | string[]>;
|
|
230
232
|
body: string;
|
|
231
233
|
contentType: string;
|
|
232
234
|
isBase64?: boolean;
|
|
@@ -241,7 +243,7 @@ interface RequestCompletedEvent {
|
|
|
241
243
|
| `hookResults` | `Record<string, any>` | Per-hook execution results, keyed by hook name |
|
|
242
244
|
| `finalResponse.status` | `number` | HTTP status code |
|
|
243
245
|
| `finalResponse.statusText` | `string` | HTTP status text |
|
|
244
|
-
| `finalResponse.headers` | `Record<string, string>`
|
|
246
|
+
| `finalResponse.headers` | `Record<string, string \| string[]>` | Response headers |
|
|
245
247
|
| `finalResponse.body` | `string` | Response body (may be base64 if `isBase64` is `true`) |
|
|
246
248
|
| `finalResponse.contentType` | `string` | Content-Type of the response |
|
|
247
249
|
| `finalResponse.isBase64` | `boolean \| undefined` | Whether `body` is base64-encoded |
|
|
@@ -361,7 +363,7 @@ interface HttpWasmRequestCompletedEvent {
|
|
|
361
363
|
response: {
|
|
362
364
|
status: number;
|
|
363
365
|
statusText: string;
|
|
364
|
-
headers: Record<string, string>;
|
|
366
|
+
headers: Record<string, string | string[] | undefined>;
|
|
365
367
|
body: string;
|
|
366
368
|
contentType: string | null;
|
|
367
369
|
isBase64?: boolean;
|
|
@@ -370,14 +372,16 @@ interface HttpWasmRequestCompletedEvent {
|
|
|
370
372
|
}
|
|
371
373
|
```
|
|
372
374
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
|
376
|
-
|
|
|
377
|
-
| `response.
|
|
378
|
-
| `response.
|
|
379
|
-
| `response.
|
|
380
|
-
| `response.
|
|
375
|
+
`response.headers` mirrors Node's `IncomingHttpHeaders` — `undefined` values are dropped during JSON serialization and will not appear on the wire.
|
|
376
|
+
|
|
377
|
+
| Field | Type | Description |
|
|
378
|
+
| ---------------------- | ------------------------------------------------- | ----------------------------------------------------- |
|
|
379
|
+
| `response.status` | `number` | HTTP status code |
|
|
380
|
+
| `response.statusText` | `string` | HTTP status text |
|
|
381
|
+
| `response.headers` | `Record<string, string \| string[] \| undefined>` | Response headers (`undefined` values omitted in JSON) |
|
|
382
|
+
| `response.body` | `string` | Response body (may be base64 if `isBase64` is `true`) |
|
|
383
|
+
| `response.contentType` | `string \| null` | Content-Type, or `null` if absent |
|
|
384
|
+
| `response.isBase64` | `boolean \| undefined` | Whether `body` is base64-encoded |
|
|
381
385
|
|
|
382
386
|
**Example:**
|
|
383
387
|
|