GioJSdocs
On this page

createTestServer

Start the real GioJS server for your app on a free port, with a private cache, for end-to-end tests of everything the Rust layer does.

ts
import { createTestServer } from '@gio.js/core/testing';

const server = await createTestServer();
const res = await fetch(`${server.url}/posts/1`);
await server.close();

Reference

OptionTypeDefaultDescription
appDirstringGIO_APP_DIR, else ./appThe app/ directory; the server runs from its parent, the project root.
envRecord<string, string | undefined>{}Extra environment for the server and its worker; undefined removes an inherited variable. { NODE_ENV: 'development' } gives a development server.
portnumbera free portThe port on 127.0.0.1. With a fixed port there is no retry when it is taken.
binarystringGIO_SERVER_BIN, else @gio.js/serverThe giojs-server binary to run. Without it, GIO_SERVER_BIN, then the platform binary that @gio.js/server installed, then (inside the GioJS repository) a target/debug or target/release build.
timeoutMsnumber60000How long to wait for the worker to be ready, in milliseconds.

Returns

A Promise<TestServer>, resolved once the Node worker is ready:

FieldTypeDefaultDescription
urlstring-The base URL, http://127.0.0.1:<port>, without a trailing slash.
portnumber-The port it listens on.
logs()string-Everything the server and its worker printed so far (the last 1 MiB).
close()Promise<void>-Stops the server and every process it started; safe to call twice.

Behavior

  • The server reads your gio.toml, middleware.ts, gio.config.ts and .env files as in production; only the listen address is overridden, through GIO_HOST and GIO_PORT.
  • Each server gets its own temporary page cache, image cache and IPC sockets, so several can run side by side, and the project's .gio/cache is never read or written. They are deleted on close().
  • It polls /_gio/health until the worker reports ready (a 404 there - [health] enabled = false - counts as ready, since the port opens only once the worker is up).
  • Values the test kit loaded from .env files and the random session secret it may have set are not passed on: the server loads the files itself, by its own mode. Variables your test set are passed on.
  • A running server never keeps the test process alive. When the process exits, or crashes, exit hooks kill the servers; when no hook can run (a SIGKILL, a vitest thread torn down), a small watchdog process does.

Errors

  • No binary found: it throws, naming npm install --save-dev @gio.js/server and GIO_SERVER_BIN. A binary or GIO_SERVER_BIN path that does not exist throws too.
  • The server exits before it is ready: it throws with the server's own error first - a gio.toml refusal, or a worker that could not boot, such as a middleware.ts that throws (giojs-server exited before it was ready: the Node worker exited before it was ready (exit status: 1): .../middleware.ts failed to load: ...) - then the last 40 lines of its log.
  • Not ready within timeoutMs: it stops the server and throws, with the log.
  • The chosen free port was taken in the meantime: it retries on another port, up to three times.

Examples

Guards, CSRF and headers (vitest)

For a project with a session guard on /admin and the /api/notes handler from Request body errors:

gio.toml
[[guards]]
path = "/admin/*rest"
require_session = true
redirect_to = "/login"
tests/server.test.ts
import { afterAll, beforeAll, expect, it } from 'vitest';
import { createTestServer, type TestServer } from '@gio.js/core/testing';

let server: TestServer;
beforeAll(async () => {
  server = await createTestServer({ env: { GIO_SESSION_SECRET: 'test-only-secret-at-least-32-bytes-long' } });
}, 60_000);   // the worker builds client bundles before it is ready
afterAll(() => server.close());

it('guards /admin in Rust', async () => {
  const res = await fetch(`${server.url}/admin`, { redirect: 'manual' });
  expect(res.status).toBe(302);
  expect(res.headers.get('location')).toBe('/login');
});

it('refuses a cross-site POST', async () => {
  const res = await fetch(`${server.url}/api/notes`, {
    method: 'POST',
    headers: { origin: 'https://evil.example', 'content-type': 'application/json' },
    body: '{"text":"hi"}',
  });
  expect(res.status).toBe(403);
});

it('sends the default security headers', async () => {
  const res = await fetch(server.url);
  expect(res.headers.get('x-content-type-options')).toBe('nosniff');
});

The page cache and revalidation

With the cached /posts/[id] page and the PUT handler that calls revalidateTag from revalidateTag:

ts
it('purges a post on update', async () => {
  await fetch(`${server.url}/posts/1`);                       // miss; stored
  const cached = await fetch(`${server.url}/posts/1`);
  expect(cached.headers.get('x-gio-cache')).toMatch(/^hit/);

  await fetch(`${server.url}/api/posts/1`, {
    method: 'PUT',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ title: 'Changed' }),
  });
  const fresh = await fetch(`${server.url}/posts/1`);
  expect(fresh.headers.get('x-gio-cache')).toMatch(/^miss/);
  expect(await fresh.text()).toContain('Changed');
});

fetch() in Node sends neither Origin nor Sec-Fetch-Site, so its POSTs pass the CSRF check like any client that is not a browser page. Set an origin header of another site to test the refusal, as in the first example.

Good to know

  • Startup takes seconds - the worker builds the client bundles first. Start one server per test file in beforeAll, give that hook a longer timeout, and close it in afterAll.
  • Production mode unless you pass NODE_ENV=development in env, so the server needs what production needs - for example a GIO_SESSION_SECRET when a session storage is created at import.
  • Plain HTTP only. A gio.toml with [server.tls] enabled needs a test copy of the project without it.
  • The binary comes from the @gio.js/server package (scaffolded apps have it), or from GIO_SERVER_BIN - useful in CI with a binary you built.

Version history

VersionChanges
v0.1.0-beta.8Introduced.