Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

This page tells you which tests to run before a pull request, which kind of test to write for a change, and what CI checks for you.

Before a pull request

just validate

just alone runs the same recipe. It checks formatting, runs Clippy and ESLint, looks for unused code and dependencies, type-checks Rust and TypeScript, runs the Rust unit tests and the TypeScript SDK’s tests, builds the Rust docs, runs the doc tests and audits the dependencies. It needs no database and no browser.

Then run the layers your change can affect: just db test when it touches SQL, migrations or code that reads and writes the database, and the browser suite when it changes a page, a script or a sign-in flow.

While you work, run only the tests for what you changed, such as just test -p auth-service.

The test layers

Write a test at the lowest layer that can show the behaviour: a unit test if one can, a database test if the behaviour lives in SQL or a transaction, a browser test only when it needs a real browser.

Rust unit tests

just test                                 # the whole workspace
just test -p auth-service -E 'test(step_up)'

just test runs the workspace’s tests with nextest, and passes its arguments on. Unit tests live in a tests.rs beside their module, as the Rust conventions describe.

They need no network. Passkey ceremonies use a software authenticator (SoftPasskey), GitHub sign-in a local HTTP server that stands in for GitHub’s API, and Google sign-in ID tokens signed with keys the test generates.

Database tests

just db test

Tests that need PostgreSQL are marked #[ignore = "requires TEST_DATABASE_URL"], so just test skips them. just db test starts a throwaway PostgreSQL 18 cluster that listens only on a Unix socket, runs every test of auth-service and sourceorigin-repository in it, ignored ones included, and deletes it. It then runs just db prepare-check, which fails if the committed query cache doesn’t match the migrated schema exactly.

To use a database of your own instead, set TEST_DATABASE_URL to it. The tests create accounts in it and leave it migrated, so name only a database you can throw away, never one that holds real accounts. A TCP URL must include sslmode=disable or sslmode=verify-full.

A new database test connects through its crate’s fixture: Database::test_pool() in crates/database, or Replicas::database() in apps/auth-service/src/tests.rs.

SDK test vectors

The Rust and TypeScript SDKs run the same cases, in tests/sdk-vectors, so both accept and refuse the same tokens, key sets, callbacks and cookies. The cases are generated by crates/common/tests/sdk_vectors.rs. When an SDK behaviour must be the same in both languages, add a case there, then:

just sdk-vectors

The vectors’ README describes the case format.

TypeScript SDK tests

just web test    # unit tests, every vector included, with coverage
just web smoke   # the built package in Node, Bun and workerd

just web test runs packages/auth/test/*.test.ts with Node’s test runner. just web smoke builds the package, then validates one vector token with it in Node, Bun and workerd, the Cloudflare Workers runtime. It needs bun installed.

Browser tests

just web test-browser

The browser suite runs Chromium against a running deployment of auth over HTTPS: CI’s, or a preview of your checkout. Write one for what only a browser shows: a page’s layout or script, a passkey ceremony, a sign-in that crosses origins. The browser tests’ README says how to run them.

Generated files

Ordinary tests compare each committed generated file with what the code generates now, so just test fails while one is stale. The query cache is checked when the code compiles, and by just db prepare-check. Generated files lists each one and the recipe that regenerates it.

Write a focused test

  • Test one behaviour, and name the test for what it proves, such as google_key_fetch_is_a_client_span_of_its_request. Its doc comment says why the behaviour matters.
  • Assert the outcome, not that something ran. A test that only checks a page loads or a command exits 0 proves little and still has to be maintained.
  • Don’t test that a removed feature stays removed. Once the code is gone, there is nothing left to break.
  • Keep browser tests few. Each spec says at its top what it covers. Auth’s pages share one request budget a minute, which the whole suite spends, so test what a lower layer can’t.

Recipes

Test recipes pass extra arguments on to nextest or Playwright, quoted filter expressions included.

TaskRecipe
Formatting, lints, unused code, type checks, unit tests, Rust docs, doc tests and auditsjust validate
Format, or check formattingjust fmt / just fmt-check
Clippy and ESLint with warnings denied, and publint and attw on the built TypeScript SDKjust lint (just rust lint / just web lint)
Unused Rust dependencies (cargo-machete), and unused web files, exports and dependencies (knip)just unused (just rust unused / just web unused)
Type-check Rust, and TypeScript with the pages’ scriptsjust check (just rust check / just web check)
Rust unit tests, or one package’sjust test / just test -p auth-service
Rust docs with warnings denied, and the doc testsjust doc / just doc-test
TypeScript SDK tests with coverage, and the runtime smoke testjust web test / just web smoke
Database tests and the query cache check, in a scratch clusterjust db test
Query cache check alone, against a migrated TEST_DATABASE_URLjust db prepare-check
Coverage per crate of every Rust test, database tests included, into target/lcov.infojust coverage
Any command against a scratch clusterjust db scratch -- psql
Browser tests, against a running deploymentjust web test-browser
Register the browser tests’ clients with auth on localhostjust web browser-client
Dependency audits (cargo-deny and pnpm)just audit (just rust audit / just web audit)
Regenerate the query cache, the SDK vectors, the TypeScript SDK’s wire types, or the generated documentationjust db prepare / just sdk-vectors / just sdk-types / just docs reference
Build the book and check its linksjust docs

What CI runs

CI runs the same recipes, in these jobs:

JobWhat it runs
checksjust rust fmt-check, just rust unused, and just rust audit when dependencies change
webjust web fmt-check, check, lint, unused, test and smoke, and just web audit when dependencies change
rustjust rust lint, just coverage against a PostgreSQL 18 service, just db prepare-check, just doc and just doc-test
e2eThe browser suite, against the debug binary behind Caddy, with Mailpit receiving auth’s email and sourceorigin.dev’s web server on its own host
dockerBuilds the auth, web and docs images
docsjust docs

A pull request runs only the jobs its changes can affect, by the paths it changes: Rust jobs for Rust code, the web job for files Prettier, ESLint or tsc can see, and e2e and docker for anything the service, its images, the TypeScript SDK or the browser suite is built from. Changes to Markdown alone run only docs, which runs on every pull request, since a change anywhere can break a link to it. The changes job in ci.yml has the exact filters.

The merge queue, pushes to main and manual runs run every job. The ci check sums the others up: it fails if any job failed, and passes when the rest succeeded or were skipped. It is the one check the merge queue waits for.

Coverage is reported, never enforced. rust prints it per crate and uploads target/lcov.info as the rust-coverage artifact, and just web test prints the TypeScript SDK’s. A separate workflow audits the dependencies every Monday, so new advisories surface without a change.

CI doesn’t run:

  • just docs screenshots, which retakes the documentation’s screenshots on a preview; see screenshots.
  • Sign-in with live Google or GitHub accounts, or Apple’s and Google’s password managers syncing passkeys between real devices. Try those by hand on a preview when your change touches them.