Skip to content
FluffBoost

Testing

Unit tests, real PostgreSQL bot flows, and browser tests of the GitHub Pages export.

Tests use bun:test with Sinon, configured in apps/discord/bunfig.toml. Test files live in apps/discord/tests/, mirroring src/, and use the .test.ts suffix.

Running tests

bun run test            # bot unit suite (excludes @fluffboost/motion)
bun run test:coverage   # with a coverage report

Run just the bot's suite from its package:

bun --filter @fluffboost/discord test

bun run brand:verify separately checks the animated brand exports in apps/motion; it needs ffmpeg and ffprobe on your PATH.

The unit-test scripts run with --isolate and preload tests/preload.ts to mock application settings before imports. They do not require .env or depend on the order test files run. Individual suites can override that fixture. The E2E script does not use this preload and exercises the real environment loader.

Always use these scripts. A bare bun test at the repository root also picks up the bot E2E suite and the Playwright specs, and skips the preload fixture. To run one file:

cd apps/discord
NODE_ENV=test bun test --isolate --preload ./tests/preload.ts tests/utils/timezones.test.ts

Patterns

  • Prefer injected dependencies. Modules such as sendMotivationCore.ts, setActivityCore.ts and premiumReconciliationCore.ts take their database, logger and helpers as arguments, so tests pass stubs directly.
  • mock.module() is per file. The scripts pass --isolate, so each test file gets a fresh module registry and the preload fixture; a mock never reaches another file and needs no restore hook. Inside one file it persists across tests and replaces the whole module, so when you mock a shared module spread its real exports and replace only what you need (tests/commands/permissionsMock.ts shows the pattern). A partial mock makes other imports fail with Export named ... not found. Without --isolate (a bare bun test) mocks leak between files, which is one more reason to use the scripts.
  • Fresh copies. To evaluate a module again under different mocks in the same file, import it with a query suffix, such as import("../../src/events/commandRegistry.js?env=production").
  • Time control — schedule-sensitive tests use sinon.useFakeTimers() to pin dayjs() to a fixed moment.
  • Shared fakes — tests/helpers.ts provides factories (mockLogger, mockDb, mockInteraction, mockClient, mockEnv, and friends) so each test stays short.
  • Routers — tests for /admin, /owner and /setup swap entries in the exported route Maps instead of mocking subcommand modules.

Unit tests isolate commands and jobs behind these fakes and never touch a real database, Redis, or the Discord API.

Bot end-to-end tests

apps/discord/e2e/ exercises the actual interaction router, command handlers, Drizzle queries, schema migrations, embeds, premium policy, and delivery worker with real PostgreSQL. Discord transport stays local: these tests never log in to Discord, create entitlements, or post to a server. Redis and BullMQ job dispatch are outside this suite; it invokes the production job handler directly.

The suite covers channel setup, administrator permissions, Premium activation and expiry, concurrent SQL delivery claims, retry after a transient send failure, and real Discord.js channel resolution when a guild is absent from a shard's cache. A separate scheduler regression checks the repeated autumn DST hour.

Start a disposable database in another terminal:

docker run --rm --name fluffboost-e2e-db \
  -e POSTGRES_USER=ci -e POSTGRES_PASSWORD=ci -e POSTGRES_DB=fluffboost_e2e \
  -p 127.0.0.1:55439:5432 postgres:18-alpine

Then run:

E2E_DATABASE_URL=postgres://ci:ci@127.0.0.1:55439/fluffboost_e2e \
  E2E_DATABASE_DISPOSABLE=true \
  bun run --filter=@fluffboost/discord test:e2e

Each run creates a unique schema, applies the repository migrations inside it, and drops only that schema on completion. The suite requires the exact database name fluffboost_e2e, a loopback host, and the explicit E2E_DATABASE_DISPOSABLE=true operator assertion; it never falls back to DATABASE_URL. The assertion means you have confirmed the target database is disposable. Host and name checks cannot identify the server behind a local port forward, so never point a tunnel at production. Use a disposable test database and a role scoped to that database; stop the container afterward with docker stop fluffboost-e2e-db.

Browser end-to-end tests

Playwright builds apps/docs with the deployed /fluffboost base path and serves the static out/ directory with no route fallback. Desktop Chromium and a Pixel 7 viewport sweep every page in the generated sitemap for broken local links, missing assets, client errors, horizontal overflow, a single <main>, and per-page canonical and og: metadata. They also cover static search (including the retry after a failed index load and focus return), the branded 404 page, a keyboard Tab walk (skip link first, focus always visible and never covered), the mobile docs drawer, and an axe-core WCAG 2.2 AA scan of every page (with reduced motion) and of the visible mobile invite bar. Mobile emulation does not replace testing on a real phone.

bun run --filter=@fluffboost/docs test:e2e:install
bun run --filter=@fluffboost/docs test:e2e

Preview the static export

bun run --filter=@fluffboost/docs preview builds with the /fluffboost base path and serves it at http://127.0.0.1:4173/fluffboost/. bun run --filter=@fluffboost/docs start serves an existing out/ directory; set NEXT_PUBLIC_BASE_PATH to match the build it serves.

To run both suites, set E2E_DATABASE_URL as above and run bun run test:e2e. Browser failure screenshots and traces are written to the ignored apps/docs/test-results/ directory, with an HTML report in apps/docs/playwright-report/.

In CI

GitHub Actions runs on every push and PR to main and dev:

JobWhat it checks
Test & Typecheckbun run test:coverage, schema drift, ESLint, brand:verify, TypeScript
Security Auditbun audit
Bot E2E (PostgreSQL)The bot E2E suite against a fresh PostgreSQL 18 service
Docs Build & Browser E2EContrast check, then Playwright against the exact static export
Docker Build TestBuilds the image and runs its migrations twice (the second is a no-op)

The coverage report and the browser report and failure traces are uploaded as build artifacts.

On this page