Skip to main content

The default suite

go test ./... runs everywhere with no Docker daemon and no network. It covers the domain packages, view rendering, provider webhook parsing, backup encryption, and log redaction. Keep it that way: a suite that needs infrastructure to run is a suite that stops being run. Alongside it CI runs TypeScript checking, frontend builds, Biome, Atlas validation, Mintlify validation, vulnerability scanning, and container builds.

The integration suite

Tests under internal/integrationtest carry the integration build tag and start PostgreSQL through Testcontainers:
The harness applies the committed migration files in order rather than a schema dump, so the suite is a migration gate as well as a repository gate. It covers:
  • migrations applying from a bare database, and the v0.4 → v0.5 adoption of existing customers onto subscriptions;
  • top-up crediting the wallet exactly once across a replayed webhook, a second event, and a reconciliation;
  • overpayment and underpayment resolving in the ledger while the order still settles;
  • a saved cart staying saved while short and being charged once when funded;
  • concurrency limits holding under simultaneous purchases;
  • the outbox and the operator notice being written once per settlement;
  • maintenance blocking new orders while a paid order still settles;
  • the durable fulfillment job committing in the settlement transaction;
  • retention removing disposable rows and never an audit event;
  • operator bootstrap creating exactly one owner, and the setup token being single-use;
  • sign-in answering identically for an unknown address and a wrong password, lockout after repeated failures, and the session, TOTP, and recovery-code lifecycles;
  • the last active owner surviving both role revocation and suspension, and only an owner granting the owner role;
  • suspension revoking live sessions, a password change sparing only the session that made it, and a reset link working once;
  • audit classification, both sort directions, and keyset pages that never repeat or skip a row.
load_test.go is part of the same suite. It is not a throughput benchmark: it drives the same settlement, cart purchase, and subscription creation from many goroutines at once, because a duplicated charge only appears when the same work arrives from several directions simultaneously. Running the suite is the cheapest way to catch a class of defect that compiles and unit-tests cleanly: a Go enum widened without its database CHECK, an expiry computed from the application clock while its row’s created_at comes from the database’s, or a constraint whose derived name was guessed wrong. Each of those only fails against a real PostgreSQL.

Migration upgrade gate

The migrations CI job runs once per supported baseline — the empty database plus each prefix of the migration history — applies the rest, then applies head a second time to prove it is a no-op. A migration that cannot run on top of a released version fails there.

What a financial change owes

New payment, wallet, authentication, RBAC, import, and Remnawave fulfillment behaviour must include focused failure-path and idempotency tests from its first implementation. In practice that means proving, for the new path:
  1. The same input applied twice produces one effect.
  2. A partial failure leaves no half-applied state.
  3. Every rejection carries a stable machine reason, not a formatted message.

Provider fixtures

internal/payments/testdata holds recorded webhook shapes with every real identifier replaced. Replaying them keeps parsing and signature verification honest without a sandbox account. Never commit a real payment payload, token, or signature — synthesise one instead.

Browser coverage

apps/web/e2e holds the Playwright suites, run with bun run e2e from apps/web. They run against a production build rather than the dev server: Next.js behaves differently in development, and a gate that passes against a build nobody ships is not a gate. The suites cover what a component test cannot see, because it is a property of a rendered page rather than of a component — the accessibility, layout, and localisation gates — plus the access boundary of both panels: every operator and customer route refuses an anonymous visitor, and nothing behind a session gate paints before the session is known. The operator journey behind the session gate is covered. CI applies the migrations, starts a real API, redeems the one-time setup token to seed an owner, and operator-journey.spec.ts signs in for real, proves the session survives a navigation, and sweeps every panel page. Locally, set PANEL_EMAIL and PANEL_PASSWORD against a running stack; without them that spec skips and says so rather than pretending to pass. A missing translation renders as a marker rather than as its own key — see apps/web/i18n/missing-message.ts — and the sweep looks for exactly that. The previous check searched for a dotted key, which cannot tell a missing message from data: the audit log stores action names in the same shape, and the check failed on a fully translated page the first time it ran against an API with rows in it. The customer journey is covered too, and needs no seeding at all: the panel provisions an account on first sign-in, so customer-journey.spec.ts signs a Telegram login-widget payload with the same bot token the API is configured with and posts it through the web server’s /v1 proxy. Set CUSTOMER_BOT_TOKEN to that token to run it locally. internal/customerauth carries an interop test asserting the TypeScript signing and the Go verification agree — without it, a mismatch would fail every run and get “fixed” by weakening the signature check the suite exists to exercise.
Nothing behind a Remnawave entitlement is covered. The customer the suite creates has signed in and bought nothing, so the screens that need a subscription render their empty state rather than their populated one. That is tracked in the roadmap’s verification debt for v0.10.

Performance budgets

bun --filter @omniflow/web perf:budget runs against a finished next build and fails when a route’s first-load JavaScript, the stylesheet payload, or the self-hosted fonts exceed their ceilings. CI runs it in the web job, straight after the build it measures. Sizes are uncompressed, which is what Next’s own route-bundle-stats.json reports and the more honest figure to gate on: compression hides a regression that still costs a phone its parse and execute time. Images, server response time, and field core web vitals are deliberately not measured. They need a running installation with real content and real clients, and a number invented from a local build would read as a gate that had passed.