/** * USER_JOURNEYS.md §11 — admin reads/writes config via the API. Asserts * the validation rules baked into [backend/src/handlers/admin.rs:patch_config]. */ import { test, expect } from '../../fixtures/test'; test.describe('Admin — config API', () => { test('PATCH /admin/config persists numeric values', async ({ api, adminToken }) => { await api.patchConfig(adminToken, { max_image_size_mb: '25' }); const cfg = await api.getConfig(adminToken); expect(cfg.max_image_size_mb).toBe('25'); // Restore the default so other specs see the seeded value. await api.patchConfig(adminToken, { max_image_size_mb: '20' }); }); test('non-numeric value for a numeric key is rejected', async ({ adminToken }) => { const res = await fetch( (process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/admin/config', { method: 'PATCH', headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ max_image_size_mb: 'not-a-number' }), } ); expect(res.status).toBe(400); }); test('unknown config key is rejected (whitelist enforced)', async ({ adminToken }) => { const res = await fetch( (process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/admin/config', { method: 'PATCH', headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ totally_fake_key: '1' }), } ); expect(res.status).toBe(400); }); test('toggle keys accept true/false but not arbitrary strings', async ({ api, adminToken }) => { await api.patchConfig(adminToken, { upload_rate_enabled: 'true' }); let cfg = await api.getConfig(adminToken); expect(cfg.upload_rate_enabled).toBe('true'); await api.patchConfig(adminToken, { upload_rate_enabled: 'false' }); cfg = await api.getConfig(adminToken); expect(cfg.upload_rate_enabled).toBe('false'); const res = await fetch( (process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/admin/config', { method: 'PATCH', headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ upload_rate_enabled: 'maybe' }), } ); expect(res.status).toBe(400); }); test('privacy_note round-trips verbatim, preserving whitespace + newlines', async ({ api, adminToken, }) => { const note = ' Datenschutz\n • Wir verwenden keine Cookies.\n • Alles bleibt im Browser.\n\n— Dein Host'; await api.patchConfig(adminToken, { privacy_note: note }); const cfg = await api.getConfig(adminToken); expect(cfg.privacy_note).toBe(note); await api.patchConfig(adminToken, { privacy_note: '' }); }); }); test.describe('Admin — stats', () => { test('GET /admin/stats returns matching counts after seeding users', async ({ api, adminToken, guest, }) => { await guest('Stat1'); await guest('Stat2'); await guest('Stat3'); const stats = await api.getStats(adminToken); // Deterministic after the per-test truncate: 3 seeded guests + the Admin account // (recreated by the adminToken fixture's login) = exactly 4. An exact assertion // catches undercount/overcount regressions a `>= 3` lower bound would miss. expect(stats.user_count).toBe(4); expect(typeof stats.disk_total_bytes).toBe('number'); }); });