Testing
Stack
| Layer | Tool |
|---|---|
| Test runner | Vitest v4 |
| HTTP integration | supertest |
| HTTP interception | nock (intercepts at Node http layer — works with CJS require('axios')) |
| Coverage | V8 (built-in, no Babel needed) |
Running tests
# 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 fromserver/index.js. Tests importcreateApp()without triggering the log-file setup,process.exit()calls, or background poller in the entry point.- nock over
vi.mock('axios')— Vitest'svi.mockonly intercepts ESMimportstatements. Sinceauth.jsuses CJSrequire('axios'), nock (which patches Node'shttp/httpsmodules) is the correct tool for intercepting outbound requests. SKIP_RATE_LIMIT=1— All supertest requests originate from127.0.0.1, which would quickly exhaust per-IP rate-limit windows. Setting this env var raises the limits toNumber.MAX_SAFE_INTEGERin both the API limiter and the login limiter.- Isolated
DATA_DIR— Each test worker gets a unique temp directory sotokenStore.jsfile 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.