# Testing ## Stack | Layer | Tool | |---|---| | Test runner | [Vitest](https://vitest.dev/) v4 | | HTTP integration | [supertest](https://github.com/ladjs/supertest) | | HTTP interception | [nock](https://github.com/nock/nock) (intercepts at Node http layer — works with CJS `require('axios')`) | | Coverage | V8 (built-in, no Babel needed) | ## Running tests ```bash # Run all tests once npm test # Watch mode (re-runs on file change) npm run test:watch # With coverage report npm run test:coverage # Interactive UI npm run test:ui ``` Coverage output lands in `coverage/` (gitignored). Open `coverage/index.html` for the HTML report. ## Structure ``` tests/ ├── setup.js # Global setup: isolated DATA_DIR, SKIP_RATE_LIMIT, console suppression ├── unit/ │ ├── sanitizeError.test.js # Secret redaction patterns (API keys, tokens, passwords) │ ├── config.test.js # JSON array + legacy single-instance config parsing │ ├── requireAuth.test.js # Auth middleware: valid/invalid/tampered cookies │ ├── verifyCsrf.test.js # CSRF double-submit cookie pattern + timing-safe compare │ ├── qbittorrent.test.js # Pure utils: formatBytes, formatEta, mapTorrentToDownload │ └── tokenStore.test.js # JSON file token store: store/get/clear, TTL expiry └── integration/ ├── health.test.js # GET /health and /ready endpoints └── auth.test.js # Full login/logout/me/csrf flows via supertest + nock ``` ## Key design decisions - **`server/app.js`** — Express factory extracted from `server/index.js`. Tests import `createApp()` without triggering the log-file setup, `process.exit()` calls, or background poller in the entry point. - **nock over `vi.mock('axios')`** — Vitest's `vi.mock` only intercepts ESM `import` statements. Since `auth.js` uses CJS `require('axios')`, nock (which patches Node's `http`/`https` modules) is the correct tool for intercepting outbound requests. - **`SKIP_RATE_LIMIT=1`** — All supertest requests originate from `127.0.0.1`, which would quickly exhaust per-IP rate-limit windows. Setting this env var raises the limits to `Number.MAX_SAFE_INTEGER` in both the API limiter and the login limiter. - **Isolated `DATA_DIR`** — Each test worker gets a unique temp directory so `tokenStore.js` file I/O never conflicts with a running dev server. - **`createApp({ skipRateLimits: true })`** — The app factory accepts an option to disable the general API rate limiter in addition to the env var for the login-specific limiter. ## Coverage targets The tested files meet these per-file minimums (enforced in CI): | File | Lines | Branches | |---|---|---| | `server/app.js` | 85% | 65% | | `server/routes/auth.js` | 85% | 70% | | `server/middleware/requireAuth.js` | 75% | 80% | | `server/utils/sanitizeError.js` | 60% | — | | `server/utils/config.js` | 50% | 55% | `dashboard.js` and `poller.js` are large files requiring complex external-service mocks (Sonarr, Radarr, qBittorrent, Emby) and are tracked as future test coverage work.