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>
139 lines
5.7 KiB
JavaScript
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);
|
|
}
|