/** * 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('quota_tolerance = 0 is rejected, with a pointer to the real off-switch', async ({ api, adminToken, }) => { // Zero is inside the documented 0–1 range and catastrophic: the per-user limit is // `free_disk * tolerance / active_uploaders`, so 0 refuses EVERY upload — mid-event, with // "Du hast dein Upload-Limit für dieses Event erreicht", which names the wrong cause // entirely. `storage_quota_enabled` is what an admin reaching for an off-switch wants. 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({ quota_tolerance: '0' }), } ); expect(res.status).toBe(400); expect( (await res.text()).toLowerCase(), 'the error must name the switch the admin actually wanted' ).toContain('speicher-quote'); // The value is untouched — validation fully precedes any write. expect((await api.getConfig(adminToken)).quota_tolerance).toBe('0.75'); }); test('a very small quota_tolerance is still accepted', async ({ api, adminToken }) => { // The mirror. Rejecting 0 must not become a floor: small tolerances are how a large disk is // throttled to a sensible per-guest ceiling, and how the quota specs steer it (~1e-5 on a // 174 GB volume). A floor of 0.01 would forbid real configurations to prevent one typo. await api.patchConfig(adminToken, { quota_tolerance: '0.00001' }); expect((await api.getConfig(adminToken)).quota_tolerance).toBe('0.00001'); await api.patchConfig(adminToken, { quota_tolerance: '0.75' }); }); }); 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'); }); });