@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.
Files changed (43) hide show
  1. package/dist/frontend/assets/{index-BpdzhbRl.js → index-CiqeJ9rz.js} +24 -24
  2. package/dist/frontend/index.html +1 -1
  3. package/dist/lib/index.cjs +164 -66
  4. package/dist/lib/index.d.ts +1 -0
  5. package/dist/lib/index.js +164 -66
  6. package/dist/lib/runner/HeaderManager.d.ts +6 -4
  7. package/dist/lib/runner/HostFunctions.d.ts +5 -5
  8. package/dist/lib/runner/HttpWasmRunner.d.ts +22 -5
  9. package/dist/lib/runner/IStateManager.d.ts +7 -7
  10. package/dist/lib/runner/IWasmRunner.d.ts +42 -10
  11. package/dist/lib/runner/PortManager.d.ts +4 -1
  12. package/dist/lib/runner/PropertyResolver.d.ts +3 -3
  13. package/dist/lib/runner/ProxyWasmRunner.d.ts +5 -2
  14. package/dist/lib/runner/standalone.d.ts +1 -1
  15. package/dist/lib/runner/types.d.ts +17 -8
  16. package/dist/lib/schemas/api.d.ts +2 -8
  17. package/dist/lib/schemas/config.d.ts +2 -13
  18. package/dist/lib/schemas/index.d.ts +2 -2
  19. package/dist/lib/test-framework/assertions.d.ts +18 -4
  20. package/dist/lib/test-framework/index.cjs +18634 -115
  21. package/dist/lib/test-framework/index.d.ts +2 -0
  22. package/dist/lib/test-framework/index.js +18651 -104
  23. package/dist/lib/test-framework/mock-origins.d.ts +56 -0
  24. package/dist/lib/test-framework/suite-runner.d.ts +16 -0
  25. package/dist/lib/test-framework/types.d.ts +1 -5
  26. package/dist/server.js +33 -33
  27. package/docs/API.md +48 -54
  28. package/docs/DEBUGGER.md +7 -8
  29. package/docs/INDEX.md +4 -1
  30. package/docs/RUNNER.md +126 -74
  31. package/docs/TEST_CONFIG.md +79 -40
  32. package/docs/TEST_FRAMEWORK.md +235 -36
  33. package/docs/WEBSOCKET.md +25 -21
  34. package/docs/quickstart.md +1 -13
  35. package/package.json +4 -1
  36. package/schemas/api-config.schema.json +5 -24
  37. package/schemas/api-load.schema.json +5 -0
  38. package/schemas/api-send.schema.json +0 -20
  39. package/schemas/fastedge-config.test.schema.json +5 -24
  40. package/schemas/full-flow-result.schema.json +17 -7
  41. package/schemas/hook-call.schema.json +16 -6
  42. package/schemas/hook-result.schema.json +16 -6
  43. package/schemas/http-response.schema.json +227 -5
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Defines the common contract for both ProxyWasmRunner and HttpWasmRunner
5
5
  */
6
+ import type { IncomingHttpHeaders } from "node:http";
6
7
  import type { IStateManager } from "./IStateManager.js";
7
8
  import type { HookCall, HookResult, FullFlowResult } from "./types.js";
8
9
  export type WasmType = "http-wasm" | "proxy-wasm";
@@ -18,6 +19,15 @@ export interface RunnerConfig {
18
19
  enforceProductionPropertyRules?: boolean;
19
20
  /** Override automatic WASM type detection. Use when detection produces wrong results. */
20
21
  runnerType?: WasmType;
22
+ /**
23
+ * HTTP-WASM only. Pin the spawned `fastedge-run` HTTP server to a specific
24
+ * port instead of allocating from the dynamic pool (8100-8199). Intended for
25
+ * Codespaces/Docker port-forwarding setups, stable live-preview URLs, or any
26
+ * external tooling that requires a fixed target. `load()` throws if the port
27
+ * is already in use — there is no fallback to dynamic allocation. Ignored
28
+ * for proxy-wasm runners.
29
+ */
30
+ httpPort?: number;
21
31
  }
22
32
  /**
23
33
  * HTTP Request type for HTTP WASM runner
@@ -29,12 +39,18 @@ export interface HttpRequest {
29
39
  body?: string;
30
40
  }
31
41
  /**
32
- * HTTP Response type for HTTP WASM runner
42
+ * HTTP Response type for HTTP WASM runner.
43
+ *
44
+ * `headers` uses Node's `IncomingHttpHeaders` shape — common single-valued
45
+ * headers (`content-type`, `location`, `etag`, …) are typed as `string`;
46
+ * `set-cookie` is `string[]`; unknown keys are `string | string[] | undefined`.
47
+ * This preserves RFC 6265 Set-Cookie semantics (each cookie kept separate)
48
+ * and matches consumer expectations for Node's fetch/http ecosystem.
33
49
  */
34
50
  export interface HttpResponse {
35
51
  status: number;
36
52
  statusText: string;
37
- headers: Record<string, string>;
53
+ headers: IncomingHttpHeaders;
38
54
  body: string;
39
55
  contentType: string | null;
40
56
  isBase64?: boolean;
@@ -54,7 +70,22 @@ export interface IWasmRunner {
54
70
  */
55
71
  load(bufferOrPath: Buffer | string, config?: RunnerConfig): Promise<void>;
56
72
  /**
57
- * Execute a request through the WASM module (HTTP WASM only)
73
+ * Execute a request through the WASM module (HTTP WASM only).
74
+ *
75
+ * Redirects are surfaced verbatim — the underlying fetch uses
76
+ * `redirect: "manual"` so tests can assert on 3xx status and the `Location`
77
+ * header. This matches how a FastEdge edge deployment returns redirects to
78
+ * the client rather than following them server-side.
79
+ *
80
+ * `execute` only hits the WASM app under test — `request.path` is a path on
81
+ * the spawned `fastedge-run` server, not a full URL. To follow a redirect
82
+ * the caller must inspect `response.headers.location`:
83
+ * - Relative Location (`/foo`) — reuse as `request.path` directly.
84
+ * - Absolute same-host Location — extract `pathname + search` via `new URL()`
85
+ * and re-issue with that path.
86
+ * - Absolute cross-host Location — cannot be followed through the runner;
87
+ * the 302 is the terminal state for the test.
88
+ *
58
89
  * @param request The HTTP request to execute
59
90
  * @returns The HTTP response
60
91
  */
@@ -66,20 +97,21 @@ export interface IWasmRunner {
66
97
  */
67
98
  callHook(hookCall: HookCall): Promise<HookResult>;
68
99
  /**
69
- * Execute full request/response flow (Proxy-WASM only)
70
- * @param url Request URL
100
+ * Execute full request/response flow (Proxy-WASM only).
101
+ *
102
+ * The upstream response is generated at runtime — either by a real HTTP
103
+ * fetch against `url`, or by the built-in responder when
104
+ * `url === "built-in"`. There is no fixture-level mock response.
105
+ *
106
+ * @param url Request URL, or `"built-in"` to use the built-in responder
71
107
  * @param method HTTP method
72
108
  * @param headers Request headers
73
109
  * @param body Request body
74
- * @param responseHeaders Response headers
75
- * @param responseBody Response body
76
- * @param responseStatus Response status code
77
- * @param responseStatusText Response status text
78
110
  * @param properties Shared properties
79
111
  * @param enforceProductionPropertyRules Whether to enforce property access rules
80
112
  * @returns Full flow execution result
81
113
  */
82
- callFullFlow(url: string, method: string, headers: Record<string, string>, body: string, responseHeaders: Record<string, string>, responseBody: string, responseStatus: number, responseStatusText: string, properties: Record<string, unknown>, enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
114
+ callFullFlow(url: string, method: string, headers: Record<string, string>, body: string, properties: Record<string, unknown>, enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
83
115
  /**
84
116
  * Apply dotenv settings to the current runner without reloading the WASM.
85
117
  * For ProxyWasmRunner: resets stores and re-loads dotenv files in-place.
@@ -18,8 +18,11 @@ export declare class PortManager {
18
18
  * This is necessary when multiple server processes run simultaneously —
19
19
  * each has its own PortManager with independent in-memory state, so
20
20
  * in-memory tracking alone is not enough to prevent cross-process conflicts.
21
+ *
22
+ * Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
23
+ * can reuse the same OS-level check without going through allocate().
21
24
  */
22
- private isPortFree;
25
+ isPortFree(port: number): Promise<boolean>;
23
26
  /**
24
27
  * Allocate an available port from the pool.
25
28
  * Combines in-memory tracking (avoids TCP TIME_WAIT reuse within this process)
@@ -1,4 +1,4 @@
1
- import type { HeaderMap } from "./types";
1
+ import type { HeaderMap, HeaderRecord } from "./types";
2
2
  export declare class PropertyResolver {
3
3
  private properties;
4
4
  private requestHeaders;
@@ -28,13 +28,13 @@ export declare class PropertyResolver {
28
28
  * User properties take precedence over calculated ones
29
29
  */
30
30
  getAllProperties(): Record<string, unknown>;
31
- setRequestMetadata(headers: HeaderMap, method: string, path?: string, scheme?: string): void;
31
+ setRequestMetadata(headers: HeaderMap | HeaderRecord, method: string, path?: string, scheme?: string): void;
32
32
  /**
33
33
  * Extract runtime properties from target URL
34
34
  * This parses the URL to populate request.url, request.host, request.path, etc.
35
35
  */
36
36
  extractRuntimePropertiesFromUrl(targetUrl: string): void;
37
- setResponseMetadata(headers: HeaderMap, status: number, statusText: string): void;
37
+ setResponseMetadata(headers: HeaderMap | HeaderRecord, status: number, statusText: string): void;
38
38
  resolve(path: string): unknown;
39
39
  private resolveStandard;
40
40
  private resolvePathSegments;
@@ -59,9 +59,12 @@ export declare class ProxyWasmRunner implements IWasmRunner {
59
59
  private getHookContext;
60
60
  private logDebug;
61
61
  /**
62
- * Interface-compliant callFullFlow method
62
+ * Interface-compliant callFullFlow method.
63
+ *
64
+ * The upstream response is generated at runtime by a real HTTP fetch
65
+ * against `url` or by the built-in responder when `url === "built-in"`.
63
66
  */
64
- callFullFlow(url: string, method: string, headers: Record<string, string>, body: string, responseHeaders: Record<string, string>, responseBody: string, responseStatus: number, responseStatusText: string, properties: Record<string, unknown>, enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
67
+ callFullFlow(url: string, method: string, headers: Record<string, string>, body: string, properties: Record<string, unknown>, enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
65
68
  /**
66
69
  * Not supported for Proxy-WASM (HTTP WASM only)
67
70
  */
@@ -7,7 +7,7 @@
7
7
  * Usage:
8
8
  * import { createRunner } from './server/runner/standalone.js';
9
9
  * const runner = await createRunner('./path/to/wasm.wasm');
10
- * const result = await runner.callFullFlow('https://example.com', 'GET', {}, '', {}, '', 200, 'OK', {}, true);
10
+ * const result = await runner.callFullFlow('https://example.com', 'GET', {}, '', {}, true);
11
11
  */
12
12
  import type { IWasmRunner, RunnerConfig } from "./IWasmRunner.js";
13
13
  /**
@@ -1,16 +1,25 @@
1
1
  export type HeaderMap = Record<string, string>;
2
2
  export type HeaderTuples = [string, string][];
3
+ export type HeaderRecord = Record<string, string | string[]>;
3
4
  export type HookCall = {
4
5
  hook: string;
5
6
  request: {
6
- headers: HeaderMap;
7
+ headers: HeaderRecord;
7
8
  body: string;
8
9
  method?: string;
9
10
  path?: string;
10
11
  scheme?: string;
11
12
  };
12
- response: {
13
- headers: HeaderMap;
13
+ /**
14
+ * Seed state for the response hooks (`onResponseHeaders` / `onResponseBody`)
15
+ * when calling them in isolation via `callHook()`. The full-flow path
16
+ * (`callFullFlow`) generates the upstream response at runtime and does not
17
+ * consume this field — request hooks ignore it, and response hooks are
18
+ * called with the response built from the live origin fetch or built-in
19
+ * responder output.
20
+ */
21
+ response?: {
22
+ headers: HeaderRecord;
14
23
  body: string;
15
24
  status?: number;
16
25
  statusText?: string;
@@ -27,22 +36,22 @@ export type HookResult = {
27
36
  }[];
28
37
  input: {
29
38
  request: {
30
- headers: HeaderMap;
39
+ headers: HeaderRecord;
31
40
  body: string;
32
41
  };
33
42
  response: {
34
- headers: HeaderMap;
43
+ headers: HeaderRecord;
35
44
  body: string;
36
45
  };
37
46
  properties?: Record<string, unknown>;
38
47
  };
39
48
  output: {
40
49
  request: {
41
- headers: HeaderMap;
50
+ headers: HeaderRecord;
42
51
  body: string;
43
52
  };
44
53
  response: {
45
- headers: HeaderMap;
54
+ headers: HeaderRecord;
46
55
  body: string;
47
56
  };
48
57
  properties?: Record<string, unknown>;
@@ -78,7 +87,7 @@ export type FullFlowResult = {
78
87
  finalResponse: {
79
88
  status: number;
80
89
  statusText: string;
81
- headers: HeaderMap;
90
+ headers: HeaderRecord;
82
91
  body: string;
83
92
  contentType: string;
84
93
  isBase64?: boolean;
@@ -6,6 +6,7 @@ export declare const ApiLoadBodySchema: z.ZodObject<{
6
6
  enabled: z.ZodOptional<z.ZodBoolean>;
7
7
  path: z.ZodOptional<z.ZodString>;
8
8
  }, z.core.$strip>>;
9
+ httpPort: z.ZodOptional<z.ZodNumber>;
9
10
  }, z.core.$strip>;
10
11
  export declare const ApiSendBodySchema: z.ZodObject<{
11
12
  url: z.ZodUnion<readonly [z.ZodLiteral<"built-in">, z.ZodString]>;
@@ -15,10 +16,6 @@ export declare const ApiSendBodySchema: z.ZodObject<{
15
16
  headers: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>>;
16
17
  body: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodString>>>;
17
18
  }, z.core.$strip>>;
18
- response: z.ZodOptional<z.ZodObject<{
19
- headers: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>>;
20
- body: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodString>>>;
21
- }, z.core.$strip>>;
22
19
  properties: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
23
20
  }, z.core.$strip>;
24
21
  export declare const ApiCallBodySchema: z.ZodObject<{
@@ -52,6 +49,7 @@ export declare const ApiConfigBodySchema: z.ZodObject<{
52
49
  path: z.ZodOptional<z.ZodString>;
53
50
  }, z.core.$strip>>;
54
51
  appType: z.ZodLiteral<"http-wasm">;
52
+ httpPort: z.ZodOptional<z.ZodNumber>;
55
53
  request: z.ZodObject<{
56
54
  method: z.ZodDefault<z.ZodString>;
57
55
  path: z.ZodString;
@@ -77,10 +75,6 @@ export declare const ApiConfigBodySchema: z.ZodObject<{
77
75
  headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
78
76
  body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
79
77
  }, z.core.$strip>;
80
- response: z.ZodOptional<z.ZodObject<{
81
- headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
82
- body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
83
- }, z.core.$strip>>;
84
78
  }, z.core.$strip>]>;
85
79
  }, z.core.$strip>;
86
80
  export type ApiLoadBody = z.infer<typeof ApiLoadBodySchema>;
@@ -15,10 +15,6 @@ export declare const HttpRequestConfigSchema: z.ZodObject<{
15
15
  headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
16
16
  body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
17
17
  }, z.core.$strip>;
18
- export declare const ResponseConfigSchema: z.ZodObject<{
19
- headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
20
- body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
21
- }, z.core.$strip>;
22
18
  declare const CdnConfigSchema: z.ZodObject<{
23
19
  $schema: z.ZodOptional<z.ZodString>;
24
20
  description: z.ZodOptional<z.ZodString>;
@@ -38,10 +34,6 @@ declare const CdnConfigSchema: z.ZodObject<{
38
34
  headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
39
35
  body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
40
36
  }, z.core.$strip>;
41
- response: z.ZodOptional<z.ZodObject<{
42
- headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
43
- body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
44
- }, z.core.$strip>>;
45
37
  }, z.core.$strip>;
46
38
  declare const HttpConfigSchema: z.ZodObject<{
47
39
  $schema: z.ZodOptional<z.ZodString>;
@@ -56,6 +48,7 @@ declare const HttpConfigSchema: z.ZodObject<{
56
48
  path: z.ZodOptional<z.ZodString>;
57
49
  }, z.core.$strip>>;
58
50
  appType: z.ZodLiteral<"http-wasm">;
51
+ httpPort: z.ZodOptional<z.ZodNumber>;
59
52
  request: z.ZodObject<{
60
53
  method: z.ZodDefault<z.ZodString>;
61
54
  path: z.ZodString;
@@ -76,6 +69,7 @@ export declare const TestConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
76
69
  path: z.ZodOptional<z.ZodString>;
77
70
  }, z.core.$strip>>;
78
71
  appType: z.ZodLiteral<"http-wasm">;
72
+ httpPort: z.ZodOptional<z.ZodNumber>;
79
73
  request: z.ZodObject<{
80
74
  method: z.ZodDefault<z.ZodString>;
81
75
  path: z.ZodString;
@@ -101,10 +95,6 @@ export declare const TestConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
101
95
  headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
102
96
  body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
103
97
  }, z.core.$strip>;
104
- response: z.ZodOptional<z.ZodObject<{
105
- headers: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
106
- body: z.ZodDefault<z.ZodOptional<z.ZodString>>;
107
- }, z.core.$strip>>;
108
98
  }, z.core.$strip>]>;
109
99
  export declare const RequestConfigSchema: z.ZodObject<{
110
100
  method: z.ZodDefault<z.ZodString>;
@@ -116,7 +106,6 @@ export type WasmConfig = z.infer<typeof WasmConfigSchema>;
116
106
  export type CdnRequestConfig = z.infer<typeof CdnRequestConfigSchema>;
117
107
  export type HttpRequestConfig = z.infer<typeof HttpRequestConfigSchema>;
118
108
  export type RequestConfig = z.infer<typeof CdnRequestConfigSchema>;
119
- export type ResponseConfig = z.infer<typeof ResponseConfigSchema>;
120
109
  export type CdnConfig = z.infer<typeof CdnConfigSchema>;
121
110
  export type HttpConfig = z.infer<typeof HttpConfigSchema>;
122
111
  export type TestConfig = z.infer<typeof TestConfigSchema>;
@@ -1,4 +1,4 @@
1
- export { WasmConfigSchema, RequestConfigSchema, ResponseConfigSchema, TestConfigSchema, } from './config';
2
- export type { WasmConfig, RequestConfig, ResponseConfig, TestConfig, } from './config';
1
+ export { WasmConfigSchema, RequestConfigSchema, TestConfigSchema, } from './config';
2
+ export type { WasmConfig, RequestConfig, TestConfig, } from './config';
3
3
  export { ApiLoadBodySchema, ApiSendBodySchema, ApiCallBodySchema, ApiConfigBodySchema, } from './api';
4
4
  export type { ApiLoadBody, ApiSendBody, ApiCallBody, ApiConfigBody, } from './api';
@@ -9,8 +9,12 @@ import type { HttpResponse } from "../runner/IWasmRunner.js";
9
9
  /**
10
10
  * Assert that a named header exists (and optionally matches a value)
11
11
  * in the hook's output request headers.
12
+ *
13
+ * When `expected` is a string and the header is multi-valued, passes if
14
+ * any value matches (`.includes()` semantics). When `expected` is a string[],
15
+ * requires an exact array match.
12
16
  */
13
- export declare function assertRequestHeader(result: HookResult, name: string, expected?: string): void;
17
+ export declare function assertRequestHeader(result: HookResult, name: string, expected?: string | string[]): void;
14
18
  /**
15
19
  * Assert that a named header is absent in the hook's output request headers.
16
20
  */
@@ -18,8 +22,12 @@ export declare function assertNoRequestHeader(result: HookResult, name: string):
18
22
  /**
19
23
  * Assert that a named header exists (and optionally matches a value)
20
24
  * in the hook's output response headers.
25
+ *
26
+ * When `expected` is a string and the header is multi-valued (e.g. set-cookie),
27
+ * passes if any value matches (`.includes()` semantics). When `expected` is a
28
+ * string[], requires an exact array match.
21
29
  */
22
- export declare function assertResponseHeader(result: HookResult, name: string, expected?: string): void;
30
+ export declare function assertResponseHeader(result: HookResult, name: string, expected?: string | string[]): void;
23
31
  /**
24
32
  * Assert that a named header is absent in the hook's output response headers.
25
33
  */
@@ -31,8 +39,10 @@ export declare function assertFinalStatus(result: FullFlowResult, expected: numb
31
39
  /**
32
40
  * Assert that a named header exists (and optionally matches a value)
33
41
  * in the final response headers from a full-flow run.
42
+ *
43
+ * Multi-value semantics match {@link assertResponseHeader}.
34
44
  */
35
- export declare function assertFinalHeader(result: FullFlowResult, name: string, expected?: string): void;
45
+ export declare function assertFinalHeader(result: FullFlowResult, name: string, expected?: string | string[]): void;
36
46
  /**
37
47
  * Assert the hook return code (e.g. 0 = Ok, 1 = Pause).
38
48
  */
@@ -68,8 +78,12 @@ export declare function assertHttpStatus(response: HttpResponse, expected: numbe
68
78
  /**
69
79
  * Assert that a named header exists (and optionally matches a value)
70
80
  * in the HTTP response.
81
+ *
82
+ * Multi-value semantics: when `expected` is a string and the header is
83
+ * multi-valued (e.g. set-cookie is `string[]` per RFC 6265), passes if any
84
+ * value matches. When `expected` is a string[], requires exact array match.
71
85
  */
72
- export declare function assertHttpHeader(response: HttpResponse, name: string, expected?: string): void;
86
+ export declare function assertHttpHeader(response: HttpResponse, name: string, expected?: string | string[]): void;
73
87
  /**
74
88
  * Assert that a named header is absent in the HTTP response.
75
89
  */