3e6af1bff2
- Create tests/frontend/utils/format.test.js with 24 tests for formatting utilities - Create tests/frontend/ui/downloads.test.js with 10 tests for DOM rendering functions - Update vitest.config.js to support jsdom environment for frontend tests - All 34 tests pass and cover edge cases (null, zero, large numbers, DOM structure)
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
│ └── 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 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
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.