Files
InkNote/e2e/admin.mjs
T
marcandClaude Opus 5 311e992496 Move the browser suites into the repository
They lived in a scratch directory under /tmp, which was cleared overnight;
four of the five were simply gone. Whatever they were worth, they were
worth nothing kept there.

Rewritten against a shared lib.mjs. The old ones registered their users
through the sign-up form, which no longer exists — each suite now creates
a throwaway account through the admin API and deletes it afterwards, so a
run leaves the database as it found it. The previous generation left nine
accounts behind; those are gone too.

Three test hooks went into the app, each of which earns its place beyond
testing: aria-label on the note title (the sidebar search box is also a
text input, and a screen reader could not tell them apart either),
data-note-row on list rows, data-user on admin rows.

Two suites were flaky in ways worth naming, because both were the test
misreading the app rather than the app misbehaving:
- ticking a task then inserting a note link put the link inside the task
  list, since Control+End lands there. Verified against the server that
  the tick itself persists correctly; the suite now leaves the list first
  and asserts where the link ended up.
- the note-title lookup matched the sidebar's Trash entry, which wears the
  same emoji as a note row.

56 checks across five suites, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 14:30:08 +02:00

139 lines
5.7 KiB
JavaScript

/* No public sign-up, and user administration.
* Checks the interface and, separately, that the server refuses on its own —
* a hidden button is not access control. */
import { BASE, OUT, api, check, launch, newPage, report, signIn } from "./lib.mjs";
const ADMIN = {
user: process.env.INKNOTE_ADMIN || "marc",
pass: process.env.INKNOTE_ADMIN_PASS || "geheim123",
};
const NEW = {
user: "kollege" + Math.floor(Math.random() * 1e6),
pass: "startpass1",
pass2: "zweitpass9",
};
const openAdmin = async (p) => {
await p.getByTitle("Account").click();
await p.waitForTimeout(500);
await p.getByRole("button", { name: "Administration" }).click();
await p.waitForTimeout(1200);
};
const browser = await launch();
try {
/* ── 1. sign-up is gone ── */
const p = await newPage(browser);
await p.goto(BASE, { waitUntil: "networkidle" });
await p.waitForTimeout(800);
const labels = await p.$$eval("button, a", (els) => els.map((e) => e.innerText.trim()).filter(Boolean));
check(!labels.some((t) => /sign ?up|create account/i.test(t)), "no sign-up control on the login screen");
await p.screenshot({ path: `${OUT}/admin-login.png` });
const reg = await p.evaluate(async () => {
const r = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "sneaky" + Date.now(), password: "hunter22" }),
});
return r.status;
});
check(reg === 404, "POST /api/auth/register is gone", "status " + reg);
/* ── 2. an admin can create ── */
await signIn(p, ADMIN.user, ADMIN.pass);
await openAdmin(p);
check((await p.locator("text=Administration").count()) > 0, "admin panel opens");
await p.getByRole("button", { name: "New account" }).click();
await p.waitForTimeout(400);
await p.getByPlaceholder("Username").fill(NEW.user);
await p.getByPlaceholder("Display name (optional)").fill("Kollege");
await p.getByPlaceholder("Initial password").fill(NEW.pass);
await p.getByRole("button", { name: "Create account" }).click();
await p.waitForTimeout(1800);
check((await p.locator(`[data-user="${NEW.user}"]`).count()) === 1, "created user appears in the list");
await p.screenshot({ path: `${OUT}/admin-panel.png` });
/* ── 3. the account works, and is not privileged ── */
const p2 = await newPage(browser);
await signIn(p2, NEW.user, NEW.pass);
check(true, "created user can sign in");
const seesAdmin = await p2.evaluate(async () => {
document.querySelector('[title="Account"]')?.click();
await new Promise((r) => setTimeout(r, 600));
return [...document.querySelectorAll("button")].some((b) => /Administration/.test(b.innerText));
});
check(!seesAdmin, "ordinary user sees no Administration entry");
check((await api(p2, "GET", "/admin/users")).status === 403, "GET /admin/users refused for non-admin");
check(
(await api(p2, "POST", "/admin/users", { username: "elev" + Date.now(), password: "hunter22" })).status ===
403,
"POST /admin/users refused for non-admin"
);
/* ── 4. password reset ── */
await p.reload({ waitUntil: "networkidle" });
await p.waitForTimeout(1500);
await openAdmin(p);
const row = p.locator(`[data-user="${NEW.user}"]`);
await row.getByRole("button", { name: "Reset password" }).click();
await p.waitForTimeout(400);
await row.getByPlaceholder(`New password for ${NEW.user}`).fill(NEW.pass2);
await row.getByRole("button", { name: "Set password", exact: true }).click();
await p.waitForTimeout(1500);
const p3 = await newPage(browser);
await p3.goto(BASE, { waitUntil: "networkidle" });
await p3.getByPlaceholder("Username").fill(NEW.user);
await p3.getByPlaceholder("Password").fill(NEW.pass);
await p3.getByRole("button", { name: "Sign In" }).click();
await p3.waitForTimeout(1500);
check((await p3.locator("text=Invalid credentials").count()) > 0, "the old password stopped working");
await p3.getByPlaceholder("Password").fill(NEW.pass2);
await p3.getByRole("button", { name: "Sign In" }).click();
await p3.waitForSelector("text=Dashboard", { timeout: 20000 });
check(true, "the new password works");
/* ── 5. deletion, and its guards ── */
await p.reload({ waitUntil: "networkidle" });
await p.waitForTimeout(1500);
await openAdmin(p);
const me = await api(p, "GET", "/auth/me");
check(
(await api(p, "DELETE", `/admin/users/${me.body.id}`)).status === 400,
"the server refuses an admin deleting themselves"
);
check(
(await p.locator(`[data-user="${ADMIN.user}"] button[title^="Delete"]`).count()) === 0,
"no delete button on the admin's own row"
);
await p.locator(`button[title="Delete ${NEW.user}"]`).click();
await p.waitForTimeout(500);
check(
await p.getByRole("button", { name: "Delete permanently" }).isDisabled(),
"delete stays disabled until the name is typed back"
);
await p.getByPlaceholder(NEW.user).fill(NEW.user);
await p.waitForTimeout(300);
await p.getByRole("button", { name: "Delete permanently" }).click();
await p.waitForTimeout(1800);
// The success banner repeats the name, so match the row itself, not any text.
check((await p.locator(`[data-user="${NEW.user}"]`).count()) === 0, "the user is gone from the list");
const p4 = await newPage(browser);
await p4.goto(BASE, { waitUntil: "networkidle" });
await p4.getByPlaceholder("Username").fill(NEW.user);
await p4.getByPlaceholder("Password").fill(NEW.pass2);
await p4.getByRole("button", { name: "Sign In" }).click();
await p4.waitForTimeout(1500);
check((await p4.locator("text=Invalid credentials").count()) > 0, "the deleted user cannot sign in");
} catch (e) {
check(false, "suite ran to the end", e.message);
} finally {
const code = report();
await browser.close();
process.exit(code);
}