Skip to content

G-Core/fastedge-test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@gcoredev/fastedge-test

Test FastEdge WASM binaries programmatically — no server required. Use it in CI pipelines, agent scripts, or alongside your existing test runner (vitest, jest, etc.).

Supports both FastEdge binary types:

  • CDN (proxy-wasm) — request/response filter binaries
  • HTTP-WASM — component model HTTP handler binaries

The WASM type is detected automatically from the binary.

Installation

npm install @gcoredev/fastedge-test
# or
pnpm add @gcoredev/fastedge-test

Requirements: Node.js 18+

Two Ways to Use This Package

1. Headless test framework (CI / scripts)

Import the test framework to test WASM binaries programmatically — no server, no browser:

Import Purpose
@gcoredev/fastedge-test Low-level runner — load and execute WASM directly
@gcoredev/fastedge-test/test High-level test framework — define and run structured test suites

Start with @gcoredev/fastedge-test/test unless you need direct runner control.

2. Local visual debugger (browser UI)

Run a local server with a browser UI, REST API, and WebSocket log streaming — useful for interactive debugging without VSCode:

npx @gcoredev/fastedge-test
# Opens http://localhost:5179

Or with a custom port:

PORT=8080 npx @gcoredev/fastedge-test

See Local Debugger Server for the full guide including programmatic usage, REST API access, and WebSocket log streaming.


Quick Start

CDN (proxy-wasm)

import { defineTestSuite, runAndExit, runFlow } from '@gcoredev/fastedge-test/test';
import { assertRequestHeader, assertFinalStatus } from '@gcoredev/fastedge-test/test';

const suite = defineTestSuite({
  wasmPath: './build/my-cdn-app.wasm',
  tests: [
    {
      name: 'injects x-custom header',
      run: async (runner) => {
        const result = await runFlow(runner, {
          url: 'https://example.com/page',
          method: 'GET',
          requestHeaders: { 'user-agent': 'Mozilla/5.0' },
          responseStatus: 200,
        });

        assertRequestHeader(result.hookResults.onRequestHeaders, 'x-custom', 'expected-value');
      },
    },
    {
      name: 'blocks /admin with 403',
      run: async (runner) => {
        const result = await runFlow(runner, { url: 'https://example.com/admin' });
        assertFinalStatus(result, 403);
      },
    },
  ],
});

await runAndExit(suite);
// Output:
//   ✓ injects x-custom header (12ms)
//   ✓ blocks /admin with 403 (5ms)
//   2/2 passed in 17ms

HTTP-WASM (component model)

import { defineTestSuite, runAndExit } from '@gcoredev/fastedge-test/test';

const suite = defineTestSuite({
  wasmPath: './build/my-http-app.wasm',
  tests: [
    {
      name: 'responds with 200 OK',
      run: async (runner) => {
        const response = await runner.execute({
          path: '/api/hello',
          method: 'GET',
          headers: { 'content-type': 'application/json' },
        });

        if (response.status !== 200) {
          throw new Error(`Expected 200, got ${response.status}`);
        }
      },
    },
  ],
});

await runAndExit(suite);

Test Suite API

defineTestSuite(config)

Validates and returns a typed suite definition.

type TestSuite =
  | { wasmPath: string;   runnerConfig?: RunnerConfig; tests: TestCase[] }
  | { wasmBuffer: Buffer; runnerConfig?: RunnerConfig; tests: TestCase[] }

Exactly one of wasmPath or wasmBuffer is required — TypeScript enforces this at compile time.

interface RunnerConfig {
  dotenvEnabled?: boolean;                  // Load .env file (default: false)
  enforceProductionPropertyRules?: boolean; // CDN property access control (default: true)
}

runTestSuite(suite)SuiteResult

Runs all tests sequentially. Each test gets a fresh, isolated runner instance.

interface SuiteResult {
  passed: number;
  failed: number;
  total: number;
  durationMs: number;
  results: TestResult[];
}

interface TestResult {
  name: string;
  passed: boolean;
  error?: string;
  durationMs: number;
}

runAndExit(suite)

Runs the suite, prints a summary to stdout, and exits with code 0 (all passed) or 1 (any failures). Designed for CI scripts and Makefile targets.

runFlow(runner, options) — CDN helper

Convenience wrapper around the full CDN request/response flow. Accepts named options and derives HTTP/2 pseudo-headers from the URL automatically.

interface FlowOptions {
  url: string;
  method?: string;                           // Default: 'GET'
  requestHeaders?: Record<string, string>;
  requestBody?: string;
  responseStatus?: number;                   // Default: 200
  responseStatusText?: string;
  responseHeaders?: Record<string, string>;
  responseBody?: string;
  properties?: Record<string, unknown>;
  enforceProductionPropertyRules?: boolean;  // Default: true
}

Returns a FullFlowResult containing hookResults, finalResponse, and calculatedProperties.

loadConfigFile(path) — Reuse fastedge-config.test.json

Load and validate a fastedge-config.test.json file, returning a typed TestConfig.

import { loadConfigFile, defineTestSuite, runAndExit } from '@gcoredev/fastedge-test/test';

const config = await loadConfigFile('./fastedge-config.test.json');

const suite = defineTestSuite({
  wasmPath: './build/app.wasm',
  tests: [
    {
      name: 'uses config properties',
      run: async (runner) => {
        const result = await runFlow(runner, {
          url: 'https://example.com',
          properties: config.properties,
        });
      },
    },
  ],
});

Assertion Helpers

All helpers throw a descriptive Error on failure — compatible with any test framework or plain try/catch.

Request / Response Headers (CDN)

assertRequestHeader(hookResult, 'x-custom')               // header exists
assertRequestHeader(hookResult, 'x-custom', 'value')      // header equals value
assertNoRequestHeader(hookResult, 'x-should-not-exist')

assertResponseHeader(hookResult, 'cache-control', 'no-store')
assertNoResponseHeader(hookResult, 'x-sensitive')

Final Response (CDN full flow)

assertFinalStatus(fullFlowResult, 200)
assertFinalHeader(fullFlowResult, 'content-type', 'application/json')

Hook Return Code

assertReturnCode(hookResult, 0)   // 0 = Ok, 1 = Pause

Logs

assertLog(hookResult, 'cache hit')          // at least one log contains substring
assertNoLog(hookResult, 'error')            // no log contains substring
const found = logsContain(hookResult, 'x')  // boolean — for conditional logic

CDN Property Access

assertPropertyAllowed(hookResult, 'client.ip')    // access was permitted
assertPropertyDenied(hookResult, 'internal.key')  // access was denied
const violated = hasPropertyAccessViolation(hookResult)

Integrating with Vitest / Jest

The assertion helpers throw plain Error instances, so they work inside any test framework:

import { describe, it } from 'vitest';
import { createRunner } from '@gcoredev/fastedge-test';
import { runFlow, assertFinalStatus } from '@gcoredev/fastedge-test/test';

describe('my CDN app', () => {
  it('returns 200 for homepage', async () => {
    const runner = await createRunner('./build/app.wasm');
    try {
      const result = await runFlow(runner, { url: 'https://example.com/' });
      assertFinalStatus(result, 200);
    } finally {
      await runner.cleanup();
    }
  });
});

Or let defineTestSuite manage lifecycle and inspect SuiteResult inside your test:

it('all flows pass', async () => {
  const suite = defineTestSuite({ wasmPath: './build/app.wasm', tests: [...] });
  const results = await runTestSuite(suite);
  expect(results.failed).toBe(0);
});

Low-Level Runner API

For cases needing direct runner control:

import { createRunner, createRunnerFromBuffer } from '@gcoredev/fastedge-test';

// From file path (preferred — faster startup, less memory)
const runner = await createRunner('./build/app.wasm');

// From buffer (e.g. fetched from URL or built in-memory)
const buffer = await fs.readFile('./build/app.wasm');
const runner = await createRunnerFromBuffer(buffer);

// CDN: run full request/response flow
const result = await runner.callFullFlow(
  'https://example.com', 'GET',
  {}, '',          // request headers, body
  {}, '',          // response headers, body
  200, 'OK',       // response status
  {},              // properties
  true,            // enforce production property rules
);

// HTTP-WASM: execute a request
const response = await runner.execute({
  path: '/api/hello',
  method: 'GET',
  headers: {},
});

// Always clean up when done
await runner.cleanup();

JSON Schemas

The package ships JSON Schema files for fastedge-config.test.json and all API request/response bodies, enabling IDE autocomplete and validation.

Reference from fastedge-config.test.json for VS Code autocomplete:

{
  "$schema": "./node_modules/@gcoredev/fastedge-test/schemas/fastedge-config.test.schema.json",
  "envVars": {},
  "secrets": {},
  "properties": {}
}

CI / GitHub Actions Example

- name: Test FastEdge WASM
  run: node test/my-suite.js

Where test/my-suite.js:

import { defineTestSuite, runAndExit, runFlow } from '@gcoredev/fastedge-test/test';
import { assertFinalStatus } from '@gcoredev/fastedge-test/test';

await runAndExit(defineTestSuite({
  wasmPath: './build/app.wasm',
  tests: [
    {
      name: 'homepage returns 200',
      run: async (runner) => {
        assertFinalStatus(await runFlow(runner, { url: 'https://example.com/' }), 200);
      },
    },
  ],
}));

Documentation


License

MIT

About

FastEdge Debugging tool. Used for local development and testing

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors