@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/RUNNER.md
CHANGED
|
@@ -102,10 +102,6 @@ interface IWasmRunner {
|
|
|
102
102
|
method: string,
|
|
103
103
|
headers: Record<string, string>,
|
|
104
104
|
body: string,
|
|
105
|
-
responseHeaders: Record<string, string>,
|
|
106
|
-
responseBody: string,
|
|
107
|
-
responseStatus: number,
|
|
108
|
-
responseStatusText: string,
|
|
109
105
|
properties: Record<string, unknown>,
|
|
110
106
|
enforceProductionPropertyRules: boolean
|
|
111
107
|
): Promise<FullFlowResult>;
|
|
@@ -126,6 +122,14 @@ load(bufferOrPath: Buffer | string, config?: RunnerConfig): Promise<void>
|
|
|
126
122
|
|
|
127
123
|
Calling `load()` again on the same runner replaces the current module and restarts any underlying process.
|
|
128
124
|
|
|
125
|
+
**`httpPort` pinning (HTTP-WASM only).** When `config.httpPort` is set, the spawned `fastedge-run` process is bound to that specific port instead of allocating from the dynamic pool (8100–8199). If the port is already in use, `load()` throws:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
fastedge-run port <N> is not available — release it or choose a different httpPort in fastedge-config.test.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
There is no fallback to dynamic allocation — pinning is only useful if the address is stable. Intended for Codespaces/Docker port-forwarding setups, live-preview URLs, or external tooling that requires a fixed target. For proxy-wasm runners, `httpPort` is ignored.
|
|
132
|
+
|
|
129
133
|
### execute (HTTP-WASM)
|
|
130
134
|
|
|
131
135
|
Executes an HTTP request through the WASM module. Only available on `HttpWasmRunner` (http-wasm). Calling this on a `ProxyWasmRunner` throws.
|
|
@@ -134,7 +138,33 @@ Executes an HTTP request through the WASM module. Only available on `HttpWasmRun
|
|
|
134
138
|
execute(request: HttpRequest): Promise<HttpResponse>
|
|
135
139
|
```
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
`request.path` is a path on the locally spawned `fastedge-run` server, not a full URL. The runner forwards the request to that local process and returns the response including logs captured from the process stdout/stderr.
|
|
142
|
+
|
|
143
|
+
**Redirects are not followed.** The underlying fetch uses `redirect: "manual"`, so 3xx responses reach the caller intact — status code and `Location` header — rather than being transparently followed. This matches FastEdge edge behavior, where redirects are returned to the client rather than followed server-side, and lets tests assert on redirect status and `Location`.
|
|
144
|
+
|
|
145
|
+
To follow a redirect, inspect `response.headers.location` and handle by `Location` shape:
|
|
146
|
+
|
|
147
|
+
- **Relative Location** (e.g. `/new-path`) — reuse directly as `request.path`.
|
|
148
|
+
- **Absolute same-host Location** (e.g. `http://localhost:8100/new-path`) — parse via `new URL()` and re-issue with `pathname + search` as `request.path`.
|
|
149
|
+
- **Absolute cross-host Location** (e.g. `https://other.example.com/`) — cannot be followed through the runner; the 3xx response is the terminal state for the test.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { createRunner } from '@gcoredev/fastedge-test';
|
|
153
|
+
|
|
154
|
+
const runner = await createRunner('./my-http-app.wasm');
|
|
155
|
+
|
|
156
|
+
// Relative Location: reuse directly as path
|
|
157
|
+
let response = await runner.execute({ path: '/moved', method: 'GET', headers: {} });
|
|
158
|
+
if (response.status >= 300 && response.status < 400 && response.headers['location']) {
|
|
159
|
+
response = await runner.execute({
|
|
160
|
+
path: response.headers['location'] as string, // e.g. "/new-location"
|
|
161
|
+
method: 'GET',
|
|
162
|
+
headers: {},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await runner.cleanup();
|
|
167
|
+
```
|
|
138
168
|
|
|
139
169
|
### callHook (Proxy-WASM)
|
|
140
170
|
|
|
@@ -158,10 +188,6 @@ callFullFlow(
|
|
|
158
188
|
method: string,
|
|
159
189
|
headers: Record<string, string>,
|
|
160
190
|
body: string,
|
|
161
|
-
responseHeaders: Record<string, string>,
|
|
162
|
-
responseBody: string,
|
|
163
|
-
responseStatus: number,
|
|
164
|
-
responseStatusText: string,
|
|
165
191
|
properties: Record<string, unknown>,
|
|
166
192
|
enforceProductionPropertyRules: boolean
|
|
167
193
|
): Promise<FullFlowResult>
|
|
@@ -175,16 +201,14 @@ callFullFlow(
|
|
|
175
201
|
| `method` | `string` | HTTP method |
|
|
176
202
|
| `headers` | `Record<string, string>` | Request headers |
|
|
177
203
|
| `body` | `string` | Request body |
|
|
178
|
-
| `responseHeaders` | `Record<string, string>` | Upstream response headers (used as initial state for response hooks) |
|
|
179
|
-
| `responseBody` | `string` | Upstream response body |
|
|
180
|
-
| `responseStatus` | `number` | Upstream response status code |
|
|
181
|
-
| `responseStatusText` | `string` | Upstream response status text |
|
|
182
204
|
| `properties` | `Record<string, unknown>` | Shared properties passed to all hooks |
|
|
183
205
|
| `enforceProductionPropertyRules` | `boolean` | When `true`, restricts property access to match CDN production behavior |
|
|
184
206
|
|
|
207
|
+
The upstream response is generated at runtime — either by a real HTTP fetch against `url`, or by the built-in responder when `url === "built-in"`. There is no caller-provided mock response.
|
|
208
|
+
|
|
185
209
|
Hook execution order: `onRequestHeaders` → `onRequestBody` → *(real HTTP fetch or built-in responder)* → `onResponseHeaders` → `onResponseBody`.
|
|
186
210
|
|
|
187
|
-
**Local response short-circuit
|
|
211
|
+
**Local response short-circuit.** If a WASM module calls `proxy_send_local_response` during `onRequestHeaders` or `onRequestBody` and returns `StopIteration` (return code `1`), the remaining hooks and origin fetch are skipped. The `finalResponse` in the result is built from the locally-sent status, headers, and body — matching CDN production behavior. This is how redirect modules (e.g., geo-redirect) and early error responses work.
|
|
188
212
|
|
|
189
213
|
Only available on `ProxyWasmRunner`. Calling on `HttpWasmRunner` throws.
|
|
190
214
|
|
|
@@ -272,23 +296,25 @@ When testing proxy-wasm modules without a real origin server, pass `BUILTIN_SHOR
|
|
|
272
296
|
|
|
273
297
|
```typescript
|
|
274
298
|
import { createRunner, BUILTIN_SHORTHAND } from '@gcoredev/fastedge-test';
|
|
299
|
+
import type { FullFlowResult } from '@gcoredev/fastedge-test';
|
|
275
300
|
|
|
276
301
|
const runner = await createRunner('./my-cdn-app.wasm');
|
|
277
|
-
const result = await runner.callFullFlow(
|
|
302
|
+
const result: FullFlowResult = await runner.callFullFlow(
|
|
278
303
|
BUILTIN_SHORTHAND, // no origin fetch
|
|
279
304
|
'GET',
|
|
280
305
|
{ 'accept': 'application/json' },
|
|
281
306
|
'',
|
|
282
|
-
{},
|
|
307
|
+
{}, // properties
|
|
308
|
+
true, // enforceProductionPropertyRules
|
|
283
309
|
);
|
|
284
310
|
```
|
|
285
311
|
|
|
286
312
|
**Built-in responder behavior** — controlled by request headers set before the origin phase:
|
|
287
313
|
|
|
288
|
-
| Header | Effect
|
|
289
|
-
| -------------------- |
|
|
290
|
-
| `x-debugger-status` | HTTP status code for the generated response (default: `200`)
|
|
291
|
-
| `x-debugger-content` | Response body mode: `"body-only"`, `"status-only"`, or full JSON echo (default)
|
|
314
|
+
| Header | Effect |
|
|
315
|
+
| -------------------- | -------------------------------------------------------------------------------- |
|
|
316
|
+
| `x-debugger-status` | HTTP status code for the generated response (default: `200`) |
|
|
317
|
+
| `x-debugger-content` | Response body mode: `"body-only"`, `"status-only"`, or full JSON echo (default) |
|
|
292
318
|
|
|
293
319
|
When `x-debugger-content` is omitted, the built-in responder returns a JSON echo of the request method, headers, body, and URL. Both control headers are stripped before response hooks execute so they do not appear in hook input state.
|
|
294
320
|
|
|
@@ -306,15 +332,17 @@ interface RunnerConfig {
|
|
|
306
332
|
};
|
|
307
333
|
enforceProductionPropertyRules?: boolean;
|
|
308
334
|
runnerType?: WasmType;
|
|
335
|
+
httpPort?: number;
|
|
309
336
|
}
|
|
310
337
|
```
|
|
311
338
|
|
|
312
|
-
| Field | Type | Default | Description
|
|
313
|
-
| -------------------------------- | ---------- | --------------- |
|
|
314
|
-
| `dotenv.enabled` | `boolean` | `false` | Whether to load `.env` files
|
|
315
|
-
| `dotenv.path` | `string` | `undefined` | Directory to load dotenv files from. When omitted, `fastedge-run` uses the process CWD — correct for most npm package users whose `.env` files live at the project root. Only set this when your dotenv files are in a non-standard location (e.g. a test fixture directory).
|
|
316
|
-
| `enforceProductionPropertyRules` | `boolean` | `true` | Restrict property access to match CDN production behavior
|
|
317
|
-
| `runnerType` | `WasmType` | auto-detected
|
|
339
|
+
| Field | Type | Default | Description |
|
|
340
|
+
| -------------------------------- | ---------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
341
|
+
| `dotenv.enabled` | `boolean` | `false` | Whether to load `.env` files |
|
|
342
|
+
| `dotenv.path` | `string` | `undefined` | Directory to load dotenv files from. When omitted, `fastedge-run` uses the process CWD — correct for most npm package users whose `.env` files live at the project root. Only set this when your dotenv files are in a non-standard location (e.g. a test fixture directory). |
|
|
343
|
+
| `enforceProductionPropertyRules` | `boolean` | `true` | Restrict property access to match CDN production behavior |
|
|
344
|
+
| `runnerType` | `WasmType` | *auto-detected* | Override WASM type detection |
|
|
345
|
+
| `httpPort` | `number` | `undefined` | HTTP-WASM only. Pin the spawned `fastedge-run` subprocess to a specific port instead of allocating from the dynamic pool (8100–8199). `load()` throws if the port is busy — there is no fallback to dynamic allocation. Intended for Codespaces/Docker port-forwarding or external tooling requiring a fixed address. Ignored for proxy-wasm. |
|
|
318
346
|
|
|
319
347
|
### HttpRequest & HttpResponse
|
|
320
348
|
|
|
@@ -329,7 +357,11 @@ interface HttpRequest {
|
|
|
329
357
|
interface HttpResponse {
|
|
330
358
|
status: number;
|
|
331
359
|
statusText: string;
|
|
332
|
-
|
|
360
|
+
// Node's IncomingHttpHeaders (from "node:http"):
|
|
361
|
+
// known single-valued headers (content-type, location, etag, …) are typed as string
|
|
362
|
+
// set-cookie is always string[] when present
|
|
363
|
+
// unknown headers are string | string[] | undefined
|
|
364
|
+
headers: IncomingHttpHeaders;
|
|
333
365
|
body: string;
|
|
334
366
|
contentType: string | null;
|
|
335
367
|
isBase64?: boolean;
|
|
@@ -343,20 +375,37 @@ interface HttpResponse {
|
|
|
343
375
|
|
|
344
376
|
`HttpResponse.logs` contains log entries captured from the `fastedge-run` process stdout/stderr during the request.
|
|
345
377
|
|
|
378
|
+
**Multi-valued headers.** `Set-Cookie` is preserved as a `string[]` — each `Set-Cookie` header emitted by the WASM app (or an upstream origin) becomes a separate array entry. This matches RFC 6265 §3 and Node's fetch behavior. Example:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const response = await runner.execute({ path: '/login', method: 'POST', headers: {} });
|
|
382
|
+
const cookies = response.headers['set-cookie']; // string[] | undefined
|
|
383
|
+
for (const cookie of cookies ?? []) {
|
|
384
|
+
console.log(cookie);
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Single-valued headers read as plain strings with no narrowing needed:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const location = response.headers['location']; // string | undefined
|
|
392
|
+
const contentType = response.headers['content-type']; // string | undefined
|
|
393
|
+
```
|
|
394
|
+
|
|
346
395
|
### HookCall
|
|
347
396
|
|
|
348
397
|
```typescript
|
|
349
398
|
type HookCall = {
|
|
350
399
|
hook: string;
|
|
351
400
|
request: {
|
|
352
|
-
headers:
|
|
401
|
+
headers: HeaderRecord;
|
|
353
402
|
body: string;
|
|
354
403
|
method?: string;
|
|
355
404
|
path?: string;
|
|
356
405
|
scheme?: string;
|
|
357
406
|
};
|
|
358
|
-
response
|
|
359
|
-
headers:
|
|
407
|
+
response?: {
|
|
408
|
+
headers: HeaderRecord;
|
|
360
409
|
body: string;
|
|
361
410
|
status?: number;
|
|
362
411
|
statusText?: string;
|
|
@@ -367,14 +416,16 @@ type HookCall = {
|
|
|
367
416
|
};
|
|
368
417
|
```
|
|
369
418
|
|
|
370
|
-
| Field | Description
|
|
371
|
-
| -------------------------------- |
|
|
372
|
-
| `hook` | Hook name: `"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"`
|
|
373
|
-
| `request` | Request state passed to the hook
|
|
374
|
-
| `response` |
|
|
375
|
-
| `properties` | Shared properties (e.g. `request.path`, `vm_config`, `plugin_config`)
|
|
376
|
-
| `dotenvEnabled` | Optional per-call dotenv override. Use `applyDotenv()` for persistent changes.
|
|
377
|
-
| `enforceProductionPropertyRules` | Defaults to `true`. Set to `false` to allow property reads that would be blocked on production CDN.
|
|
419
|
+
| Field | Description |
|
|
420
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| `hook` | Hook name: `"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"` |
|
|
422
|
+
| `request` | Request state passed to the hook |
|
|
423
|
+
| `response` | Seed state for response hooks called via `callHook()`. Ignored by `callFullFlow()` and by request hooks — the full-flow path generates the upstream response at runtime. |
|
|
424
|
+
| `properties` | Shared properties (e.g. `request.path`, `vm_config`, `plugin_config`) |
|
|
425
|
+
| `dotenvEnabled` | Optional per-call dotenv override. Use `applyDotenv()` for persistent changes. |
|
|
426
|
+
| `enforceProductionPropertyRules` | Defaults to `true`. Set to `false` to allow property reads that would be blocked on production CDN. |
|
|
427
|
+
|
|
428
|
+
`HeaderRecord` is `Record<string, string | string[]>` — multi-valued headers (e.g. multiple `Set-Cookie`) are represented as `string[]`.
|
|
378
429
|
|
|
379
430
|
### HookResult
|
|
380
431
|
|
|
@@ -383,26 +434,26 @@ type HookResult = {
|
|
|
383
434
|
returnCode: number | null;
|
|
384
435
|
logs: { level: number; message: string }[];
|
|
385
436
|
input: {
|
|
386
|
-
request: { headers:
|
|
387
|
-
response: { headers:
|
|
437
|
+
request: { headers: HeaderRecord; body: string };
|
|
438
|
+
response: { headers: HeaderRecord; body: string };
|
|
388
439
|
properties?: Record<string, unknown>;
|
|
389
440
|
};
|
|
390
441
|
output: {
|
|
391
|
-
request: { headers:
|
|
392
|
-
response: { headers:
|
|
442
|
+
request: { headers: HeaderRecord; body: string };
|
|
443
|
+
response: { headers: HeaderRecord; body: string };
|
|
393
444
|
properties?: Record<string, unknown>;
|
|
394
445
|
};
|
|
395
446
|
properties: Record<string, unknown>;
|
|
396
447
|
};
|
|
397
448
|
```
|
|
398
449
|
|
|
399
|
-
| Field | Description
|
|
400
|
-
| ------------ |
|
|
401
|
-
| `returnCode` | The numeric value returned by the WASM hook export, or `null` if the export was not found
|
|
402
|
-
| `logs` | Log entries emitted via `proxy_log` during hook execution
|
|
403
|
-
| `input` | Request/response state as seen by the hook before execution
|
|
404
|
-
| `output` | Request/response state after hook execution (reflects WASM mutations)
|
|
405
|
-
| `properties` | All shared properties after hook execution
|
|
450
|
+
| Field | Description |
|
|
451
|
+
| ------------ | ------------------------------------------------------------------------------------------- |
|
|
452
|
+
| `returnCode` | The numeric value returned by the WASM hook export, or `null` if the export was not found |
|
|
453
|
+
| `logs` | Log entries emitted via `proxy_log` during hook execution |
|
|
454
|
+
| `input` | Request/response state as seen by the hook before execution |
|
|
455
|
+
| `output` | Request/response state after hook execution (reflects WASM mutations) |
|
|
456
|
+
| `properties` | All shared properties after hook execution |
|
|
406
457
|
|
|
407
458
|
### FullFlowResult
|
|
408
459
|
|
|
@@ -412,7 +463,7 @@ type FullFlowResult = {
|
|
|
412
463
|
finalResponse: {
|
|
413
464
|
status: number;
|
|
414
465
|
statusText: string;
|
|
415
|
-
headers:
|
|
466
|
+
headers: HeaderRecord;
|
|
416
467
|
body: string;
|
|
417
468
|
contentType: string;
|
|
418
469
|
isBase64?: boolean;
|
|
@@ -421,19 +472,23 @@ type FullFlowResult = {
|
|
|
421
472
|
};
|
|
422
473
|
```
|
|
423
474
|
|
|
424
|
-
| Field | Description
|
|
425
|
-
| ---------------------- |
|
|
426
|
-
| `hookResults` | A `Record` keyed by hook name (`"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"`), each containing a `HookResult`
|
|
427
|
-
| `finalResponse` | The final response after all hooks have executed, or the local response if a hook short-circuited (see `callFullFlow`). `body` is base64-encoded when `isBase64` is `true`.
|
|
428
|
-
| `calculatedProperties` | Runtime properties computed from the request URL (e.g. `request.path`, `request.host`)
|
|
475
|
+
| Field | Description |
|
|
476
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
477
|
+
| `hookResults` | A `Record` keyed by hook name (`"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"`), each containing a `HookResult` |
|
|
478
|
+
| `finalResponse` | The final response after all hooks have executed, or the local response if a hook short-circuited (see `callFullFlow`). `body` is base64-encoded when `isBase64` is `true`. |
|
|
479
|
+
| `calculatedProperties` | Runtime properties computed from the request URL (e.g. `request.path`, `request.host`) |
|
|
429
480
|
|
|
430
481
|
### Supporting Types
|
|
431
482
|
|
|
432
483
|
```typescript
|
|
433
484
|
type WasmType = 'http-wasm' | 'proxy-wasm';
|
|
434
485
|
|
|
486
|
+
// Single-valued headers only — used as callFullFlow input parameters
|
|
435
487
|
type HeaderMap = Record<string, string>;
|
|
436
488
|
|
|
489
|
+
// Single- or multi-valued headers — used in HookCall, HookResult, and FullFlowResult
|
|
490
|
+
type HeaderRecord = Record<string, string | string[]>;
|
|
491
|
+
|
|
437
492
|
type LogEntry = {
|
|
438
493
|
level: number;
|
|
439
494
|
message: string;
|
|
@@ -461,7 +516,7 @@ interface IStateManager {
|
|
|
461
516
|
emitRequestStarted(
|
|
462
517
|
url: string,
|
|
463
518
|
method: string,
|
|
464
|
-
headers: Record<string, string>,
|
|
519
|
+
headers: Record<string, string | string[]>,
|
|
465
520
|
source?: EventSource,
|
|
466
521
|
): void;
|
|
467
522
|
|
|
@@ -470,12 +525,12 @@ interface IStateManager {
|
|
|
470
525
|
returnCode: number | null,
|
|
471
526
|
logCount: number,
|
|
472
527
|
input: {
|
|
473
|
-
request: { headers: Record<string, string>; body: string };
|
|
474
|
-
response: { headers: Record<string, string>; body: string };
|
|
528
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
529
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
475
530
|
},
|
|
476
531
|
output: {
|
|
477
|
-
request: { headers: Record<string, string>; body: string };
|
|
478
|
-
response: { headers: Record<string, string>; body: string };
|
|
532
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
533
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
479
534
|
},
|
|
480
535
|
source?: EventSource,
|
|
481
536
|
): void;
|
|
@@ -485,7 +540,7 @@ interface IStateManager {
|
|
|
485
540
|
finalResponse: {
|
|
486
541
|
status: number;
|
|
487
542
|
statusText: string;
|
|
488
|
-
headers: Record<string, string>;
|
|
543
|
+
headers: Record<string, string | string[]>;
|
|
489
544
|
body: string;
|
|
490
545
|
contentType: string;
|
|
491
546
|
isBase64?: boolean;
|
|
@@ -507,7 +562,7 @@ interface IStateManager {
|
|
|
507
562
|
response: {
|
|
508
563
|
status: number;
|
|
509
564
|
statusText: string;
|
|
510
|
-
headers: Record<string, string>;
|
|
565
|
+
headers: Record<string, string | string[] | undefined>;
|
|
511
566
|
body: string;
|
|
512
567
|
contentType: string | null;
|
|
513
568
|
isBase64?: boolean;
|
|
@@ -538,19 +593,15 @@ async function testCdnApp() {
|
|
|
538
593
|
try {
|
|
539
594
|
// Execute the full CDN request/response lifecycle
|
|
540
595
|
const result: FullFlowResult = await runner.callFullFlow(
|
|
541
|
-
'https://example.com/api/data',
|
|
542
|
-
'GET',
|
|
543
|
-
{ 'accept': 'application/json' },
|
|
544
|
-
'',
|
|
545
|
-
{ 'content-type': 'application/json' }, // upstream response headers
|
|
546
|
-
'{"key":"value"}', // upstream response body
|
|
547
|
-
200, // upstream response status
|
|
548
|
-
'OK', // upstream response status text
|
|
596
|
+
'https://example.com/api/data', // request URL
|
|
597
|
+
'GET', // method
|
|
598
|
+
{ 'accept': 'application/json' }, // request headers
|
|
599
|
+
'', // request body
|
|
549
600
|
{
|
|
550
601
|
'request.path': '/api/data',
|
|
551
602
|
'request.host': 'example.com',
|
|
552
|
-
},
|
|
553
|
-
true,
|
|
603
|
+
}, // shared properties
|
|
604
|
+
true, // enforce production property rules
|
|
554
605
|
);
|
|
555
606
|
|
|
556
607
|
// Inspect hook results
|
|
@@ -606,11 +657,12 @@ async function testCdnAppOffline() {
|
|
|
606
657
|
try {
|
|
607
658
|
// Use built-in responder — no origin server required
|
|
608
659
|
const result: FullFlowResult = await runner.callFullFlow(
|
|
609
|
-
BUILTIN_SHORTHAND,
|
|
660
|
+
BUILTIN_SHORTHAND, // generates a local response instead of fetching
|
|
610
661
|
'GET',
|
|
611
662
|
{ 'accept': 'application/json' },
|
|
612
663
|
'',
|
|
613
|
-
{},
|
|
664
|
+
{}, // properties
|
|
665
|
+
true, // enforceProductionPropertyRules
|
|
614
666
|
);
|
|
615
667
|
|
|
616
668
|
console.log('Final status:', result.finalResponse.status);
|
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,27 +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 | Type | Required (Schema) | Default
|
|
26
|
-
| -------------------- | --------- | -------------------------------------- |
|
|
27
|
-
| `$schema` | `string` | No | —
|
|
28
|
-
| `description` | `string` | No | —
|
|
29
|
-
| `wasm` | `object` | No | —
|
|
30
|
-
| `wasm.path` | `string` | Yes (if `wasm` present) | —
|
|
31
|
-
| `wasm.description` | `string` | No | —
|
|
32
|
-
| `appType` | `string` | Yes (schema) / CDN has runtime default | `"proxy-wasm"`
|
|
33
|
-
| `request` | `object` | **Yes** | —
|
|
34
|
-
| `request.method` | `string` | Yes (schema) / runtime default | `"GET"`
|
|
35
|
-
| `request.url` | `string` | **Yes** (CDN only) | —
|
|
36
|
-
| `request.path` | `string` | **Yes** (HTTP-WASM only) | —
|
|
37
|
-
| `request.headers` | `object` | Yes (schema) / runtime default | `{}`
|
|
38
|
-
| `request.body` | `string` | Yes (schema) / runtime default | `""`
|
|
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. |
|
|
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. |
|
|
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. |
|
|
46
44
|
|
|
47
45
|
### Required vs. Default Distinction
|
|
48
46
|
|
|
@@ -64,6 +62,55 @@ When `dotenv.enabled` is `true`, the runner loads a `.env` file and merges its c
|
|
|
64
62
|
|
|
65
63
|
**Security note**: Do not commit `.env` files containing secrets. Add `.env` to `.gitignore` and use `dotenv.enabled: true` with `dotenv.path` pointing to a file that exists only in the local or CI environment.
|
|
66
64
|
|
|
65
|
+
## HTTP Port Pinning
|
|
66
|
+
|
|
67
|
+
`httpPort` is an optional integer field available only in HTTP-WASM configs (`appType: "http-wasm"`). It pins the `fastedge-run` subprocess to a fixed port instead of dynamically allocating one from the pool (8100–8199). The schema rejects `httpPort` on proxy-wasm configs.
|
|
68
|
+
|
|
69
|
+
### Default Behaviour
|
|
70
|
+
|
|
71
|
+
Without `httpPort`, the runner allocates a port from the 8100–8199 range for each test run. The allocated port is not stable across runs.
|
|
72
|
+
|
|
73
|
+
### When to Use Port Pinning
|
|
74
|
+
|
|
75
|
+
- **Codespaces / Docker port-forwarding**: Port-forward rules reference a specific port number. Without pinning, the port changes between runs and breaks the forwarding rule.
|
|
76
|
+
- **Stable live-preview URLs**: Browser tabs or external tooling pointing at a fixed URL (e.g. `http://localhost:8250`) require a stable port.
|
|
77
|
+
- **External tooling integration**: Monitoring, load testing, or proxy configurations that hard-code a target address.
|
|
78
|
+
|
|
79
|
+
### Fail-Fast Semantics
|
|
80
|
+
|
|
81
|
+
If the pinned port is already in use, `HttpWasmRunner.load()` throws immediately:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
fastedge-run port 8250 is not available — release it or choose a different httpPort in fastedge-config.test.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
There is no fallback to dynamic allocation. Free the port or choose a different one before running.
|
|
88
|
+
|
|
89
|
+
### Port Range Considerations
|
|
90
|
+
|
|
91
|
+
Pinning a port inside the 8100–8199 dynamic pool is allowed by the schema but risks collisions with other concurrent debug sessions using dynamic allocation. For production-like setups or shared environments, choose a port outside the pool (e.g. 8250 or any unused port above 8199).
|
|
92
|
+
|
|
93
|
+
### Example
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"$schema": "./node_modules/@gcoredev/fastedge-test/schemas/fastedge-config.test.schema.json",
|
|
98
|
+
"description": "HTTP-WASM handler with pinned port for Codespaces",
|
|
99
|
+
"appType": "http-wasm",
|
|
100
|
+
"wasm": {
|
|
101
|
+
"path": "./dist/http-handler.wasm"
|
|
102
|
+
},
|
|
103
|
+
"request": {
|
|
104
|
+
"method": "GET",
|
|
105
|
+
"path": "/health",
|
|
106
|
+
"headers": {},
|
|
107
|
+
"body": ""
|
|
108
|
+
},
|
|
109
|
+
"properties": {},
|
|
110
|
+
"httpPort": 8250
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
67
114
|
## Examples
|
|
68
115
|
|
|
69
116
|
### Minimal CDN Configuration
|
|
@@ -153,37 +200,32 @@ An HTTP-WASM scenario simulating a `POST` request with a JSON body. `appType` mu
|
|
|
153
200
|
}
|
|
154
201
|
```
|
|
155
202
|
|
|
156
|
-
###
|
|
203
|
+
### Built-In Responder (No Network)
|
|
157
204
|
|
|
158
|
-
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.
|
|
159
206
|
|
|
160
207
|
```json
|
|
161
208
|
{
|
|
162
209
|
"$schema": "./node_modules/@gcoredev/fastedge-test/schemas/fastedge-config.test.schema.json",
|
|
163
|
-
"description": "CDN handler
|
|
210
|
+
"description": "CDN handler against the built-in responder",
|
|
164
211
|
"appType": "proxy-wasm",
|
|
165
212
|
"wasm": {
|
|
166
213
|
"path": "./dist/handler.wasm"
|
|
167
214
|
},
|
|
168
215
|
"request": {
|
|
169
216
|
"method": "GET",
|
|
170
|
-
"url": "
|
|
217
|
+
"url": "built-in",
|
|
171
218
|
"headers": {},
|
|
172
219
|
"body": ""
|
|
173
220
|
},
|
|
174
|
-
"response": {
|
|
175
|
-
"headers": {
|
|
176
|
-
"content-type": "application/json",
|
|
177
|
-
"cache-control": "max-age=86400"
|
|
178
|
-
},
|
|
179
|
-
"body": "{\"status\": \"ok\", \"data\": []}"
|
|
180
|
-
},
|
|
181
221
|
"properties": {
|
|
182
222
|
"CACHE_TTL": 86400
|
|
183
223
|
}
|
|
184
224
|
}
|
|
185
225
|
```
|
|
186
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
|
+
|
|
187
229
|
## IDE Integration
|
|
188
230
|
|
|
189
231
|
Adding `$schema` to your config file enables JSON Schema validation and autocompletion in VSCode and any editor that supports the JSON Language Server.
|
|
@@ -240,10 +282,6 @@ type CdnConfig = {
|
|
|
240
282
|
headers: Record<string, string>; // default: {}
|
|
241
283
|
body: string; // default: ""
|
|
242
284
|
};
|
|
243
|
-
response?: {
|
|
244
|
-
headers: Record<string, string>; // default: {}
|
|
245
|
-
body: string; // default: ""
|
|
246
|
-
};
|
|
247
285
|
properties: Record<string, unknown>; // default: {}
|
|
248
286
|
dotenv?: {
|
|
249
287
|
enabled?: boolean;
|
|
@@ -265,6 +303,7 @@ type HttpConfig = {
|
|
|
265
303
|
headers: Record<string, string>; // default: {}
|
|
266
304
|
body: string; // default: ""
|
|
267
305
|
};
|
|
306
|
+
httpPort?: number; // pin subprocess port (1024–65535); throws if busy
|
|
268
307
|
properties: Record<string, unknown>; // default: {}
|
|
269
308
|
dotenv?: {
|
|
270
309
|
enabled?: boolean;
|
|
@@ -282,9 +321,9 @@ const config = await loadConfigFile("./fastedge-config.test.json");
|
|
|
282
321
|
|
|
283
322
|
if (config.appType === "proxy-wasm") {
|
|
284
323
|
console.log(config.request.url); // string — CDN full URL
|
|
285
|
-
console.log(config.response); // ResponseConfig | undefined
|
|
286
324
|
} else {
|
|
287
325
|
console.log(config.request.path); // string — HTTP-WASM path
|
|
326
|
+
console.log(config.httpPort); // number | undefined
|
|
288
327
|
}
|
|
289
328
|
```
|
|
290
329
|
|