Framework:
- Vitest v4 as test runner (fast ESM/CJS support, V8 coverage built-in)
- supertest for integration tests against createApp() factory
- nock for HTTP interception (works with CJS require('axios'), unlike vi.mock)
New files:
- vitest.config.js — test config: node env, isolate, V8 coverage, per-file thresholds
- tests/setup.js — isolated DATA_DIR per worker, SKIP_RATE_LIMIT, console suppression
- tests/README.md — approach, structure, design decisions
- server/app.js — testable Express factory (extracted from index.js side-effects)
Unit tests (91 tests):
- tests/unit/sanitizeError.test.js — secret redaction: apikey, token, bearer, basic-auth URLs
- tests/unit/config.test.js — JSON array + legacy single-instance config parsing
- tests/unit/requireAuth.test.js — valid/invalid/tampered cookies, schema validation
- tests/unit/verifyCsrf.test.js — double-submit pattern, timing-safe compare, safe methods
- tests/unit/qbittorrent.test.js — formatBytes, formatEta, mapTorrentToDownload state map
- tests/unit/tokenStore.test.js — store/get/clear lifecycle, TTL expiry, atomic disk write
Integration tests (24 tests):
- tests/integration/health.test.js — /health and /ready endpoints
- tests/integration/auth.test.js — full login/logout/me/csrf flows, input validation,
cookie attributes, no token leakage, Emby mock via nock
Production code changes (minimal, no behaviour change):
- server/routes/auth.js: EMBY_URL captured at request-time (not module load) for testability
- server/routes/auth.js: loginLimiter max → Number.MAX_SAFE_INTEGER when SKIP_RATE_LIMIT set
- server/utils/sanitizeError.js: fix HEADER_PATTERN to redact full line (not just first token)
CI:
- .gitea/workflows/ci.yml: add parallel 'test' job (npm run test:coverage, artifact upload)
- package.json: add test/test:watch/test:coverage/test:ui scripts
- .gitignore: add coverage/
56 lines
1.4 KiB
JavaScript
56 lines
1.4 KiB
JavaScript
/**
|
|
* Integration tests for health and readiness endpoints.
|
|
*
|
|
* /health and /ready are used by Docker HEALTHCHECK and must:
|
|
* - Require no authentication
|
|
* - Not be rate-limited
|
|
* - Return the correct status codes
|
|
*/
|
|
|
|
import request from 'supertest';
|
|
import { createApp } from '../../server/app.js';
|
|
|
|
describe('GET /health', () => {
|
|
let app;
|
|
|
|
beforeEach(() => {
|
|
app = createApp();
|
|
});
|
|
|
|
it('returns 200 with status ok', async () => {
|
|
const res = await request(app).get('/health');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.status).toBe('ok');
|
|
});
|
|
|
|
it('includes uptime as a number', async () => {
|
|
const res = await request(app).get('/health');
|
|
expect(typeof res.body.uptime).toBe('number');
|
|
expect(res.body.uptime).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
describe('GET /ready', () => {
|
|
let app;
|
|
|
|
afterEach(() => {
|
|
delete process.env.EMBY_URL;
|
|
});
|
|
|
|
it('returns 200 when EMBY_URL is configured', async () => {
|
|
process.env.EMBY_URL = 'https://emby.local';
|
|
app = createApp();
|
|
const res = await request(app).get('/ready');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.status).toBe('ready');
|
|
});
|
|
|
|
it('returns 503 when EMBY_URL is not configured', async () => {
|
|
delete process.env.EMBY_URL;
|
|
app = createApp();
|
|
const res = await request(app).get('/ready');
|
|
expect(res.status).toBe(503);
|
|
expect(res.body.status).toBe('not ready');
|
|
});
|
|
});
|