# 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 │ └── dashboard.test.js # Pure helper functions: getCoverArt, extractAllTags, │ # extractUserTag, sanitizeTagLabel, tagMatchesUser, │ # getImportIssues, getSonarrLink, getRadarrLink, │ # canBlocklist, extractEpisode, gatherEpisodes, buildTagBadges └── integration/ ├── health.test.js # GET /health and /ready endpoints ├── auth.test.js # Full login/logout/me/csrf flows via supertest + nock ├── history.test.js # GET /api/history/recent: auth, filtering, deduplication ├── webhook.test.js # POST /api/webhook/sonarr+radarr: secret, validation, │ # replay protection, metrics, security assertions ├── dashboard.test.js # GET /user-downloads (SAB+Sonarr, SAB+Radarr, qBit, showAll, │ # paused queue, history, importIssues), GET /status, │ # GET /webhook-metrics, GET /cover-art, POST /blocklist-search ├── emby.test.js # GET /sessions, /users, /users/:id, /session/:id/user └── arrRoutes.test.js # Sonarr + Radarr: queue, history, series/movies, notifications # CRUD, /test, /schema, /sofarr-webhook (create + update) # SABnzbd: queue, history ``` ## 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 Global thresholds (enforced in CI via `vitest.config.js`): | Metric | Threshold | |---|---| | Statements | 55% | | Functions | 55% | | Branches | 40% | | Lines | 55% | Notable per-file coverage after the current suite: | File | Lines | Branches | Notes | |---|---|---|---| | `server/app.js` | ~92% | ~71% | | | `server/routes/auth.js` | ~88% | ~78% | | | `server/routes/dashboard.js` | ~42% | ~25% | SSE `/stream` endpoint intentionally untested | | `server/routes/emby.js` | 100% | 100% | | | `server/routes/radarr.js` | ~87% | ~77% | | | `server/routes/sonarr.js` | ~89% | ~82% | | | `server/routes/sabnzbd.js` | 100% | 100% | | | `server/routes/webhook.js` | ~85% | ~79% | | | `server/middleware/requireAuth.js` | ~92% | ~81% | | | `server/middleware/verifyCsrf.js` | 100% | 80% | | | `server/utils/sanitizeError.js` | 100% | 75% | | | `server/utils/config.js` | ~70% | ~58% | | `poller.js` (background polling engine) and the SSE `/stream` endpoint in `dashboard.js` require time-based behaviour and long-lived HTTP connections; they remain tracked as future test coverage work.