Zero is inside the documented 0–1 range and catastrophic. The per-user limit is `free_disk * tolerance / active_uploaders`, so a tolerance of 0 makes every limit 0 and refuses EVERY upload -- mid-event, with "Du hast dein Upload-Limit für dieses Event erreicht", an error naming the wrong cause entirely. An admin reaching for an off-switch wants `storage_quota_enabled`; the rejection now says so. Rejecting the value rather than raising the floor. A floor of 0.01 was the obvious fix and it is wrong: very small tolerances are legitimate -- they are how a large disk is throttled down to a sensible per-guest ceiling, and how the quota specs steer it (tolerance = target * active / free lands around 1e-5 on the 174 GB volume this suite runs on). A floor would forbid real configurations, and would have broken the entire storage-quota describe block, to prevent one typo. Verified: those four tests still pass. Tests: the rejection, that the stored value is untouched (validation fully precedes any write), and the mirror -- 0.00001 still round-trips -- so the guard can't quietly become a floor later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|
||
});
|