@gcoredev/fastedge-test 0.1.6 → 0.2.0
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/frontend/assets/{index-BpdzhbRl.js → index-CiqeJ9rz.js} +24 -24
- package/dist/frontend/index.html +1 -1
- package/dist/lib/index.cjs +164 -66
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +164 -66
- package/dist/lib/runner/HeaderManager.d.ts +6 -4
- package/dist/lib/runner/HostFunctions.d.ts +5 -5
- package/dist/lib/runner/HttpWasmRunner.d.ts +22 -5
- package/dist/lib/runner/IStateManager.d.ts +7 -7
- package/dist/lib/runner/IWasmRunner.d.ts +42 -10
- package/dist/lib/runner/PortManager.d.ts +4 -1
- package/dist/lib/runner/PropertyResolver.d.ts +3 -3
- package/dist/lib/runner/ProxyWasmRunner.d.ts +5 -2
- 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 +2 -8
- package/dist/lib/schemas/config.d.ts +2 -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 +18634 -115
- package/dist/lib/test-framework/index.d.ts +2 -0
- package/dist/lib/test-framework/index.js +18651 -104
- package/dist/lib/test-framework/mock-origins.d.ts +56 -0
- package/dist/lib/test-framework/suite-runner.d.ts +16 -0
- package/dist/lib/test-framework/types.d.ts +1 -5
- package/dist/server.js +33 -33
- package/docs/API.md +48 -54
- package/docs/DEBUGGER.md +7 -8
- package/docs/INDEX.md +4 -1
- package/docs/RUNNER.md +126 -74
- package/docs/TEST_CONFIG.md +79 -40
- package/docs/TEST_FRAMEWORK.md +235 -36
- package/docs/WEBSOCKET.md +25 -21
- package/docs/quickstart.md +1 -13
- package/package.json +4 -1
- package/schemas/api-config.schema.json +5 -24
- package/schemas/api-load.schema.json +5 -0
- package/schemas/api-send.schema.json +0 -20
- package/schemas/fastedge-config.test.schema.json +5 -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_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
|
|
|
@@ -108,34 +111,53 @@ Object-based options for `runFlow()`. HTTP/2 pseudo-headers (`:method`, `:path`,
|
|
|
108
111
|
```typescript
|
|
109
112
|
interface FlowOptions {
|
|
110
113
|
url: string;
|
|
111
|
-
method?: string;
|
|
114
|
+
method?: string; // Default: "GET"
|
|
112
115
|
requestHeaders?: Record<string, string>;
|
|
113
|
-
requestBody?: string;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
responseHeaders?: Record<string, string>; // Default: {}
|
|
117
|
-
responseBody?: string; // Default: ""
|
|
118
|
-
properties?: Record<string, unknown>; // Default: {}
|
|
119
|
-
enforceProductionPropertyRules?: boolean; // Default: true
|
|
116
|
+
requestBody?: string; // Default: ""
|
|
117
|
+
properties?: Record<string, unknown>; // Default: {}
|
|
118
|
+
enforceProductionPropertyRules?: boolean; // Default: true
|
|
120
119
|
}
|
|
121
120
|
```
|
|
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
|
-
Object-based options for `runHttpRequest()`. Used with HTTP WASM apps (as opposed to CDN proxy-wasm filter apps).
|
|
160
|
+
Object-based options for `runHttpRequest()`. Used with HTTP WASM apps (as opposed to CDN proxy-wasm filter apps tested with `runFlow`).
|
|
139
161
|
|
|
140
162
|
```typescript
|
|
141
163
|
interface HttpRequestOptions {
|
|
@@ -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
|
|
@@ -243,24 +285,28 @@ Executes a complete request/response flow through the WASM filter. Object-based
|
|
|
243
285
|
The returned `FullFlowResult` has this shape:
|
|
244
286
|
|
|
245
287
|
```typescript
|
|
246
|
-
|
|
247
|
-
hookResults: Record<string, HookResult>;
|
|
288
|
+
type FullFlowResult = {
|
|
289
|
+
hookResults: Record<string, HookResult>;
|
|
248
290
|
finalResponse: {
|
|
249
291
|
status: number;
|
|
250
|
-
|
|
292
|
+
statusText: string;
|
|
293
|
+
headers: Record<string, string | string[]>;
|
|
251
294
|
body: string;
|
|
295
|
+
contentType: string;
|
|
296
|
+
isBase64?: boolean;
|
|
252
297
|
};
|
|
253
|
-
|
|
298
|
+
calculatedProperties?: Record<string, unknown>;
|
|
299
|
+
};
|
|
254
300
|
```
|
|
255
301
|
|
|
256
302
|
Hook results are accessed by camelCase key:
|
|
257
303
|
|
|
258
|
-
| Key
|
|
259
|
-
|
|
|
260
|
-
| `onRequestHeaders`
|
|
261
|
-
| `onRequestBody`
|
|
262
|
-
| `onResponseHeaders`
|
|
263
|
-
| `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 |
|
|
264
310
|
|
|
265
311
|
```typescript
|
|
266
312
|
const result = await runFlow(runner, {
|
|
@@ -268,8 +314,6 @@ const result = await runFlow(runner, {
|
|
|
268
314
|
method: "POST",
|
|
269
315
|
requestHeaders: { "content-type": "application/json" },
|
|
270
316
|
requestBody: '{"key":"value"}',
|
|
271
|
-
responseStatus: 201,
|
|
272
|
-
responseHeaders: { "x-upstream": "backend-1" },
|
|
273
317
|
});
|
|
274
318
|
|
|
275
319
|
// Access hook results
|
|
@@ -277,7 +321,8 @@ const reqHook = result.hookResults.onRequestHeaders;
|
|
|
277
321
|
const resHook = result.hookResults.onResponseHeaders;
|
|
278
322
|
|
|
279
323
|
// Access final response
|
|
280
|
-
console.log(result.finalResponse.status);
|
|
324
|
+
console.log(result.finalResponse.status);
|
|
325
|
+
console.log(result.finalResponse.contentType);
|
|
281
326
|
```
|
|
282
327
|
|
|
283
328
|
### runHttpRequest
|
|
@@ -288,7 +333,26 @@ function runHttpRequest(runner: IWasmRunner, options: HttpRequestOptions): Promi
|
|
|
288
333
|
|
|
289
334
|
Executes a single HTTP request through an HTTP WASM app. Object-based wrapper around the runner's `execute` method. Use this for WASM apps that handle HTTP requests directly, as opposed to CDN proxy-wasm filter apps tested with `runFlow`.
|
|
290
335
|
|
|
336
|
+
**Redirects are not followed.** The underlying fetch uses `redirect: "manual"`, so a 302 (or any 3xx) returned by the WASM is surfaced verbatim — `response.status` is `302` and `response.headers.location` is preserved. This matches FastEdge edge behaviour, where redirects are returned to the client rather than followed server-side.
|
|
337
|
+
|
|
338
|
+
`runHttpRequest` only targets the WASM app under test. `options.path` is a path on the local `fastedge-run` server, not a full URL. Following a redirect depends on the shape of `response.headers.location`:
|
|
339
|
+
|
|
340
|
+
- **Relative** (e.g. `/auth/complete`) — pass it directly as `path` in a second `runHttpRequest` call.
|
|
341
|
+
- **Absolute, same host** — extract `pathname + search` via `new URL()` and re-issue with that path.
|
|
342
|
+
- **Absolute, external host** — cannot be followed through the runner; assert on status and `Location` and stop there.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Assert on a 302 redirect and follow it (relative Location)
|
|
346
|
+
const response = await runHttpRequest(runner, { path: "/login" });
|
|
347
|
+
assertHttpStatus(response, 302);
|
|
348
|
+
assertHttpHeader(response, "location", "/dashboard");
|
|
349
|
+
|
|
350
|
+
const redirected = await runHttpRequest(runner, { path: response.headers["location"] as string });
|
|
351
|
+
assertHttpStatus(redirected, 200);
|
|
352
|
+
```
|
|
353
|
+
|
|
291
354
|
```typescript
|
|
355
|
+
// Standard request
|
|
292
356
|
const response = await runHttpRequest(runner, {
|
|
293
357
|
path: "/api/greet",
|
|
294
358
|
method: "GET",
|
|
@@ -312,6 +376,23 @@ Reads and validates a `fastedge-config.test.json` file. Returns the parsed `Test
|
|
|
312
376
|
const config = await loadConfigFile("./fastedge-config.test.json");
|
|
313
377
|
```
|
|
314
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
|
+
|
|
315
396
|
## Assertion Helpers
|
|
316
397
|
|
|
317
398
|
All assertion helpers throw an `Error` on failure, making them compatible with any test framework (vitest, jest, node:assert) or plain try/catch.
|
|
@@ -319,11 +400,11 @@ All assertion helpers throw an `Error` on failure, making them compatible with a
|
|
|
319
400
|
### Request Headers
|
|
320
401
|
|
|
321
402
|
```typescript
|
|
322
|
-
function assertRequestHeader(result: HookResult, name: string, expected?: string): void
|
|
403
|
+
function assertRequestHeader(result: HookResult, name: string, expected?: string | string[]): void
|
|
323
404
|
function assertNoRequestHeader(result: HookResult, name: string): void
|
|
324
405
|
```
|
|
325
406
|
|
|
326
|
-
`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.
|
|
327
408
|
|
|
328
409
|
`assertNoRequestHeader` asserts the named header is absent.
|
|
329
410
|
|
|
@@ -338,7 +419,7 @@ assertNoRequestHeader(hookResult, "x-internal-secret"); // absent
|
|
|
338
419
|
### Response Headers
|
|
339
420
|
|
|
340
421
|
```typescript
|
|
341
|
-
function assertResponseHeader(result: HookResult, name: string, expected?: string): void
|
|
422
|
+
function assertResponseHeader(result: HookResult, name: string, expected?: string | string[]): void
|
|
342
423
|
function assertNoResponseHeader(result: HookResult, name: string): void
|
|
343
424
|
```
|
|
344
425
|
|
|
@@ -356,9 +437,11 @@ assertNoResponseHeader(hookResult, "server");
|
|
|
356
437
|
|
|
357
438
|
```typescript
|
|
358
439
|
function assertFinalStatus(result: FullFlowResult, expected: number): void
|
|
359
|
-
function assertFinalHeader(result: FullFlowResult, name: string, expected?: string): void
|
|
440
|
+
function assertFinalHeader(result: FullFlowResult, name: string, expected?: string | string[]): void
|
|
360
441
|
```
|
|
361
442
|
|
|
443
|
+
Multi-value semantics on `assertFinalHeader` 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.
|
|
444
|
+
|
|
362
445
|
`assertFinalStatus` asserts the final response status code after the full flow completes.
|
|
363
446
|
|
|
364
447
|
`assertFinalHeader` asserts a header in `result.finalResponse.headers`. If `expected` is provided, also asserts the value.
|
|
@@ -440,7 +523,7 @@ These assertions operate on an `HttpResponse` returned by `runHttpRequest`. Use
|
|
|
440
523
|
|
|
441
524
|
```typescript
|
|
442
525
|
function assertHttpStatus(response: HttpResponse, expected: number): void
|
|
443
|
-
function assertHttpHeader(response: HttpResponse, name: string, expected?: string): void
|
|
526
|
+
function assertHttpHeader(response: HttpResponse, name: string, expected?: string | string[]): void
|
|
444
527
|
function assertHttpNoHeader(response: HttpResponse, name: string): void
|
|
445
528
|
function assertHttpBody(response: HttpResponse, expected: string): void
|
|
446
529
|
function assertHttpBodyContains(response: HttpResponse, substring: string): void
|
|
@@ -452,7 +535,18 @@ function assertHttpNoLog(response: HttpResponse, messageSubstring: string): void
|
|
|
452
535
|
|
|
453
536
|
`assertHttpStatus` — asserts the response status code.
|
|
454
537
|
|
|
455
|
-
`assertHttpHeader` — asserts the named header exists (case-insensitive). If `expected` is
|
|
538
|
+
`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.
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// Single-valued header — exact match
|
|
542
|
+
assertHttpHeader(response, "content-type", "application/json");
|
|
543
|
+
|
|
544
|
+
// Multi-valued header — one-of-many match
|
|
545
|
+
assertHttpHeader(response, "set-cookie", "sid=abc; Path=/");
|
|
546
|
+
|
|
547
|
+
// Multi-valued header — exact array
|
|
548
|
+
assertHttpHeader(response, "set-cookie", ["sid=abc; Path=/", "theme=dark; Path=/"]);
|
|
549
|
+
```
|
|
456
550
|
|
|
457
551
|
`assertHttpNoHeader` — asserts the named header is absent (case-insensitive).
|
|
458
552
|
|
|
@@ -483,6 +577,113 @@ const data = assertHttpJson<{ items: unknown[] }>(response);
|
|
|
483
577
|
console.log(data.items.length);
|
|
484
578
|
```
|
|
485
579
|
|
|
580
|
+
## Origin Mocking
|
|
581
|
+
|
|
582
|
+
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.
|
|
583
|
+
|
|
584
|
+
### Basic Usage
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { mockOrigins } from "@gcoredev/fastedge-test/test";
|
|
588
|
+
|
|
589
|
+
let mocks: MockOriginsHandle | null = null;
|
|
590
|
+
|
|
591
|
+
beforeEach(() => {
|
|
592
|
+
mocks = mockOrigins();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
afterEach(async () => {
|
|
596
|
+
await mocks?.close();
|
|
597
|
+
mocks = null;
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("renders a retry UI when the origin returns 503", async () => {
|
|
601
|
+
mocks!
|
|
602
|
+
.origin("https://origin.example.com")
|
|
603
|
+
.intercept({ path: "/api/resource" })
|
|
604
|
+
.reply(503, "upstream down");
|
|
605
|
+
|
|
606
|
+
const result = await runFlow(runner, {
|
|
607
|
+
url: "https://origin.example.com/api/resource",
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
assertFinalStatus(result, 503);
|
|
611
|
+
mocks!.assertAllCalled();
|
|
612
|
+
});
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
`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.
|
|
616
|
+
|
|
617
|
+
### Multi-Upstream with `proxy_http_call`
|
|
618
|
+
|
|
619
|
+
Every upstream the WASM initiates via `proxy_http_call` goes through the same global dispatcher, so multiple origins can be stacked in one setup:
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
mocks!
|
|
623
|
+
.origin("https://auth.example.com")
|
|
624
|
+
.intercept({ path: "/token", method: "POST" })
|
|
625
|
+
.reply(200, '{"jwt":"xyz"}');
|
|
626
|
+
|
|
627
|
+
mocks!
|
|
628
|
+
.origin("https://analytics.example.com")
|
|
629
|
+
.intercept({ path: "/event", method: "POST" })
|
|
630
|
+
.reply(204);
|
|
631
|
+
|
|
632
|
+
mocks!
|
|
633
|
+
.origin("https://origin.example.com")
|
|
634
|
+
.intercept({ path: "/" })
|
|
635
|
+
.reply(200, "hello");
|
|
636
|
+
|
|
637
|
+
const result = await runFlow(runner, {
|
|
638
|
+
url: "https://origin.example.com/",
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Fails if any registered interceptor was never hit
|
|
642
|
+
mocks!.assertAllCalled();
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Lifecycle and `assertAllCalled`
|
|
646
|
+
|
|
647
|
+
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.
|
|
648
|
+
|
|
649
|
+
`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.
|
|
650
|
+
|
|
651
|
+
### `allowNetConnect` and the HTTP-WASM caveat
|
|
652
|
+
|
|
653
|
+
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.
|
|
654
|
+
|
|
655
|
+
**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:
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
mocks = mockOrigins({
|
|
659
|
+
allowNetConnect: [/^127\.0\.0\.1/, /^localhost/],
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
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.
|
|
664
|
+
|
|
665
|
+
### Advanced: the raw `MockAgent`
|
|
666
|
+
|
|
667
|
+
`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.
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
mocks!.agent
|
|
671
|
+
.get("https://flaky.example.com")
|
|
672
|
+
.intercept({ path: "/api" })
|
|
673
|
+
.reply(503, "down")
|
|
674
|
+
.times(2);
|
|
675
|
+
|
|
676
|
+
mocks!.agent
|
|
677
|
+
.get("https://flaky.example.com")
|
|
678
|
+
.intercept({ path: "/api" })
|
|
679
|
+
.reply(200, "ok")
|
|
680
|
+
.persist();
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Pseudo-headers and the outbound fetch
|
|
684
|
+
|
|
685
|
+
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.
|
|
686
|
+
|
|
486
687
|
## CI Integration
|
|
487
688
|
|
|
488
689
|
`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.
|
|
@@ -566,8 +767,6 @@ const suite = defineTestSuite({
|
|
|
566
767
|
async run(runner) {
|
|
567
768
|
const result = await runFlow(runner, {
|
|
568
769
|
url: "https://cdn.example.com/static/app.js",
|
|
569
|
-
responseStatus: 200,
|
|
570
|
-
responseHeaders: { "content-type": "application/javascript" },
|
|
571
770
|
});
|
|
572
771
|
|
|
573
772
|
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
|
|
package/docs/quickstart.md
CHANGED
|
@@ -62,8 +62,6 @@ const suite = defineTestSuite({
|
|
|
62
62
|
const result = await runFlow(runner, {
|
|
63
63
|
url: "https://example.com/",
|
|
64
64
|
method: "GET",
|
|
65
|
-
responseStatus: 200,
|
|
66
|
-
responseBody: "hello",
|
|
67
65
|
});
|
|
68
66
|
|
|
69
67
|
const header = result.finalResponse.headers["x-custom-header"];
|
|
@@ -102,10 +100,6 @@ try {
|
|
|
102
100
|
":scheme": "https",
|
|
103
101
|
},
|
|
104
102
|
"", // request body
|
|
105
|
-
{}, // response headers
|
|
106
|
-
"", // response body
|
|
107
|
-
200, // response status
|
|
108
|
-
"OK", // response status text
|
|
109
103
|
{}, // properties
|
|
110
104
|
true, // enforceProductionPropertyRules
|
|
111
105
|
);
|
|
@@ -130,7 +124,7 @@ See [RUNNER.md](RUNNER.md) for the full `IWasmRunner` interface, `RunnerConfig`
|
|
|
130
124
|
|
|
131
125
|
## Configuration
|
|
132
126
|
|
|
133
|
-
Place a `fastedge-config.test.json` file in `.fastedge-debug/` at your project root to define default request
|
|
127
|
+
Place a `fastedge-config.test.json` file in `.fastedge-debug/` at your project root to define the default request and WASM properties for the interactive debugger.
|
|
134
128
|
|
|
135
129
|
```json
|
|
136
130
|
{
|
|
@@ -147,12 +141,6 @@ Place a `fastedge-config.test.json` file in `.fastedge-debug/` at your project r
|
|
|
147
141
|
},
|
|
148
142
|
"body": ""
|
|
149
143
|
},
|
|
150
|
-
"response": {
|
|
151
|
-
"headers": {
|
|
152
|
-
"content-type": "text/html"
|
|
153
|
-
},
|
|
154
|
-
"body": "<html><body>Hello</body></html>"
|
|
155
|
-
},
|
|
156
144
|
"properties": {
|
|
157
145
|
"my-property": "value"
|
|
158
146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gcoredev/fastedge-test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
|
+
"types": "./dist/lib/index.d.ts",
|
|
14
15
|
"import": "./dist/lib/index.js",
|
|
15
16
|
"require": "./dist/lib/index.cjs"
|
|
16
17
|
},
|
|
17
18
|
"./server": "./dist/server.js",
|
|
18
19
|
"./test": {
|
|
20
|
+
"types": "./dist/lib/test-framework/index.d.ts",
|
|
19
21
|
"import": "./dist/lib/test-framework/index.js",
|
|
20
22
|
"require": "./dist/lib/test-framework/index.cjs"
|
|
21
23
|
},
|
|
@@ -83,6 +85,7 @@
|
|
|
83
85
|
"immer": "^11.1.3",
|
|
84
86
|
"react": "^19.2.3",
|
|
85
87
|
"react-dom": "^19.2.3",
|
|
88
|
+
"undici": "^6.0.0",
|
|
86
89
|
"ws": "^8.19.0",
|
|
87
90
|
"zod": "^4.3.6",
|
|
88
91
|
"zod-to-json-schema": "^3.25.1",
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"type": "string",
|
|
53
53
|
"const": "http-wasm"
|
|
54
54
|
},
|
|
55
|
+
"httpPort": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"minimum": 1024,
|
|
58
|
+
"maximum": 65535
|
|
59
|
+
},
|
|
55
60
|
"request": {
|
|
56
61
|
"type": "object",
|
|
57
62
|
"properties": {
|
|
@@ -174,30 +179,6 @@
|
|
|
174
179
|
"body"
|
|
175
180
|
],
|
|
176
181
|
"additionalProperties": false
|
|
177
|
-
},
|
|
178
|
-
"response": {
|
|
179
|
-
"type": "object",
|
|
180
|
-
"properties": {
|
|
181
|
-
"headers": {
|
|
182
|
-
"default": {},
|
|
183
|
-
"type": "object",
|
|
184
|
-
"propertyNames": {
|
|
185
|
-
"type": "string"
|
|
186
|
-
},
|
|
187
|
-
"additionalProperties": {
|
|
188
|
-
"type": "string"
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
"body": {
|
|
192
|
-
"default": "",
|
|
193
|
-
"type": "string"
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
"required": [
|
|
197
|
-
"headers",
|
|
198
|
-
"body"
|
|
199
|
-
],
|
|
200
|
-
"additionalProperties": false
|
|
201
182
|
}
|
|
202
183
|
},
|
|
203
184
|
"required": [
|
|
@@ -41,26 +41,6 @@
|
|
|
41
41
|
},
|
|
42
42
|
"additionalProperties": false
|
|
43
43
|
},
|
|
44
|
-
"response": {
|
|
45
|
-
"type": "object",
|
|
46
|
-
"properties": {
|
|
47
|
-
"headers": {
|
|
48
|
-
"default": {},
|
|
49
|
-
"type": "object",
|
|
50
|
-
"propertyNames": {
|
|
51
|
-
"type": "string"
|
|
52
|
-
},
|
|
53
|
-
"additionalProperties": {
|
|
54
|
-
"type": "string"
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"body": {
|
|
58
|
-
"default": "",
|
|
59
|
-
"type": "string"
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
"additionalProperties": false
|
|
63
|
-
},
|
|
64
44
|
"properties": {
|
|
65
45
|
"default": {},
|
|
66
46
|
"type": "object",
|