@gcoredev/fastedge-test 0.0.1-beta.0 → 0.1.0-beta.2

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/README.md +6 -6
  2. package/bin/fastedge-debug.js +2 -0
  3. package/dist/fastedge-cli/METADATA.json +1 -3
  4. package/dist/fastedge-cli/{fastedge-run-linux-x64-unkown → fastedge-run-darwin-arm64} +0 -0
  5. package/dist/fastedge-cli/fastedge-run-linux-x64 +0 -0
  6. package/dist/fastedge-cli/fastedge-run.exe +0 -0
  7. package/dist/frontend/assets/index-CEFjsU8e.js +35 -0
  8. package/dist/frontend/assets/index-DdlINQc_.css +1 -0
  9. package/dist/frontend/index.html +2 -2
  10. package/dist/lib/index.cjs +329 -112
  11. package/dist/lib/index.js +331 -115
  12. package/dist/lib/runner/HostFunctions.d.ts +8 -0
  13. package/dist/lib/runner/HttpWasmRunner.d.ts +34 -14
  14. package/dist/lib/runner/IStateManager.d.ts +3 -2
  15. package/dist/lib/runner/IWasmRunner.d.ts +18 -1
  16. package/dist/lib/runner/NullStateManager.d.ts +1 -0
  17. package/dist/lib/runner/PortManager.d.ts +17 -19
  18. package/dist/lib/runner/ProxyWasmRunner.d.ts +7 -0
  19. package/dist/lib/runner/standalone.d.ts +1 -1
  20. package/dist/lib/schemas/api.d.ts +8 -2
  21. package/dist/lib/schemas/config.d.ts +4 -1
  22. package/dist/lib/test-framework/index.cjs +330 -114
  23. package/dist/lib/test-framework/index.js +332 -117
  24. package/dist/lib/test-framework/suite-runner.d.ts +1 -1
  25. package/dist/server.js +30 -30
  26. package/docs/API.md +758 -360
  27. package/docs/DEBUGGER.md +151 -0
  28. package/docs/INDEX.md +111 -0
  29. package/docs/RUNNER.md +582 -0
  30. package/docs/TEST_CONFIG.md +242 -0
  31. package/docs/TEST_FRAMEWORK.md +384 -284
  32. package/docs/WEBSOCKET.md +499 -0
  33. package/docs/quickstart.md +171 -0
  34. package/llms.txt +72 -14
  35. package/package.json +17 -6
  36. package/schemas/api-config.schema.json +12 -5
  37. package/schemas/api-load.schema.json +11 -6
  38. package/schemas/{test-config.schema.json → fastedge-config.test.schema.json} +12 -5
  39. package/dist/fastedge-cli/.gitkeep +0 -0
  40. package/dist/frontend/assets/index-CnXStFTd.css +0 -1
  41. package/dist/frontend/assets/index-FR9Oqsow.js +0 -37
  42. package/docs/HYBRID_LOADING.md +0 -546
  43. package/docs/LOCAL_SERVER.md +0 -153
@@ -1,420 +1,520 @@
1
- # FastEdge Test Framework
1
+ # Test Framework API
2
2
 
3
- `@gcoredev/fastedge-test` provides a programmatic API for testing FastEdge WASM binaries no server required. Use it in CI pipelines, agent scripts, or alongside your existing test runner (vitest, jest, etc.).
3
+ High-level API for defining and running proxy-wasm test suites with `@gcoredev/fastedge-test`.
4
4
 
5
- ## Installation
5
+ ## Import
6
6
 
7
- ```bash
8
- npm install @gcoredev/fastedge-test
9
- # or
10
- pnpm add @gcoredev/fastedge-test
7
+ ```typescript
8
+ import {
9
+ defineTestSuite,
10
+ runTestSuite,
11
+ runAndExit,
12
+ runFlow,
13
+ loadConfigFile,
14
+ assertRequestHeader,
15
+ assertResponseHeader,
16
+ assertFinalStatus,
17
+ assertReturnCode,
18
+ assertLog,
19
+ } from "@gcoredev/fastedge-test/test";
20
+ import type { TestSuite, TestCase, SuiteResult, FlowOptions } from "@gcoredev/fastedge-test/test";
11
21
  ```
12
22
 
13
- ## Two Entry Points
23
+ ## Types
24
+
25
+ ### TestSuite
26
+
27
+ A discriminated union requiring exactly one of `wasmPath` or `wasmBuffer`. Supplying both, or neither, is a TypeScript compile-time error.
14
28
 
15
- | Import | Purpose |
16
- |--------|---------|
17
- | `@gcoredev/fastedge-test` | Low-level runner — load and execute WASM directly |
18
- | `@gcoredev/fastedge-test/test` | High-level test framework — define and run structured test suites |
29
+ ```typescript
30
+ type TestSuiteBase = {
31
+ runnerConfig?: RunnerConfig;
32
+ tests: TestCase[];
33
+ };
19
34
 
20
- ---
35
+ type TestSuite =
36
+ | (TestSuiteBase & { wasmPath: string; wasmBuffer?: never })
37
+ | (TestSuiteBase & { wasmBuffer: Buffer; wasmPath?: never });
38
+ ```
21
39
 
22
- ## High-Level: Test Suites
40
+ | Field | Type | Required | Description |
41
+ | -------------- | -------------- | -------- | ----------------------------------------------------------- |
42
+ | `wasmPath` | `string` | One of | Filesystem path to the `.wasm` file |
43
+ | `wasmBuffer` | `Buffer` | One of | Pre-loaded WASM binary |
44
+ | `tests` | `TestCase[]` | Yes | Test cases to execute; must be non-empty |
45
+ | `runnerConfig` | `RunnerConfig` | No | Optional runner configuration (see [RUNNER.md](RUNNER.md)) |
23
46
 
24
- The `./test` entry point is the recommended starting point. It manages the runner lifecycle for you — each test case gets a fresh, isolated runner instance.
47
+ ### TestCase
25
48
 
26
- ### CDN (Proxy-WASM) Example
49
+ A single test case. The `run` function receives a fully loaded `IWasmRunner` instance. Throw an error (or use assertion helpers) to fail the test.
27
50
 
28
51
  ```typescript
29
- import { defineTestSuite, runTestSuite, runFlow } from '@gcoredev/fastedge-test/test';
30
- import {
31
- assertRequestHeader,
32
- assertFinalStatus,
33
- assertLog,
34
- } from '@gcoredev/fastedge-test/test';
52
+ interface TestCase {
53
+ name: string;
54
+ run: (runner: IWasmRunner) => Promise<void>;
55
+ }
56
+ ```
35
57
 
36
- const suite = defineTestSuite({
37
- wasmPath: './build/my-cdn-app.wasm',
38
- tests: [
39
- {
40
- name: 'injects x-custom header on request',
41
- run: async (runner) => {
42
- const result = await runFlow(runner, {
43
- url: 'https://example.com/page',
44
- method: 'GET',
45
- requestHeaders: { 'user-agent': 'Mozilla/5.0' },
46
- responseBody: '<html>content</html>',
47
- responseStatus: 200,
48
- });
58
+ ### TestResult & SuiteResult
49
59
 
50
- assertRequestHeader(
51
- result.hookResults.onRequestHeaders,
52
- 'x-custom',
53
- 'expected-value',
54
- );
55
- },
56
- },
57
- {
58
- name: 'returns 403 for blocked paths',
59
- run: async (runner) => {
60
- const result = await runFlow(runner, {
61
- url: 'https://example.com/admin',
62
- method: 'GET',
63
- });
60
+ ```typescript
61
+ interface TestResult {
62
+ name: string;
63
+ passed: boolean;
64
+ error?: string; // Present when passed is false
65
+ durationMs: number;
66
+ }
64
67
 
65
- assertFinalStatus(result, 403);
66
- },
67
- },
68
- ],
69
- });
68
+ interface SuiteResult {
69
+ passed: number;
70
+ failed: number;
71
+ total: number;
72
+ durationMs: number;
73
+ results: TestResult[];
74
+ }
75
+ ```
76
+
77
+ ### FlowOptions
70
78
 
71
- const results = await runTestSuite(suite);
72
- console.log(`${results.passed}/${results.total} passed`);
79
+ Object-based options for `runFlow()`. HTTP/2 pseudo-headers (`:method`, `:path`, `:authority`, `:scheme`) are derived from `url` and `method` automatically. Any pseudo-header supplied in `requestHeaders` overrides the derived default.
80
+
81
+ ```typescript
82
+ interface FlowOptions {
83
+ url: string;
84
+ method?: string; // Default: "GET"
85
+ requestHeaders?: Record<string, string>;
86
+ requestBody?: string; // Default: ""
87
+ responseStatus?: number; // Default: 200
88
+ responseStatusText?: string; // Default: "OK"
89
+ responseHeaders?: Record<string, string>; // Default: {}
90
+ responseBody?: string; // Default: ""
91
+ properties?: Record<string, unknown>; // Default: {}
92
+ enforceProductionPropertyRules?: boolean; // Default: true
93
+ }
73
94
  ```
74
95
 
75
- ### HTTP-WASM Example
96
+ | Field | Default | Description |
97
+ | -------------------------------- | ------- | --------------------------------------------------------------------------- |
98
+ | `url` | — | Full URL including scheme and host; used to derive pseudo-headers |
99
+ | `method` | `"GET"` | HTTP method |
100
+ | `requestHeaders` | `{}` | Additional request headers; pseudo-headers here override derived values |
101
+ | `requestBody` | `""` | Request body string |
102
+ | `responseStatus` | `200` | Simulated upstream response status code |
103
+ | `responseStatusText` | `"OK"` | Simulated upstream response status text |
104
+ | `responseHeaders` | `{}` | Simulated upstream response headers |
105
+ | `responseBody` | `""` | Simulated upstream response body |
106
+ | `properties` | `{}` | Proxy-wasm properties to inject |
107
+ | `enforceProductionPropertyRules` | `true` | When true, denies access to properties not available in production FastEdge |
76
108
 
77
- HTTP-WASM binaries (component model) use `runner.execute()` instead of `runFlow()`:
109
+ ### RunnerConfig
110
+
111
+ Re-exported from the runner. Controls WASM execution behaviour. See [RUNNER.md](RUNNER.md) for the full definition.
78
112
 
79
113
  ```typescript
80
- import { defineTestSuite, runTestSuite } from '@gcoredev/fastedge-test/test';
114
+ import type { RunnerConfig } from "@gcoredev/fastedge-test/test";
115
+ ```
116
+
117
+ ## Functions
118
+
119
+ ### defineTestSuite
120
+
121
+ ```typescript
122
+ function defineTestSuite(suite: TestSuite): TestSuite
123
+ ```
81
124
 
125
+ Validates and returns a typed `TestSuite` definition. Throws if neither `wasmPath` nor `wasmBuffer` is provided, or if `tests` is empty.
126
+
127
+ ```typescript
82
128
  const suite = defineTestSuite({
83
- wasmPath: './build/my-http-app.wasm',
129
+ wasmPath: "./build/my-filter.wasm",
84
130
  tests: [
85
131
  {
86
- name: 'responds with 200 OK',
87
- run: async (runner) => {
88
- const response = await runner.execute({
89
- path: '/api/hello',
90
- method: 'GET',
91
- headers: { 'content-type': 'application/json' },
132
+ name: "adds x-request-id header",
133
+ async run(runner) {
134
+ const result = await runFlow(runner, {
135
+ url: "https://example.com/api",
92
136
  });
93
-
94
- if (response.status !== 200) {
95
- throw new Error(`Expected 200, got ${response.status}`);
96
- }
137
+ assertRequestHeader(result.hookResults.onRequestHeaders, "x-request-id");
97
138
  },
98
139
  },
99
140
  ],
100
141
  });
101
-
102
- await runTestSuite(suite);
103
142
  ```
104
143
 
105
- The WASM type (CDN vs HTTP) is detected automatically from the binary.
106
-
107
- ---
108
-
109
- ## `defineTestSuite(config)`
110
-
111
- Validates and returns a typed suite definition. Throws if the config is invalid.
144
+ ### runTestSuite
112
145
 
113
146
  ```typescript
114
- type TestSuite =
115
- | { wasmPath: string; runnerConfig?: RunnerConfig; tests: TestCase[] }
116
- | { wasmBuffer: Buffer; runnerConfig?: RunnerConfig; tests: TestCase[] }
147
+ function runTestSuite(suite: TestSuite): Promise<SuiteResult>
117
148
  ```
118
149
 
119
- Exactly one of `wasmPath` or `wasmBuffer` must be providedTypeScript enforces this at compile time.
120
-
121
- ### `RunnerConfig`
150
+ Executes all test cases in the suite sequentially. Each test receives a **fresh runner instance** tests are fully isolated. A thrown error or failed assertion marks that test as failed; remaining tests still execute.
122
151
 
123
152
  ```typescript
124
- interface RunnerConfig {
125
- dotenvEnabled?: boolean; // Load .env file (default: false)
126
- enforceProductionPropertyRules?: boolean; // CDN property access control (default: true)
153
+ const suite = defineTestSuite({ wasmPath: "./filter.wasm", tests: [...] });
154
+ const result = await runTestSuite(suite);
155
+
156
+ console.log(`${result.passed}/${result.total} passed`);
157
+ for (const r of result.results) {
158
+ if (!r.passed) console.error(`FAIL: ${r.name} — ${r.error}`);
127
159
  }
128
160
  ```
129
161
 
130
- ---
131
-
132
- ## `runTestSuite(suite)`
133
-
134
- Executes all test cases and returns a `SuiteResult`. Tests run sequentially; each gets its own runner instance.
162
+ ### runAndExit
135
163
 
136
164
  ```typescript
137
- interface SuiteResult {
138
- passed: number;
139
- failed: number;
140
- total: number;
141
- durationMs: number;
142
- results: TestResult[];
143
- }
144
-
145
- interface TestResult {
146
- name: string;
147
- passed: boolean;
148
- error?: string; // Set when passed is false
149
- durationMs: number;
150
- }
165
+ function runAndExit(suite: TestSuite): Promise<never>
151
166
  ```
152
167
 
153
- ---
154
-
155
- ## `runAndExit(suite)` — CI / Standalone Scripts
156
-
157
- Runs the suite, prints a summary to stdout, and exits the process. Exit code 0 = all passed, 1 = any failures.
168
+ Runs the suite, prints a summary to stdout, then calls `process.exit(0)` if all tests pass or `process.exit(1)` if any fail. Intended for standalone Node.js test scripts run in CI.
158
169
 
159
170
  ```typescript
160
- import { defineTestSuite, runAndExit } from '@gcoredev/fastedge-test/test';
161
-
162
- const suite = defineTestSuite({ ... });
171
+ // test.mjs
172
+ import { defineTestSuite, runAndExit } from "@gcoredev/fastedge-test/test";
163
173
 
174
+ const suite = defineTestSuite({ wasmPath: "./filter.wasm", tests: [...] });
164
175
  await runAndExit(suite);
165
- // Output:
166
- // ✓ injects x-custom header (12ms)
167
- // ✗ returns 403 for blocked paths (5ms)
168
- // Expected final response status 403, got 200
169
- //
170
- // 1/2 passed in 17ms
171
176
  ```
172
177
 
173
- Designed for use in Makefile targets, shell scripts, and CI pipelines where a non-zero exit code signals failure.
178
+ Output format:
174
179
 
175
- ---
180
+ ```
181
+ ✓ adds x-request-id header (12ms)
182
+ ✗ blocks requests without auth (5ms)
183
+ Expected request header 'authorization' to be absent, but found 'Bearer token'
176
184
 
177
- ## `runFlow(runner, options)` — CDN Helper
185
+ 1/2 passed in 17ms
186
+ ```
178
187
 
179
- A convenience wrapper around the low-level `runner.callFullFlow()` that accepts named options and automatically derives HTTP/2 pseudo-headers (`:method`, `:path`, `:authority`, `:scheme`) from the URL.
188
+ ### runFlow
180
189
 
181
190
  ```typescript
182
- interface FlowOptions {
183
- url: string;
184
- method?: string; // Default: 'GET'
185
- requestHeaders?: Record<string, string>;
186
- requestBody?: string;
187
- responseStatus?: number; // Default: 200
188
- responseStatusText?: string; // Default: 'OK'
189
- responseHeaders?: Record<string, string>;
190
- responseBody?: string;
191
- properties?: Record<string, unknown>;
192
- enforceProductionPropertyRules?: boolean; // Default: true
193
- }
191
+ function runFlow(runner: IWasmRunner, options: FlowOptions): Promise<FullFlowResult>
194
192
  ```
195
193
 
196
- Returns a `FullFlowResult`:
194
+ Executes a complete request/response flow through the WASM filter. Object-based wrapper around the runner's low-level `callFullFlow` method — callers do not need to construct pseudo-headers manually.
195
+
196
+ The returned `FullFlowResult` has this shape:
197
197
 
198
198
  ```typescript
199
199
  interface FullFlowResult {
200
- hookResults: Record<string, HookResult>; // Keys: 'onRequestHeaders', 'onRequestBody', etc.
200
+ hookResults: Record<string, HookResult>; // keyed by camelCase hook name
201
201
  finalResponse: {
202
202
  status: number;
203
- statusText: string;
204
203
  headers: Record<string, string>;
205
204
  body: string;
206
- contentType: string;
207
- isBase64?: boolean;
208
205
  };
209
- calculatedProperties?: Record<string, unknown>;
210
206
  }
211
207
  ```
212
208
 
213
- Each `HookResult` contains:
209
+ Hook results are accessed by camelCase key:
214
210
 
215
- ```typescript
216
- interface HookResult {
217
- returnCode: number | null;
218
- logs: { level: number; message: string }[];
219
- input: {
220
- request: { headers: Record<string, string>; body: string };
221
- response: { headers: Record<string, string>; body: string };
222
- };
223
- output: {
224
- request: { headers: Record<string, string>; body: string };
225
- response: { headers: Record<string, string>; body: string };
226
- };
227
- properties: Record<string, unknown>;
228
- }
229
- ```
211
+ | Key | Hook |
212
+ | --------------------- | ---------------------------- |
213
+ | `onRequestHeaders` | `on_request_headers` hook |
214
+ | `onRequestBody` | `on_request_body` hook |
215
+ | `onResponseHeaders` | `on_response_headers` hook |
216
+ | `onResponseBody` | `on_response_body` hook |
230
217
 
231
- ---
218
+ ```typescript
219
+ const result = await runFlow(runner, {
220
+ url: "https://api.example.com/v1/resource",
221
+ method: "POST",
222
+ requestHeaders: { "content-type": "application/json" },
223
+ requestBody: '{"key":"value"}',
224
+ responseStatus: 201,
225
+ responseHeaders: { "x-upstream": "backend-1" },
226
+ });
232
227
 
233
- ## Assertion Helpers
228
+ // Access hook results
229
+ const reqHook = result.hookResults.onRequestHeaders;
230
+ const resHook = result.hookResults.onResponseHeaders;
234
231
 
235
- All helpers throw a descriptive `Error` on failure — compatible with any test framework or plain try/catch.
232
+ // Access final response
233
+ console.log(result.finalResponse.status); // 201
234
+ ```
236
235
 
237
- ### Request Headers (CDN)
236
+ ### loadConfigFile
238
237
 
239
238
  ```typescript
240
- // Assert a header is present (and optionally matches a value)
241
- assertRequestHeader(hookResult, 'x-custom')
242
- assertRequestHeader(hookResult, 'x-custom', 'expected-value')
243
-
244
- // Assert a header is absent
245
- assertNoRequestHeader(hookResult, 'x-should-not-exist')
239
+ function loadConfigFile(configPath: string): Promise<TestConfig>
246
240
  ```
247
241
 
248
- ### Response Headers (CDN)
242
+ Reads and validates a `fastedge-config.test.json` file. Returns the parsed `TestConfig` or throws with a descriptive error. See [TEST_CONFIG.md](TEST_CONFIG.md) for the full config schema.
249
243
 
250
244
  ```typescript
251
- assertResponseHeader(hookResult, 'cache-control', 'no-store')
252
- assertNoResponseHeader(hookResult, 'x-sensitive')
245
+ const config = await loadConfigFile("./fastedge-config.test.json");
253
246
  ```
254
247
 
255
- ### Final Response (CDN full flow)
248
+ ## Assertion Helpers
256
249
 
257
- ```typescript
258
- assertFinalStatus(fullFlowResult, 200)
259
- assertFinalHeader(fullFlowResult, 'content-type', 'application/json')
260
- ```
250
+ All assertion helpers throw an `Error` on failure, making them compatible with any test framework (vitest, jest, node:assert) or plain try/catch.
261
251
 
262
- ### Hook Return Code
252
+ ### Request Headers
263
253
 
264
254
  ```typescript
265
- assertReturnCode(hookResult, 0) // 0 = Ok, 1 = Pause
255
+ function assertRequestHeader(result: HookResult, name: string, expected?: string): void
256
+ function assertNoRequestHeader(result: HookResult, name: string): void
266
257
  ```
267
258
 
268
- ### Logs
259
+ `assertRequestHeader` asserts the named header exists in the hook's output request headers. If `expected` is provided, also asserts the value matches exactly.
260
+
261
+ `assertNoRequestHeader` asserts the named header is absent.
269
262
 
270
263
  ```typescript
271
- assertLog(hookResult, 'cache hit') // At least one log contains substring
272
- assertNoLog(hookResult, 'error') // No log contains substring
273
- const found = logsContain(hookResult, 'x') // Boolean — for conditional logic
264
+ const hookResult = result.hookResults.onRequestHeaders;
265
+
266
+ assertRequestHeader(hookResult, "x-forwarded-for"); // exists
267
+ assertRequestHeader(hookResult, "x-country-code", "DE"); // exists with value
268
+ assertNoRequestHeader(hookResult, "x-internal-secret"); // absent
274
269
  ```
275
270
 
276
- ### CDN Property Access
271
+ ### Response Headers
277
272
 
278
273
  ```typescript
279
- assertPropertyAllowed(hookResult, 'client.ip') // Access was permitted
280
- assertPropertyDenied(hookResult, 'internal.key') // Access was denied
281
- const violated = hasPropertyAccessViolation(hookResult) // Boolean
274
+ function assertResponseHeader(result: HookResult, name: string, expected?: string): void
275
+ function assertNoResponseHeader(result: HookResult, name: string): void
282
276
  ```
283
277
 
284
- ---
278
+ Same semantics as the request header variants, but operates on the hook's output response headers.
279
+
280
+ ```typescript
281
+ const hookResult = result.hookResults.onResponseHeaders;
285
282
 
286
- ## `loadConfigFile(path)` — Reuse `test-config.json`
283
+ assertResponseHeader(hookResult, "cache-control");
284
+ assertResponseHeader(hookResult, "content-type", "application/json");
285
+ assertNoResponseHeader(hookResult, "server");
286
+ ```
287
287
 
288
- Load and validate a `test-config.json` file, returning a typed `TestConfig`. Throws with a descriptive error if the file is missing, invalid JSON, or fails schema validation.
288
+ ### Final Response
289
289
 
290
290
  ```typescript
291
- import { loadConfigFile, defineTestSuite, runAndExit } from '@gcoredev/fastedge-test/test';
291
+ function assertFinalStatus(result: FullFlowResult, expected: number): void
292
+ function assertFinalHeader(result: FullFlowResult, name: string, expected?: string): void
293
+ ```
292
294
 
293
- const config = await loadConfigFile('./test-config.json');
295
+ `assertFinalStatus` asserts the final response status code after the full flow completes.
294
296
 
295
- const suite = defineTestSuite({
296
- wasmPath: './build/app.wasm',
297
- tests: [
298
- {
299
- name: 'uses env var from config',
300
- run: async (runner) => {
301
- // config.envVars, config.secrets, config.properties available here
302
- const result = await runFlow(runner, {
303
- url: 'https://example.com',
304
- properties: config.properties,
305
- });
306
- // ...
307
- },
308
- },
309
- ],
310
- });
297
+ `assertFinalHeader` asserts a header in `result.finalResponse.headers`. If `expected` is provided, also asserts the value.
311
298
 
312
- await runAndExit(suite);
299
+ ```typescript
300
+ assertFinalStatus(result, 200);
301
+ assertFinalHeader(result, "x-cache", "HIT");
302
+ assertFinalHeader(result, "content-encoding"); // exists, any value
313
303
  ```
314
304
 
315
- ---
305
+ ### Return Code
316
306
 
317
- ## Low-Level: Runner API
307
+ ```typescript
308
+ function assertReturnCode(result: HookResult, expected: number): void
309
+ ```
318
310
 
319
- For cases where you need direct control over the runner lifecycle or want to integrate with an existing test framework — use the root entry point.
311
+ Asserts the hook's return code. Common values: `0` = Continue, `1` = Pause/StopIteration.
320
312
 
321
313
  ```typescript
322
- import { createRunner, createRunnerFromBuffer } from '@gcoredev/fastedge-test';
323
-
324
- // From a file path
325
- const runner = await createRunner('./build/app.wasm');
326
-
327
- // From a buffer (e.g. fetched from a URL or built in-memory)
328
- const buffer = await fs.readFile('./build/app.wasm');
329
- const runner = await createRunnerFromBuffer(buffer);
330
-
331
- // CDN: run the full request/response flow
332
- const result = await runner.callFullFlow(
333
- 'https://example.com', // url
334
- 'GET', // method
335
- {}, // request headers
336
- '', // request body
337
- {}, // response headers
338
- '', // response body
339
- 200, // response status
340
- 'OK', // response status text
341
- {}, // properties
342
- true, // enforce production property rules
343
- );
344
-
345
- // HTTP-WASM: execute a request
346
- const response = await runner.execute({
347
- path: '/api/hello',
348
- method: 'GET',
349
- headers: {},
350
- });
351
-
352
- // Always clean up when done
353
- await runner.cleanup();
314
+ assertReturnCode(result.hookResults.onRequestHeaders, 0); // filter continued
315
+ assertReturnCode(result.hookResults.onRequestHeaders, 1); // filter paused
354
316
  ```
355
317
 
356
- > **Tip**: Prefer `runFlow()` from `@gcoredev/fastedge-test/test` over `callFullFlow()` directly — it handles pseudo-headers and has named parameters.
318
+ ### Logs
319
+
320
+ ```typescript
321
+ function assertLog(result: HookResult, messageSubstring: string): void
322
+ function assertNoLog(result: HookResult, messageSubstring: string): void
323
+ function logsContain(result: HookResult, messageSubstring: string): boolean
324
+ ```
357
325
 
358
- ---
326
+ `assertLog` asserts at least one log entry contains `messageSubstring`.
359
327
 
360
- ## Integrating with Vitest / Jest
328
+ `assertNoLog` asserts no log entry contains `messageSubstring`.
361
329
 
362
- The assertion helpers throw plain `Error` instances, so they work naturally inside any test framework:
330
+ `logsContain` is a non-throwing predicate useful for conditional checks.
363
331
 
364
332
  ```typescript
365
- import { describe, it } from 'vitest';
366
- import { createRunner } from '@gcoredev/fastedge-test';
367
- import { runFlow, assertFinalStatus } from '@gcoredev/fastedge-test/test';
368
-
369
- describe('my CDN app', () => {
370
- it('returns 200 for homepage', async () => {
371
- const runner = await createRunner('./build/app.wasm');
372
- try {
373
- const result = await runFlow(runner, { url: 'https://example.com/' });
374
- assertFinalStatus(result, 200);
375
- } finally {
376
- await runner.cleanup();
377
- }
378
- });
379
- });
333
+ const hookResult = result.hookResults.onRequestHeaders;
334
+
335
+ assertLog(hookResult, "cache miss");
336
+ assertNoLog(hookResult, "error");
337
+
338
+ if (logsContain(hookResult, "debug:")) {
339
+ // conditional logic based on log presence
340
+ }
380
341
  ```
381
342
 
382
- Or use `defineTestSuite` / `runTestSuite` to manage the lifecycle automatically and inspect `SuiteResult` inside your test:
343
+ ### Properties
383
344
 
384
345
  ```typescript
385
- it('all flows pass', async () => {
386
- const suite = defineTestSuite({ wasmPath: './build/app.wasm', tests: [...] });
387
- const results = await runTestSuite(suite);
388
- expect(results.failed).toBe(0);
389
- });
346
+ function hasPropertyAccessViolation(result: HookResult): boolean
347
+ function assertPropertyAllowed(result: HookResult, propertyPath: string): void
348
+ function assertPropertyDenied(result: HookResult, propertyPath: string): void
390
349
  ```
391
350
 
392
- ---
351
+ Property access violations appear as log messages containing `"Property access denied"`.
393
352
 
394
- ## JSON Schemas
353
+ `hasPropertyAccessViolation` returns true if any such message exists.
395
354
 
396
- The package ships JSON Schema files for `test-config.json` and all API request/response bodies. These enable IDE autocomplete and validation.
355
+ `assertPropertyAllowed` throws if the named property path was denied.
356
+
357
+ `assertPropertyDenied` throws if the named property path was not denied.
397
358
 
398
359
  ```typescript
399
- import { createRequire } from 'module';
400
- const require = createRequire(import.meta.url);
401
- const schema = require('@gcoredev/fastedge-test/schemas/test-config.schema.json');
360
+ const hookResult = result.hookResults.onRequestHeaders;
361
+
362
+ assertPropertyAllowed(hookResult, "request.path");
363
+ assertPropertyDenied(hookResult, "upstream.address");
364
+
365
+ if (hasPropertyAccessViolation(hookResult)) {
366
+ console.warn("At least one property access was denied");
367
+ }
402
368
  ```
403
369
 
404
- Or reference them directly from `test-config.json` for VS Code autocomplete:
370
+ ## CI Integration
371
+
372
+ `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.
373
+
374
+ **package.json script:**
405
375
 
406
376
  ```json
407
377
  {
408
- "$schema": "./node_modules/@gcoredev/fastedge-test/schemas/test-config.schema.json",
409
- "envVars": {},
410
- "secrets": {},
411
- "properties": {}
378
+ "scripts": {
379
+ "test:wasm": "node --experimental-vm-modules test/suite.mjs"
380
+ }
412
381
  }
413
382
  ```
414
383
 
415
- ---
384
+ **GitHub Actions example:**
385
+
386
+ ```yaml
387
+ - name: Run WASM tests
388
+ run: npm run test:wasm
389
+ ```
390
+
391
+ **Makefile example:**
392
+
393
+ ```makefile
394
+ test:
395
+ node test/suite.mjs
396
+ ```
397
+
398
+ For programmatic use (e.g. collecting results before exiting), use `runTestSuite` directly and inspect `SuiteResult.failed`:
399
+
400
+ ```typescript
401
+ const result = await runTestSuite(suite);
402
+ process.exitCode = result.failed > 0 ? 1 : 0;
403
+ ```
404
+
405
+ ## Complete Example
406
+
407
+ ```typescript
408
+ import {
409
+ defineTestSuite,
410
+ runAndExit,
411
+ runFlow,
412
+ assertRequestHeader,
413
+ assertNoRequestHeader,
414
+ assertResponseHeader,
415
+ assertFinalStatus,
416
+ assertFinalHeader,
417
+ assertReturnCode,
418
+ assertLog,
419
+ assertPropertyAllowed,
420
+ assertPropertyDenied,
421
+ } from "@gcoredev/fastedge-test/test";
422
+
423
+ const suite = defineTestSuite({
424
+ wasmPath: "./build/cdn-filter.wasm",
425
+ tests: [
426
+ {
427
+ name: "injects geo headers on request",
428
+ async run(runner) {
429
+ const result = await runFlow(runner, {
430
+ url: "https://cdn.example.com/image.png",
431
+ method: "GET",
432
+ properties: {
433
+ "request.geo.country_code": "DE",
434
+ },
435
+ });
436
+
437
+ const req = result.hookResults.onRequestHeaders;
438
+ assertReturnCode(req, 0);
439
+ assertRequestHeader(req, "x-country-code", "DE");
440
+ assertNoRequestHeader(req, "x-internal-token");
441
+ },
442
+ },
443
+
444
+ {
445
+ name: "sets cache-control on response",
446
+ async run(runner) {
447
+ const result = await runFlow(runner, {
448
+ url: "https://cdn.example.com/static/app.js",
449
+ responseStatus: 200,
450
+ responseHeaders: { "content-type": "application/javascript" },
451
+ });
452
+
453
+ const res = result.hookResults.onResponseHeaders;
454
+ assertResponseHeader(res, "cache-control", "public, max-age=31536000");
455
+ assertFinalStatus(result, 200);
456
+ },
457
+ },
458
+
459
+ {
460
+ name: "blocks requests missing required auth header",
461
+ async run(runner) {
462
+ const result = await runFlow(runner, {
463
+ url: "https://cdn.example.com/api/private",
464
+ method: "POST",
465
+ requestHeaders: { "content-type": "application/json" },
466
+ });
467
+
468
+ assertFinalStatus(result, 403);
469
+ assertFinalHeader(result, "x-block-reason", "missing-auth");
470
+ },
471
+ },
472
+
473
+ {
474
+ name: "logs cache decision",
475
+ async run(runner) {
476
+ const result = await runFlow(runner, {
477
+ url: "https://cdn.example.com/data.json",
478
+ });
479
+
480
+ const req = result.hookResults.onRequestHeaders;
481
+ assertLog(req, "cache-check:");
482
+ },
483
+ },
484
+
485
+ {
486
+ name: "allows access to request path property",
487
+ async run(runner) {
488
+ const result = await runFlow(runner, {
489
+ url: "https://cdn.example.com/path/to/resource",
490
+ });
491
+
492
+ const req = result.hookResults.onRequestHeaders;
493
+ assertPropertyAllowed(req, "request.path");
494
+ assertPropertyDenied(req, "upstream.address");
495
+ },
496
+ },
497
+
498
+ {
499
+ name: "passes through with pre-loaded buffer",
500
+ async run(runner) {
501
+ const result = await runFlow(runner, {
502
+ url: "https://cdn.example.com/",
503
+ responseStatus: 304,
504
+ });
505
+
506
+ assertFinalStatus(result, 304);
507
+ },
508
+ },
509
+ ],
510
+ });
511
+
512
+ await runAndExit(suite);
513
+ ```
416
514
 
417
- ## Related Documentation
515
+ ## See Also
418
516
 
419
- - **REST API**: `docs/API.md`HTTP endpoints for server-based testing
420
- - **WASM Loading**: `docs/HYBRID_LOADING.md`path vs buffer performance tradeoffs
517
+ - [RUNNER.md](RUNNER.md)Low-level `IWasmRunner` interface, `RunnerConfig`, and `callFullFlow`
518
+ - [API.md](API.md)REST API for running tests via HTTP
519
+ - [TEST_CONFIG.md](TEST_CONFIG.md) — `fastedge-config.test.json` schema and `loadConfigFile` config options
520
+ - [quickstart.md](quickstart.md) — Installation and first test walkthrough