Skip to content

Testing

Kilat ships a test suite across five files in tests/ using bun:test: app.test.ts (E2E), auth.test.ts, db.test.ts, validation.test.ts, and rate-limit.test.ts (unit). The E2E suite boots the full Hono app and drives it through app.request() — no HTTP server, no Workers runtime, just the app logic with an in-memory D1. The unit suites test pure functions and modules in isolation.

Terminal window
bun test --isolate
# or
bun run test

Never run plain bun test. Bun 1.3 runs all test files in one shared process, but each suite sets its env in beforeAll and calls cleanup in afterAll as if process-isolated. Without --isolate, one file’s teardown finalizes the next file’s cached values — tests fail with stale state.

--isolate gives each file its own module registry, so initConfig / initDb / db.close() run cleanly per suite.

import { afterAll, beforeAll, describe, expect, it } from "bun:test";
let app: Awaited<ReturnType<typeof import("../src/server/app")["createApp"]>>;
beforeAll(async () => {
// set env, initConfig, create in-memory D1, apply schema, createApp
});
afterAll(async () => {
// db.close()
});

The helper call(path, options) builds a Request and invokes app.request() directly — same code path a real Worker takes, minus the network.

The unit suites test pure functions and modules without booting the app. They share the --isolate requirement (each file gets its own module registry) but need no D1 or Hono setup.

Exercises the auth helpers in src/server/auth.ts:

  • hashPassword / verifyPassword round-trip, wrong-password rejection, timing-safe comparison
  • hashToken produces stable, distinct digests
  • Session CRUD: create, get, delete, expiry
  • Flash messages: set, peek, consume, auto-expiry
  • Password reset tokens: issue, verify, invalidate, expiry
  • Email verification tokens: issue, verify, invalidate, expiry

Exercises the D1 data layer in src/server/db.ts against an in-memory D1 mock:

  • Users CRUD: insert, get by id/email, update, delete
  • Sessions: insert, get, delete
  • Password resets: insert, get, delete, expiry
  • Email verifications: insert, get, delete
  • Uploads: insert, list, delete
  • GET /health row count
  • toPublicUser strips sensitive fields (password hash, tokens)

Exercises validateJson and the ValidationFailed error in src/server/validation.ts:

  • Valid body passes through unchanged
  • Missing required fields → 422 with field list
  • Wrong types → 422
  • Invalid email format → 422
  • additionalProperties: false rejects extra keys
  • Malformed JSON body → 400
  • Non-JSON content-type → 400
  • ValidationFailed exposes .status, .code, .fields
  • Email format registry validates and rejects common malformed variants

Exercises the KV fixed-window limiter in src/server/rate-limit.ts with an in-memory mock KV:

  • First request within window is allowed
  • Requests over the limit within the window are blocked
  • Counter resets at the window boundary
  • Per-key isolation (different keys have independent counters)
  • X-RateLimit-Remaining decrements correctly
  • Headers reflect the active window

The subsections below describe the E2E coverage in app.test.ts. Unit coverage lives in the four unit suites — see Unit tests above for the per-file breakdown.

  • Registration creates a user and sets a session cookie
  • Login with correct credentials → 303 redirect to /dashboard
  • Login with wrong credentials → 422 with “credentials do not match”
  • Logout clears the session and cookie
  • requireAuth redirects unauthenticated users to /login
  • guestOnly redirects authenticated users to /dashboard
  • requireRole('admin') blocks non-admins from /admin (302 → /dashboard)
  • Admin page serves paginated users with meta.total and currentPage
  • /forgot-password answers identically for known and unknown emails (no enumeration)
  • End-to-end reset: email sent → token extracted from sentMails → new password set → old password fails, new password works
  • Mismatched password confirmation → 422
  • Expired/invalid tokens → 422 with “invalid or has expired”
  • Version mismatch (x-inertia-version: stale) → 409 + X-Inertia-Location header
  • Unknown routes → 404 with NotFound component payload
  • XHR with accept-encoding: gzip → valid JSON (compress middleware must not consume small bodies)
  • Full browser request → SSR HTML with data-server-rendered="true"
  • Authenticated routes skip SSR (client-only render via data-page)
  • Cross-origin POST (Origin: https://evil.example) → 403
  • GET /health{ status: "ok" }
  • /assets/* serves static files with correct content-type
  • /auth/google returns 400 when unconfigured, 302 to Google when configured

The unit suites cover the pure-function and data-layer contracts the E2E suite relies on: password/token hashing, session and token CRUD, the D1 query layer, JSON request validation, and the KV rate limiter. See Unit tests for the full per-file breakdown.

Terminal window
bun run typecheck # tsc --noEmit (covers src/ and scripts/)
bun run lint # biome lint src tests scripts
bun run test # bun test --isolate

Both must be green. tsc does not cover tests/ — the test runner catches type issues there.