docs: refresh project documentation against current codebase
Publish / publish (push) Successful in 26s

This commit is contained in:
Lucas Berger
2026-06-18 06:44:29 -04:00
parent 18d3ee6a4f
commit 1e2cc52659
11 changed files with 872 additions and 178 deletions
+64 -33
View File
@@ -6,12 +6,14 @@
Both apps use **Vitest** (`^4.1.8`).
| App | Environment | Setup file |
| ---------- | ----------- | ---------------------------- |
| `apps/api` | `node` | `apps/api/test/setup.ts` |
| `apps/pwa` | `jsdom` | `apps/pwa/src/test-setup.ts` |
| 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 setup** (`test/setup.ts`) registers a global `afterEach` that truncates `list_items`, `list_shares`, `push_subscriptions`, and `lists` in FK-safe order after every test. This keeps DB-backed integration tests isolated without requiring a full DB reset between runs. Parallel file execution is disabled (`fileParallelism: false`) to prevent FK violations when multiple test files share the same MariaDB.
**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.
@@ -49,16 +51,17 @@ 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 two device profiles:
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) |
| 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 |
Both 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.
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 (both profiles):**
**Run all e2e tests (all profiles):**
```bash
pnpm test:e2e
@@ -71,6 +74,7 @@ pnpm --filter @familysync/pwa test:e2e
```bash
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:**
@@ -79,6 +83,12 @@ pnpm --filter @familysync/pwa exec playwright test --project=iphone
pnpm --filter @familysync/pwa test:e2e:ui
```
**Headed mode (for local debugging):**
```bash
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, MariaDB, and Redis must already be running via Docker Compose before launching e2e tests locally — see `docs/DEVELOPMENT.md`.
### Type checking (separate from tests — required)
@@ -111,10 +121,11 @@ pnpm test:e2e
| ---------------- | ------------------------------------ | -------------------------------------------------------- |
| 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 profiles |
| 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.
@@ -126,7 +137,7 @@ 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 the real dev MariaDB rather than mocking the DB layer. These tests require the dev Docker stack to be running with port 3306 exposed.
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:**
@@ -142,6 +153,8 @@ 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.ts``getAccessibleListIds` access-scope queries
@@ -162,17 +175,17 @@ Pure-logic tests (e.g. `apps/api/tests/broker/expand.test.ts`, `apps/api/tests/l
Test categories for `apps/api`:
- `apps/api/tests/auth/` — authentication middleware and session handling
- `apps/api/tests/broker/` — CalDAV sync, outbox worker, event expansion, push dispatch
- `apps/api/tests/lib/` — pure library functions and service logic
- `apps/api/tests/routes/` — HTTP route integration tests
- `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.ts``createMockDb()` 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 (`allday-birthday.ics`, `exdate-series.ics`, `single-duration.ics`, `weekly-count3.ics`, `weekly-dst.ics`).
- `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`.
@@ -184,22 +197,25 @@ No coverage thresholds are configured in either `vitest.config.ts`. There is no
## CI integration
CI runs on a self-hosted Gitea Actions runner and triggers on every pull request targeting `main` (`.gitea/workflows/ci.yml`). Three jobs run in parallel:
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.
Five jobs run in total — `fast-checks` and `security` always run; `api`, `harness`, and `changes` run conditionally.
### `fast-checks`
Runs lint, format check, typecheck, and PWA unit tests — no external services required.
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.
Runs the full API test suite against a `mariadb:11` service container. Skipped for doc-only PRs.
| Step | Detail |
| ----------------- | ---------------------------------------------------------- |
@@ -214,17 +230,32 @@ The throwaway credentials (`DB_USER=familysync`, `DB_PASSWORD=testpass`) are sco
### `harness`
Runs the Playwright mobile e2e harness (iphone + pixel) against a runner-hosted dev stack.
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` |
| 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) |
| 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.