7.1 KiB
Testing
Ekklesia separates fast code-level evidence from live infrastructure evidence.
Unit and repository verification
pnpm test
pnpm verify
These commands exercise package tests, repository boundaries, type checking, offline Core and
module migration discovery, identity schema drift detection, formatting, linting, and builds.
Migration-runner unit tests use a narrow database/session port to prove deterministic planning,
approval, transaction orchestration, cleanup, and sanitized errors. They do not claim to prove
PostgreSQL behavior. Identity unit tests cover branded opaque-ID contracts, principal shapes,
configuration validation (secret, base URL, and trusted-origin rules), bounded/validated
connection and operation (query_timeout/statement_timeout) database timeouts, the Core identity
runtime (explicit construction, exact schema mapping, only email/password and rate limiting
enabled, an explicit advanced.disableCSRFCheck/disableOriginCheck: false pair, an explicit
error-only sanitized logger that strips Better Auth's own message and arguments regardless of what
they contain, pinned 8–128 password-length bounds, sanitized failures, and that a database outage
during principal resolution is never silently reported as anonymous), and Core migration discovery
(deterministic ordering, the reserved core identity, and collision rejection) — all without a
live database. apps/api's own unit tests cover its pure Hono app factory (route shapes, CORS
allow/deny/preflight, deterministic 404 and sanitized error responses), its runtime configuration,
its idempotent shutdown lifecycle (concurrent callers share one server-close-then-pool-close
promise; a startup failure after the pool was already created still closes it; SIGINT/SIGTERM
wiring is exercised through an injectable exit function rather than the real process.exit), and
that importing any of its modules — including the executable server module without calling its
exported main — never reads the environment, connects to PostgreSQL, or starts a server.
pnpm verify is intentionally Docker-free. It never plans or applies migrations against a live
database.
Live PostgreSQL migration evidence
Docker and the Docker Compose plugin are required for the live suite:
pnpm test:migrations:integration
The command first builds the workspace through the repository's Turbo build graph, then starts the
repository's pinned PostgreSQL image in a unique Compose project, waits for bounded health
readiness, runs the production PostgreSQL adapter and migration services, and removes the
container, network, and volume. A build failure occurs before Docker provisioning. Each scenario
also receives a safely named disposable database. Direct host runs use a Docker-assigned random
port bound only to 127.0.0.1; a containerized Gitea job joins the isolated Compose network and
uses the PostgreSQL service name.
The live suite proves fresh read-only planning, initial and incremental application, genuine no-op behavior, exact applied checksums and history, edited or corrupt history rejection, whole-batch DDL rollback, Drizzle breakpoint execution, real advisory-lock concurrency and timeouts, lock release after failure, exact destructive approvals, and public-error sanitization. It does not make migrations automatic during Core startup.
A dedicated set of scenarios exercises the committed Core identity migration against real
PostgreSQL: read-only planning on a fresh database, applying it and confirming exactly the four
expected ekklesia_core_* tables and their indexes exist, a genuine no-op on a second apply, Core
executing before a fixture module migration in one batch, previously applied module-only history
staying byte-for-byte unchanged once Core is introduced, and a later module migration's failure
rolling back cleanly without damaging the already-committed Core identity migration or leaking the
advisory lock.
A second dedicated suite (apps/api/tests/api-http.integration.ts, included in the same
pnpm test:migrations:integration run) starts the real apps/api HTTP server against a freshly
migrated disposable database and exercises it with real HTTP requests: public sign-up (with no
automatic session, per autoSignIn: false), explicit sign-in producing a host-only
HttpOnly/SameSite=Lax/secure session cookie, /api/v1/principal for both the authenticated and
anonymous shapes, sign-out actually invalidating the session, a malformed or missing cookie
resolving as anonymous, duplicate sign-up creating no second identity row, two distinct accounts
producing identical principal shapes with no elevation and no owner/role/permission data at the
database level, no plaintext password ever stored, the exact 8–128 password-length boundary (7
rejected, 8 accepted, 128 accepted, 129 rejected), CORS never returning an allow-origin header for
an untrusted origin, Better Auth's handler being unreachable outside /api/auth/*, a database
outage during principal resolution producing a sanitized 503 rather than a false anonymous 200,
starting the API against an unmigrated database creating no tables at all, and a clean listener/pool
shutdown.
The same suite also proves Better Auth's own origin/CSRF protection is production-equivalent under
the test runner, not just in a plain Node script: a no-cookie first authentication attempt succeeds
from a trusted browser origin and is rejected (403, no permissive CORS header, no leaked detail)
from an untrusted one; a cookie-bearing state-changing request (sign-out) is rejected the same way
from an untrusted origin, and the presented session remains valid afterward, proving the rejected
request never reached sign-out logic. See
ADR 0012 for why
this previously could not be reproduced reliably under Vitest (Better Auth's own default silently
skips these checks whenever isTest() is true, unless advanced.disableOriginCheck is set
explicitly) and how the fix removes that gap entirely. A further scenario forces a real identity
database failure and asserts, against actual console.error/warn/log output, that Better
Auth's own internal error logging — configured explicitly in core/src/identity/runtime.ts — never
emits a raw exception, SQL, connection detail, or the rejected request's origin.
CI runs the same command in the dedicated migration-smoke job. After this change is merged, add
the following pull-request status context to main branch protection:
CI / migration-smoke (pull_request)
Safe troubleshooting
Confirm the prerequisites without printing configuration:
docker info
docker compose version
Then rerun pnpm test:migrations:integration. The harness emits fixed failure messages and does
not dump PostgreSQL logs, SQL, container inspection output, or the generated database URL. Do not
enable query logging, print the test environment, run docker compose config without redaction,
or paste Docker inspection output into an issue. If the Docker daemon itself stopped during
cleanup, restore it before rerunning so the harness can create a fresh uniquely named project.