Compare commits
6 Commits
b459b99fe9
...
chore/ui-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42e273479 | ||
|
|
f32ed73561 | ||
|
|
64799b73ff | ||
|
|
beb3bcb97c | ||
|
|
79c8db2cb7 | ||
|
|
f4cd883d76 |
@@ -325,9 +325,11 @@ async fn admin_is_implicit_app_admin_on_every_app(pool: PgPool) {
|
||||
.await
|
||||
.assert_status_ok();
|
||||
|
||||
// Allowed: delete the default app (AppAdmin).
|
||||
// Allowed: delete the default app (AppAdmin). ?force=true because
|
||||
// the script we created above pushes us past the soft no-cascade
|
||||
// guard — this test is about the capability, not the cascade.
|
||||
s.server
|
||||
.delete("/api/v1/admin/apps/default")
|
||||
.delete("/api/v1/admin/apps/default?force=true")
|
||||
.add_header("authorization", format!("Bearer {token}"))
|
||||
.await
|
||||
.assert_status(axum::http::StatusCode::NO_CONTENT);
|
||||
|
||||
@@ -3,28 +3,40 @@ import { adminApi } from './api';
|
||||
|
||||
// Resources to delete after a test, in LIFO order. Tests register
|
||||
// their creations and the registry tears everything down in
|
||||
// `cleanupRegistered` — typically called from `test.afterEach`.
|
||||
// `run()` — typically called from `test.afterEach`.
|
||||
//
|
||||
// A non-2xx status (other than 404) is treated as a real failure and
|
||||
// logged to stderr. The previous shape silently swallowed every
|
||||
// error, so a backend that started returning 500 on cleanup would
|
||||
// have leaked orphans invisibly across runs. 404 stays tolerated —
|
||||
// the test may have already deleted the resource itself.
|
||||
|
||||
type Cleanup = (api: APIRequestContext) => Promise<void>;
|
||||
interface CleanupItem {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class CleanupRegistry {
|
||||
private items: Cleanup[] = [];
|
||||
private items: CleanupItem[] = [];
|
||||
|
||||
app(slugOrId: string): void {
|
||||
this.items.push(async (api) => {
|
||||
await api.delete(`/api/v1/admin/apps/${encodeURIComponent(slugOrId)}?force=true`);
|
||||
this.items.push({
|
||||
label: `app=${slugOrId}`,
|
||||
path: `/api/v1/admin/apps/${encodeURIComponent(slugOrId)}?force=true`
|
||||
});
|
||||
}
|
||||
|
||||
adminUser(userId: string): void {
|
||||
this.items.push(async (api) => {
|
||||
await api.delete(`/api/v1/admin/admins/${userId}`);
|
||||
this.items.push({
|
||||
label: `admin=${userId}`,
|
||||
path: `/api/v1/admin/admins/${userId}`
|
||||
});
|
||||
}
|
||||
|
||||
apiKey(keyId: string): void {
|
||||
this.items.push(async (api) => {
|
||||
await api.delete(`/api/v1/admin/api-keys/${keyId}`);
|
||||
this.items.push({
|
||||
label: `key=${keyId}`,
|
||||
path: `/api/v1/admin/api-keys/${keyId}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,13 +44,11 @@ export class CleanupRegistry {
|
||||
if (this.items.length === 0) return;
|
||||
const api = await adminApi();
|
||||
try {
|
||||
for (const item of this.items.reverse()) {
|
||||
try {
|
||||
await item(api);
|
||||
} catch {
|
||||
// Best-effort cleanup — a missing resource (already
|
||||
// deleted by the test) shouldn't fail the suite.
|
||||
}
|
||||
// Copy-then-reverse so a defensive double-`run()` (or a
|
||||
// caller that inspects the registry after a partial
|
||||
// teardown) doesn't see the items in a re-reversed order.
|
||||
for (const item of [...this.items].reverse()) {
|
||||
await deleteAndReport(api, item);
|
||||
}
|
||||
} finally {
|
||||
await api.dispose();
|
||||
@@ -46,3 +56,22 @@ export class CleanupRegistry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAndReport(
|
||||
api: APIRequestContext,
|
||||
item: CleanupItem
|
||||
): Promise<void> {
|
||||
try {
|
||||
const res = await api.delete(item.path);
|
||||
// 2xx and 404 are both "this resource is no longer here" — fine.
|
||||
if (!res.ok() && res.status() !== 404) {
|
||||
console.warn(
|
||||
`[cleanup] ${item.label} failed: HTTP ${res.status()} ${await res.text()}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
// Network-level failure (request never reached the server,
|
||||
// timeout, etc.). Log so a leak doesn't accumulate silently.
|
||||
console.warn(`[cleanup] ${item.label} failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +87,12 @@ test('end-to-end: app + domain + script + route via dashboard → invoke via pub
|
||||
});
|
||||
|
||||
test('api key minted via dashboard works as a CLI bearer, then revoke disables it', async ({
|
||||
page
|
||||
page,
|
||||
uniqueUsername
|
||||
}) => {
|
||||
const name = `e2e-cli-${Date.now()}`;
|
||||
// Worker-aware unique helper instead of Date.now() — keeps two
|
||||
// workers from minting the same name on the same millisecond.
|
||||
const name = uniqueUsername('cli');
|
||||
|
||||
// 1. Mint the key from /profile and capture the revealed token.
|
||||
await page.goto('/admin/profile');
|
||||
|
||||
@@ -114,7 +114,7 @@ test.describe('B6 instance users', () => {
|
||||
await expect(page.locator('.row', { hasText: decoy })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('deactivate then reactivate toggles the inactive indicator', async ({
|
||||
test('deactivate confirm modal: Cancel keeps active, Deactivate flips, reactivate is one click', async ({
|
||||
page,
|
||||
uniqueUsername
|
||||
}) => {
|
||||
@@ -127,10 +127,25 @@ test.describe('B6 instance users', () => {
|
||||
const row = page.locator('.row:not(.head-row):not(.empty-row)', { hasText: username });
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
// Deactivate opens the confirm modal.
|
||||
await row.getByRole('button', { name: new RegExp(`User actions for ${username}`) }).click();
|
||||
await page.getByRole('menuitem', { name: /^Deactivate$/ }).click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog).toContainText(username);
|
||||
|
||||
// Cancel leaves the user active.
|
||||
await dialog.getByRole('button', { name: /^Cancel$/ }).click();
|
||||
await expect(dialog).toHaveCount(0);
|
||||
await expect(row).not.toContainText(/inactive/i);
|
||||
|
||||
// Open again and confirm — user becomes inactive.
|
||||
await row.getByRole('button', { name: new RegExp(`User actions for ${username}`) }).click();
|
||||
await page.getByRole('menuitem', { name: /^Deactivate$/ }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: /^Deactivate$/ }).click();
|
||||
await expect(row).toContainText(/inactive/i);
|
||||
|
||||
// Reactivate is still one-click (non-destructive — no modal).
|
||||
await row.getByRole('button', { name: new RegExp(`User actions for ${username}`) }).click();
|
||||
await page.getByRole('menuitem', { name: /^Reactivate$/ }).click();
|
||||
await expect(row).not.toContainText(/inactive/i);
|
||||
|
||||
@@ -61,6 +61,7 @@ services:
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PICLOUD_HOST_PORT:-8000}:80"
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user