Files
Lucas BergerandClaude Opus 4.8 6ade8d59bf
Publish / publish (push) Successful in 11s
docs: correct rrule drift (code uses ical.js RecurExpansion) + minor fixes
Recurrence expansion uses ical.js ICAL.RecurExpansion, not the rrule
library (never installed/imported). Also fix playwright-cli path
(/usr/bin), prod-compose service count (two, post-Redis-removal), and
CI job count (six). Found via /gsd-docs-update --verify-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:29:03 -04:00

15 KiB
Raw Permalink Blame History

Testing

Test framework and setup

Both apps use Vitest (^4.1.8).

App Environment Global setup Per-file setup
apps/api node apps/api/test/global-setup.ts apps/api/test/setup.ts
apps/pwa jsdom apps/pwa/src/test-setup.ts

apps/api global setup (test/global-setup.ts) runs once before any test file. Locally it provisions an isolated familysync_test database (root connection → CREATE DATABASE IF NOT EXISTS familysync_test → GRANT → drizzle migrate) and then truncates every table to give each run a clean slate. Under CI (process.env.CI truthy) it returns immediately — the CI api job provisions its own familysync service container via db:migrate.

apps/api per-file setup (test/setup.ts) registers a global afterEach that truncates list_items, list_shares, push_subscriptions, lists, and local_credentials in FK-safe order after every test. This keeps DB-backed integration tests isolated without requiring a full DB reset between runs. The users table is intentionally left intact across tests within a single run — many tests seed user id=1 once and reuse it. Parallel file execution is disabled (fileParallelism: false) to prevent FK violations when multiple test files share the same MariaDB.

apps/pwa setup (src/test-setup.ts) imports @testing-library/jest-dom for extended matchers and polyfills window.matchMedia for jsdom (required because Zustand's calendarStore calls window.matchMedia at module initialisation time). The timezone is pinned to UTC via env: { TZ: 'UTC' } so date-extraction assertions are deterministic across developer machines and CI.

No additional install step is needed beyond the normal pnpm install at the repo root.

Running tests

Unit and integration tests

All API tests (from repo root):

pnpm --filter @familysync/api test

This is also the command run by pnpm test at the root.

All PWA unit tests:

pnpm --filter @familysync/pwa test

Watch mode (API):

pnpm --filter @familysync/api test:watch

Single test file:

pnpm --filter @familysync/api exec vitest run tests/routes/lists.test.ts

End-to-end tests (Playwright)

The PWA has a Playwright harness configured in apps/pwa/playwright.config.ts with three device profiles:

Profile Viewport Engine User-Agent
iphone 390×844 WebKit Mobile Safari (iPhone 14)
pixel 412×915 Chromium Chrome Android (Pixel 7)
desktop 1280×720 Chromium Desktop Chrome

All profiles block the service worker (serviceWorkers: 'block') so the Workbox SW does not intercept requests during tests. Auth is handled via DEV_AUTH_BYPASS=true on the API — never via stored browser state.

Run all e2e tests (all profiles):

pnpm test:e2e
# or
pnpm --filter @familysync/pwa test:e2e

Run a single profile:

pnpm --filter @familysync/pwa exec playwright test --project=pixel
pnpm --filter @familysync/pwa exec playwright test --project=iphone
pnpm --filter @familysync/pwa exec playwright test --project=desktop

Interactive UI mode:

pnpm --filter @familysync/pwa test:e2e:ui

Headed mode (for local debugging):

pnpm --filter @familysync/pwa test:e2e:headed

The baseURL is driven by PLAYWRIGHT_BASE_URL (default: http://localhost:5173). In local mode the config reuses a running Vite dev server; in CI it starts Vite itself. The API and MariaDB must already be running via Docker Compose before launching e2e tests locally — see docs/DEVELOPMENT.md.

Type checking (separate from tests — required)

Vitest uses esbuild, which strips TypeScript types at runtime. A test run can pass while tsc reports errors. Always run type checks separately:

pnpm typecheck          # runs tsc --noEmit across both apps
pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck

The PWA typecheck also covers the e2e spec files: tsc --project tsconfig.e2e.json --noEmit.

Quality gate

The full local quality gate before opening a PR:

pnpm lint && pnpm format:check && pnpm typecheck && pnpm test

Add e2e when changing PWA behaviour:

pnpm test:e2e
Step Command What it checks
Lint pnpm lint ESLint --max-warnings 0 across both apps (type-aware)
Format check pnpm format:check Prettier — fails on any unformatted file
Markdown lint pnpm md:lint markdownlint-cli2 across all .md files
Typecheck pnpm typecheck tsc --noEmit across both apps (including e2e tsconfig)
Unit / API tests pnpm test API integration tests via Vitest
PWA unit tests pnpm --filter @familysync/pwa test Component and logic tests in jsdom
E2E pnpm test:e2e Playwright iphone + pixel + desktop profiles

A deliberate ESLint violation makes pnpm lint exit non-zero; a formatting deviation makes pnpm format:check exit non-zero. Both block the PR in CI.

To auto-fix formatting locally:

pnpm format     # prettier --write .

Integration tests requiring a real database

Several API tests in apps/api/tests/lib/ and apps/api/tests/routes/ connect to a real MariaDB instance. Locally, the Vitest global setup (test/global-setup.ts) auto-provisions and migrates the familysync_test database — there is no need to manually set DB_NAME. The dev familysync database is never touched by the test suite.

Start the dev stack:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mariadb

Set environment variables, then run:

set -a; . ./.env; set +a
export DB_HOST=127.0.0.1 DB_PORT=3306
pnpm --filter @familysync/api test

The global setup requires root access to create and grant the test database. By default it reads DB_ROOT_PASSWORD from the environment (defaults to root to match the dev Docker Compose). The app user (DB_USER) is validated against /^[A-Za-z0-9_]+$/ before the GRANT statement is interpolated.

DB-backed tests that require this setup include:

  • apps/api/tests/lib/listAccess.test.tsgetAccessibleListIds access-scope queries
  • apps/api/tests/lib/listChangeDispatcher.test.ts — list change dispatcher with real DB rows
  • apps/api/tests/routes/lists.test.ts — full lists API router (creates/deletes real rows)

Pure-logic tests (e.g. apps/api/tests/broker/expand.test.ts, apps/api/tests/lib/rank.test.ts) do not require DB — the afterEach cleanup is a no-op when tables are empty or no DB connection is available.

Writing new tests

File naming and location

App Convention Example
apps/api apps/api/tests/{category}/*.test.ts apps/api/tests/routes/push.test.ts
apps/pwa co-located *.test.ts / *.test.tsx src/components/AppNav.test.tsx
apps/pwa e2e specs e2e/*.spec.ts

Test categories for apps/api:

  • apps/api/tests/auth/ — authentication middleware, session handling, local auth, admin guards, and bypass behaviour
  • apps/api/tests/broker/ — CalDAV sync, outbox worker, event expansion, push dispatch, crypto utilities, and VEVENT parsing
  • apps/api/tests/lib/ — pure library functions and service logic (list access, rank, push coalescer/dispatcher, SSE emitter, timezone, boot guards)
  • apps/api/tests/routes/ — HTTP route integration tests (events, lists, push, admin, login, me, local auth, setup)
  • apps/api/tests/health.test.ts — health check endpoint
  • apps/api/tests/fixtures/ — shared .ics fixture files and DB fixture helpers

Test helpers

  • apps/api/tests/helpers/db.tscreateMockDb() returns a Vitest mock of the Drizzle db singleton; also exports sample VEVENT strings (SAMPLE_VEVENT_TIMED, SAMPLE_VEVENT_ALLDAY, SAMPLE_VEVENT_RECURRING_TIMED, SAMPLE_VEVENT_RECURRING_ALLDAY) for broker tests.
  • apps/api/tests/fixtures/*.ics — Raw iCalendar fixture files for broker parsing tests (absolute-alarm.ics, allday-birthday.ics, exdate-series.ics, multi-alarm.ics, single-duration.ics, weekly-count3.ics, weekly-dst.ics).
  • apps/api/tests/fixtures/vapid.ts — VAPID key fixture for push tests.
  • apps/pwa/src/test-setup.ts — Provides matchMedia polyfill and jest-dom matchers for all PWA tests automatically via setupFiles.

For PWA component tests, use @testing-library/react (^16.3.0) render helpers. Import from vitest for assertions — @testing-library/jest-dom matchers are available globally via the setup file.

Coverage requirements

No coverage thresholds are configured in either vitest.config.ts. There is no minimum coverage enforcement in CI.

CI integration

CI runs on a self-hosted Gitea Actions runner and triggers on every pull request targeting main (.gitea/workflows/ci.yml). A changes job using dorny/paths-filter@v4 determines whether the PR touches code (as opposed to docs or planning files only). The api and harness jobs are skipped for doc-only PRs.

Six jobs run in total — changes, fast-checks, security, and gate always run; api and harness run conditionally (skipped on doc-only PRs).

fast-checks

Runs lint, format check, markdown lint, typecheck, and PWA unit tests — no external services required. Always runs regardless of the changes filter.

Step Command
Lint pnpm lint
Format check pnpm format:check
Markdown lint pnpm md:lint
Typecheck pnpm typecheck
PWA unit tests pnpm --filter @familysync/pwa test

api

Runs the full API test suite against a mariadb:11 service container. Skipped for doc-only PRs.

Step Detail
MariaDB service mariadb:11 container; DB_HOST=mariadb, DB_PORT=3306
Readiness poll Node script via mysql2 driver (no mysql CLI in runner)
Schema migrations pnpm --filter @familysync/api db:migrate
Tests pnpm --filter @familysync/api test

The throwaway credentials (DB_USER=familysync, DB_PASSWORD=testpass) are scoped to the ephemeral CI container and are never production secrets.

actions/cache@v4 is intentionally omitted — the cache server times out on this runner (socket hang-up). pnpm install without cache takes ~30 s and is acceptable.

harness

Runs the Playwright mobile and desktop e2e harness (iphone + pixel + desktop) against a runner-hosted dev stack. Skipped for doc-only PRs.

Step Detail
MariaDB service Same mariadb:11 setup as the api job
Schema migrations pnpm --filter @familysync/api db:migrate
Dev user seed Inserts users row id=1 (INSERT IGNORE) for DEV_AUTH_BYPASS
Local credentials seed Inserts local_credentials row for dev user (username: devuser, password: devpass) via inline scrypt hash — Phase 19 requirement
API build pnpm --filter @familysync/api build (dist/ is gitignored)
Playwright install npx playwright install --with-deps webkit chromium (no cache)
API start + tests API started as a background process in the same step as playwright test to survive the step boundary; DEV_AUTH_BYPASS=true, NODE_ENV=development
Base URL http://127.0.0.1:5173 (not localhost — runner resolves localhost to ::1 but Vite binds IPv4-only)
Artifacts on fail Traces, screenshots, videos, and HTML report uploaded via ChristopherHX/gitea-upload-artifact@v4 (standard upload-artifact aborts on Gitea)

The API process is started and the Playwright suite invoked within a single CI step. Starting the API in an earlier step causes it to be reaped at the step boundary before Playwright runs.

security

Runs secret scanning and dependency audits. Always runs regardless of the changes filter (secrets can appear in doc-only commits). Dependency audit and outdated checks run only when code changes are detected.

Step Tool/Command Detail
Secret scan gitleaks (v8.30.1) Scans the PR diff range; blocks on any finding
Dependency audit node scripts/check-audit.mjs Blocks on High or Critical severity vulnerabilities
Outdated report node scripts/check-outdated.mjs Advisory only — always exits 0, logged but never gates

gate

A required final job that checks all other jobs passed or were legitimately skipped. fast-checks and security must succeed; api and harness may be skipped (doc-only PRs) but not failed.