All checks were successful
CD / Build and push images (push) Successful in 3m17s
CI / Lint, typecheck, test (push) Successful in 2m34s
CI / Auth e2e pack (push) Successful in 3m27s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
Site Admins tune quotas per user and per pond on the three-level ladder (pond override → user override → instance default, data-model.md §Quotas). - api `admin/`: a `QuotaAdminService` + Site-Admin-gated endpoints under `/admin/quotas` — look up a user (username/e-mail) or pond (slug), list every quota's override / instance default / effective value (resolved through the existing QuotaService, the single consumption path, so a change takes effect immediately) plus current usage, and set/clear a per-subject override. Every change is audit-logged. A pond's effective values resolve on its own override then its owner's, matching the consumption checks. - web: the Admin area gains a 'Quotas' surface — the instance defaults move into a proper number-input form (was raw settings, #19), and a per-subject panel looks a user/pond up, shows the ladder with usage, flags subjects over their effective limit, and sets/clears overrides. New `quotas` i18n namespace (de+en). - tests: `quota-admin.e2e.db.test.ts` (override → effective changes at once and QuotaService sees it; clear → falls back to the default; lookup; Site-Admin gating); a browser `admin-quotas` pack proving an override raised in the UI immediately lets a user create another shared pond (issue #22 consumption). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
58 lines
2.5 KiB
TypeScript
58 lines
2.5 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import type { BrowserContext } from '@playwright/test';
|
|
|
|
import { contextForUser } from './helpers';
|
|
|
|
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
|
|
|
/**
|
|
* Site-Admin quota override management (issue #58): an override set through the
|
|
* admin UI takes effect immediately — a user at their `additional_ponds` limit
|
|
* can create one more shared pond once the override is raised above their
|
|
* current usage. Written to be independent of pre-existing state.
|
|
*/
|
|
async function userId(ctx: BrowserContext): Promise<string> {
|
|
return ((await (await ctx.request.get('/api/v1/auth/me')).json()) as { id: string }).id;
|
|
}
|
|
|
|
test('a quota override set in the admin UI takes effect immediately', async ({ browser }) => {
|
|
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
|
const user = await contextForUser(browser, BASE_URL, 'fixture-editor');
|
|
const uid = await userId(user);
|
|
const override = `/api/v1/admin/quotas/user/${uid}/additional_ponds`;
|
|
|
|
// Baseline: no override → the default (0) applies, so the user is at/over
|
|
// limit and cannot create another shared pond.
|
|
await admin.request.delete(override);
|
|
const owned = (
|
|
(await (await admin.request.get(`/api/v1/admin/quotas/user/${uid}`)).json()) as {
|
|
lines: { key: string; usage: number | null }[];
|
|
}
|
|
).lines.find((l) => l.key === 'additional_ponds')!.usage!;
|
|
const before = await user.request.post('/api/v1/ponds', { data: { name: `Q ${Date.now()}` } });
|
|
expect(before.status()).toBe(403);
|
|
|
|
const page = await admin.newPage();
|
|
await page.goto('/admin');
|
|
await page.locator('.quota-manager__type').selectOption('user');
|
|
await page.locator('.quota-manager__query').fill('fixture-editor');
|
|
await page.getByRole('button', { name: /find|suchen/i }).click();
|
|
|
|
const row = page.locator('.quota-row[data-key="additional_ponds"]');
|
|
await expect(row).toBeVisible();
|
|
await row.locator('.quota-row__input').fill(String(owned + 1)); // room for exactly one more
|
|
await row.getByRole('button', { name: /^(set|setzen)$/i }).click();
|
|
await expect(row.locator('.quota-row__effective')).toHaveText(String(owned + 1));
|
|
|
|
try {
|
|
const after = await user.request.post('/api/v1/ponds', { data: { name: `Q ${Date.now()}` } });
|
|
expect(after.status()).toBe(201);
|
|
const pond = (await after.json()) as { id: string };
|
|
await user.request.delete(`/api/v1/ponds/${pond.id}`);
|
|
} finally {
|
|
await admin.request.delete(override); // repeatable
|
|
await admin.close();
|
|
await user.close();
|
|
}
|
|
});
|