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.
Run the tests
Section titled “Run the tests”bun test --isolate# orbun run test--isolate is required
Section titled “--isolate is required”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.
Suite structure
Section titled “Suite structure”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.
Unit tests
Section titled “Unit tests”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.
auth.test.ts (32 tests)
Section titled “auth.test.ts (32 tests)”Exercises the auth helpers in src/server/auth.ts:
hashPassword/verifyPasswordround-trip, wrong-password rejection, timing-safe comparisonhashTokenproduces 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
db.test.ts (35 tests)
Section titled “db.test.ts (35 tests)”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 /healthrow counttoPublicUserstrips sensitive fields (password hash, tokens)
validation.test.ts (10 tests)
Section titled “validation.test.ts (10 tests)”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: falserejects extra keys- Malformed JSON body → 400
- Non-JSON content-type → 400
ValidationFailedexposes.status,.code,.fields- Email format registry validates and rejects common malformed variants
rate-limit.test.ts (6 tests)
Section titled “rate-limit.test.ts (6 tests)”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-Remainingdecrements correctly- Headers reflect the active window
What’s covered
Section titled “What’s covered”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.
Auth basics
Section titled “Auth basics”- 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
Guards & roles
Section titled “Guards & roles”requireAuthredirects unauthenticated users to/loginguestOnlyredirects authenticated users to/dashboardrequireRole('admin')blocks non-admins from/admin(302 →/dashboard)- Admin page serves paginated users with
meta.totalandcurrentPage
Password reset (log mail driver)
Section titled “Password reset (log mail driver)”/forgot-passwordanswers 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”
Inertia protocol
Section titled “Inertia protocol”- Version mismatch (
x-inertia-version: stale) → 409 +X-Inertia-Locationheader - Unknown routes → 404 with
NotFoundcomponent 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
Infrastructure
Section titled “Infrastructure”GET /health→{ status: "ok" }/assets/*serves static files with correct content-type/auth/googlereturns 400 when unconfigured, 302 to Google when configured
Unit tests
Section titled “Unit tests”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.
Before submitting
Section titled “Before submitting”bun run typecheck # tsc --noEmit (covers src/ and scripts/)bun run lint # biome lint src tests scriptsbun run test # bun test --isolateBoth must be green. tsc does not cover tests/ — the test runner catches
type issues there.