Skip to content

Testing

The suite is end-to-end, not unit — it boots the full app against an in-memory SQLite database and drives it through app.request() (no port, no server process) across two files:

  • tests/app.test.ts — auth, guards, roles, password reset, Inertia protocol (409/404/SSR), CSRF, health, static assets
  • tests/tus.test.ts — the tus protocol (creation, resume, checksum, termination, ownership) and the profile/avatar flows

Each file is written as an independent suite: it sets its env (DATABASE_PATH=:memory:, UPLOAD_DIR, RATE_LIMIT_AUTH_MAX=1000, RATE_LIMIT_GLOBAL_MAX=10000, …) in beforeAll before importing the app modules (config/db read env at import), and calls db.close() in afterAll. Bun 1.3 runs all test files in one shared process, so without --isolate one file’s teardown finalizes the next file’s prepared statements (“Statement has finalized”) and cached config leaks across files.

Terminal window
bun run test # = bun test --isolate
const xhr = { "x-inertia": "true" }; // simulate an Inertia SPA request
async function call(path, options) { // app.request with cookie/body
return app.request(`${BASE}${path}`, { method, headers, body });
}
function sessionCookie(res) { // extract the session cookie
const cookie = allSetCookies(res).find((c) => c.startsWith("session="));
return cookie ? cookie.split(";")[0]! : "";
}
async function registerUser(email, password = "password123") {
const res = await call("/register", {
method: "POST",
headers: xhr,
body: { name: "Test User", email, password },
});
expect(res.status).toBe(303);
return sessionCookie(res); // returns the cookie for later calls
}
  1. Register a user (returns a session cookie).
  2. Actcall() the endpoint with xhr + cookie.
  3. Assert — status, Inertia payload (component, props.errors, props.auth.user), redirect location, headers.

Inertia XHR requests return JSON page payloads, so assertions are structured:

const data = await res.json();
expect(data.component).toBe("Dashboard");
expect(data.props.auth.user.email).toBe("loginok@example.com");
expect(JSON.stringify(data.props)).not.toContain("passwordHash");

Mirror the helpers at the top of the file and add a case to the relevant describe block — or a new block. Env is already configured by beforeAll; if your test needs different env (e.g. tus expiration), set it in a new test FILE (each --isolate process gets fresh globals).

Keep tests deterministic: no sleeps, no external network (fetch is mocked in the Google OAuth block), and control time explicitly where it matters (the expiration sweep takes now as a parameter for this reason).

The E2E suite covers server behavior (status codes, redirects, Inertia payloads, CSRF, auth). It does not catch client-side errors — a page can return 200 with a valid payload and still crash in the browser because of a prop shape mismatch, a missing import, or a Svelte/React runtime error.

When testing UI changes manually, the browser console is the source of truth:

  1. Open DevTools → Console before interacting with the page.
  2. Reload and watch for red errors (runtime exceptions, failed imports, hydration mismatches).
  3. Check the Network tab for failed asset loads (404 on JS/CSS, wrong manifest paths).
  4. A blank page with no console error usually means the Inertia component name doesn’t match a registered page — check src/client/pages.ts.

bun run typecheck and bun run build catch type and compile errors, but neither runs the client code. Only the browser does.