e2e/workspace-access.spec.ts
Rama main · réplica de lectura del repositorio original.
import { navigateWorkspace } from "./workspace-controls";
import { expect, test, type Page } from "@playwright/test";
const LAN_WARNING = "Anyone who can reach this BenchLedger address can view inventory, change records, and change workspace security settings. Use LAN-open mode only on a trusted network. Enable a password before using guest Wi-Fi, port forwarding, internet exposure, or a public reverse proxy.";
type AccessMode = "lan_open" | "password";
function inventoryItem(id: string, name: string) {
return {
id, name, kind: "tool", quantity: 1, availableQuantity: 1, unit: "each", tags: [], links: [],
evidence: { state: "physically_counted" }, createdAt: "2026-09-01T10:00:00.000Z", updatedAt: "2026-09-01T10:00:00.000Z", version: 1
};
}
async function mockWorkspaceAccess(page: Page) {
let mode: AccessMode = "lan_open";
let version = 1;
let workspaceCredential = "old-password-please";
let signedIn = false;
let itemCount = 0;
let lanSessionCount = 0;
let loseNextResponse = false;
let malformedNextResponse = false;
const accessRequestKeys: string[] = [];
const idempotentResponses = new Map<string, { body: string; response: string }>();
let currentItems = [inventoryItem("tool-1", "Digital caliper")];
await page.route("**/api/v1/auth/access", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ mode, passwordConfigured: mode === "password", version }) });
return;
}
const body = route.request().postDataJSON() as { operation?: "enable" | "disable" | "change_password"; expectedVersion?: number; currentPassword?: string; newPassword?: string };
const idempotencyKey = route.request().headers()["idempotency-key"] ?? "";
accessRequestKeys.push(idempotencyKey);
const requestBody = JSON.stringify(body);
const replay = idempotentResponses.get(idempotencyKey);
if (replay) {
if (replay.body !== requestBody) {
await route.fulfill({ status: 409, contentType: "application/json", body: JSON.stringify({ error: { code: "idempotency_conflict", message: "The request key was already used for a different change." } }) });
return;
}
signedIn = true;
await route.fulfill({ status: 200, contentType: "application/json", body: replay.response });
return;
}
const expectedVersion = Number(route.request().headers()["if-match"]);
if (expectedVersion !== version || body.expectedVersion !== version) {
await route.fulfill({ status: 409, contentType: "application/json", body: JSON.stringify({ error: { code: "version_conflict", message: "Workspace settings changed elsewhere." } }) });
return;
}
if (body.operation !== "enable" && body.operation !== "disable" && body.operation !== "change_password") {
await route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: { code: "invalid_operation", message: "A canonical workspace security operation is required." } }) });
return;
}
if ((body.operation === "disable" || body.operation === "change_password") && body.currentPassword !== workspaceCredential) {
await route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ error: { code: "invalid_credentials", message: "The current password is not correct." } }) });
return;
}
if ((body.operation === "enable" || body.operation === "change_password") && !body.newPassword) {
await route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: { code: "invalid_password", message: "A new password is required." } }) });
return;
}
if (body.newPassword) workspaceCredential = body.newPassword;
mode = body.operation === "disable" ? "lan_open" : "password";
version += 1;
signedIn = true;
const responseBody = JSON.stringify({ mode, passwordConfigured: mode === "password", version, authenticated: true, actor: "admin", csrfToken: `csrf-${version}`, expiresAt: "2026-09-01T18:00:00.000Z" });
idempotentResponses.set(idempotencyKey, { body: requestBody, response: responseBody });
if (malformedNextResponse) {
malformedNextResponse = false;
signedIn = false;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ access: { mode, passwordConfigured: mode === "password", version }, session: { authenticated: true } }) });
return;
}
if (loseNextResponse) {
loseNextResponse = false;
signedIn = false;
await route.abort("failed");
return;
}
await route.fulfill({ status: 200, contentType: "application/json", body: responseBody });
});
await page.route("**/api/v1/auth/lan-session", async (route) => {
lanSessionCount += 1;
signedIn = true;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "admin", csrfToken: "csrf-lan", expiresAt: "2026-09-01T18:00:00.000Z" }) });
});
await page.route("**/api/v1/auth/login", async (route) => {
const body = route.request().postDataJSON() as { password?: string };
if (body.password !== workspaceCredential) {
await route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ error: { code: "invalid_credentials", message: "Email or password is invalid" } }) });
return;
}
signedIn = true;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "admin", csrfToken: "csrf-login", expiresAt: "2026-09-01T18:00:00.000Z" }) });
});
await page.route("**/api/v1/auth/session", async (route) => {
if (!signedIn) {
await route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ error: { code: "unauthenticated", message: "Authentication is required" } }) });
return;
}
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: true, actor: "admin", source: "ui", scopes: ["read", "write"] }) });
});
await page.route("**/api/v1/auth/logout", async (route) => {
signedIn = false;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: false }) });
});
await page.route("**/api/v1/health", async (route) => {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ status: "ok", service: "benchledger", version: "test", demo: false, now: "2026-09-01T10:00:00.000Z" }) });
});
await page.route("**/api/v1/workspace", async (route) => {
await route.fulfill({ status: signedIn ? 200 : 401, contentType: "application/json", body: JSON.stringify(signedIn ? { source: "api", fetchedAt: "2026-09-01T10:00:00.000Z", inventory: currentItems, projects: [], offers: [] } : { error: { code: "unauthenticated", message: "Authentication is required" } }) });
});
await page.route("**/api/v1/projects?*", async (route) => {
if (route.request().method() !== "GET") return route.continue();
await route.fulfill({ status: signedIn ? 200 : 401, contentType: "application/json", body: JSON.stringify(signedIn ? { data: [], limit: 200, total: 0 } : { error: { code: "unauthenticated", message: "Authentication is required" } }) });
});
await page.route("**/api/v1/inventory?*", async (route) => {
if (route.request().method() !== "GET") return route.continue();
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: currentItems, limit: 25, total: currentItems.length }) });
});
await page.route("**/api/v1/inventory/categories*", async (route) => {
if (route.request().method() !== "GET") return route.continue();
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: [{ id: "category-tools", name: "Tools", sortOrder: 0, archived: false, createdAt: "2026-09-01T10:00:00.000Z", updatedAt: "2026-09-01T10:00:00.000Z", version: 1 }],
limit: 200,
total: 1
})
});
});
await page.route("**/api/v1/inventory", async (route) => {
if (route.request().method() !== "POST") return route.continue();
if (!signedIn) {
await route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ error: { code: "unauthenticated", message: "Authentication is required" } }) });
return;
}
const body = route.request().postDataJSON() as { name: string; quantity: number; unit: string };
itemCount += 1;
const item = inventoryItem(`new-tool-${itemCount}`, body.name);
currentItems = [item, ...currentItems];
await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ data: item }) });
});
return {
setPasswordRequired: () => { signedIn = false; },
loseNextAccessResponse: () => { loseNextResponse = true; },
malformedNextAccessResponse: () => { malformedNextResponse = true; },
state: () => ({ mode, version, signedIn, credential: workspaceCredential, itemCount, lanSessionCount, accessRequestKeys: [...accessRequestKeys] })
};
}
test("completes the LAN-open to password and back access journey without exposing secrets", async ({ page }) => {
const harness = await mockWorkspaceAccess(page);
await page.goto("/");
await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Open workspace settings" }).click();
await expect(page.getByRole("heading", { name: "Workspace access" })).toBeVisible();
await expect(page.getByText("LAN open", { exact: true })).toBeVisible();
await expect(page.getByRole("alert").filter({ hasText: LAN_WARNING })).toContainText(LAN_WARNING);
await expect(page.getByRole("button", { name: "Sign out" })).toHaveCount(0);
await page.getByLabel("New workspace password", { exact: true }).fill("new-password-please");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("new-password-please");
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("Password protection is enabled.")).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "password", version: 2, signedIn: true });
harness.setPasswordRequired();
await page.reload();
await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
await expect(page.getByLabel("Workspace password")).toBeVisible();
await page.getByLabel("Workspace password").fill("new-password-please");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible();
await page.getByLabel("Current workspace password", { exact: true }).fill("new-password-please");
await page.getByLabel("New workspace password", { exact: true }).fill("replacement-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("replacement-password");
await page.getByRole("button", { name: "Change password" }).click();
await expect(page.getByText("Password changed. Your current session remains active.")).toBeVisible();
await expect(page.getByLabel("Current workspace password", { exact: true })).toHaveValue("");
await expect(page.getByLabel("New workspace password", { exact: true })).toHaveValue("");
await expect(page.getByLabel("Confirm new workspace password", { exact: true })).toHaveValue("");
await expect(page.getByLabel("Current workspace password to disable protection", { exact: true })).toHaveValue("");
await page.getByLabel("Current workspace password to disable protection").fill("replacement-password");
await page.getByRole("button", { name: "Disable password protection" }).click();
await expect(page.getByText("Password protection is disabled. BenchLedger is LAN open.")).toBeVisible();
await expect(page.getByText("LAN open", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Sign out" })).toHaveCount(0);
await navigateWorkspace(page, "Inventory");
await page.getByRole("button", { name: "Add item", exact: true }).click();
await page.getByLabel("What are you adding?").selectOption("tool");
await expect(page.getByLabel("Category (required)")).toHaveCount(0);
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByLabel("Name", { exact: true }).fill("LAN-open write check");
await page.getByRole("button", { name: "Add item", exact: true }).last().click();
await expect(page.getByRole("dialog").getByRole("heading", { name: "LAN-open write check", exact: true })).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "lan_open", itemCount: 1 });
await expect(page.locator("body")).not.toContainText("replacement-password");
});
test("keeps the access warning and password controls usable on mobile", async ({ page }) => {
await mockWorkspaceAccess(page);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await page.getByRole("button", { name: "Open workspace settings" }).click();
await expect(page.getByRole("heading", { name: "Workspace access" })).toBeVisible();
await expect(page.getByRole("alert").filter({ hasText: LAN_WARNING })).toHaveText(LAN_WARNING);
await expect(page.getByLabel("New workspace password", { exact: true })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
await expect(page.getByRole("button", { name: "Enable password" })).toBeVisible();
});
test("retries a malformed access response immediately with the same request key", async ({ page }) => {
const harness = await mockWorkspaceAccess(page);
await page.goto("/");
await page.getByRole("button", { name: "Open workspace settings" }).click();
await page.getByLabel("New workspace password", { exact: true }).fill("malformed-response-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("malformed-response-password");
harness.malformedNextAccessResponse();
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("We could not confirm the change. It may have been saved; reload settings before trying again.")).toBeVisible();
await expect(page.getByText("A previous request was not confirmed.")).toBeVisible();
const firstKey = harness.state().accessRequestKeys.at(-1);
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("Password protection is enabled.")).toBeVisible();
expect(harness.state().accessRequestKeys.at(-1)).toBe(firstKey);
await expect(page.locator("body")).not.toContainText("malformed-response-password");
});
test("retries a lost access response immediately with the same request key", async ({ page }) => {
const harness = await mockWorkspaceAccess(page);
await page.goto("/");
await page.getByRole("button", { name: "Open workspace settings" }).click();
await page.getByLabel("New workspace password", { exact: true }).fill("immediate-retry-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("immediate-retry-password");
harness.loseNextAccessResponse();
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("We could not confirm the change. It may have been saved; reload settings before trying again.")).toBeVisible();
const firstKey = harness.state().accessRequestKeys.at(-1);
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("Password protection is enabled.")).toBeVisible();
expect(harness.state().accessRequestKeys.at(-1)).toBe(firstKey);
await expect(page.locator("body")).not.toContainText("immediate-retry-password");
});
test("replays a lost enable response after reauthentication without retaining the password", async ({ page }) => {
const harness = await mockWorkspaceAccess(page);
await page.goto("/");
await page.getByRole("button", { name: "Open workspace settings" }).click();
await page.getByLabel("New workspace password", { exact: true }).fill("lost-enable-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("lost-enable-password");
harness.loseNextAccessResponse();
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("We could not confirm the change. It may have been saved; reload settings before trying again.")).toBeVisible();
await expect(page.getByText("A previous request was not confirmed.")).toBeVisible();
const storedBeforeReload = await page.evaluate(() => sessionStorage.getItem("benchledger:workspace-access-retry:v1"));
expect(storedBeforeReload).not.toContain("lost-enable-password");
expect(harness.state()).toMatchObject({ mode: "password", version: 2, signedIn: false });
await page.reload();
await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
await page.getByLabel("Workspace password").fill("lost-enable-password");
await page.getByRole("button", { name: "Sign in" }).click();
await page.getByRole("button", { name: "Open workspace settings" }).click();
await expect(page.getByRole("form", { name: "Retry enabling workspace password" })).toBeVisible();
await page.getByLabel("New workspace password", { exact: true }).fill("lost-enable-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("lost-enable-password");
await page.getByRole("button", { name: "Retry enable" }).click();
await expect(page.getByText("Password protection is enabled.")).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "password", version: 2, signedIn: true });
await expect(page.locator("body")).not.toContainText("lost-enable-password");
});
test("replays a lost password change after logging in with the replacement", async ({ page }) => {
const harness = await mockWorkspaceAccess(page);
await page.goto("/");
await page.getByRole("button", { name: "Open workspace settings" }).click();
await page.getByLabel("New workspace password", { exact: true }).fill("first-change-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("first-change-password");
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("Password protection is enabled.")).toBeVisible();
await page.getByLabel("Current workspace password", { exact: true }).fill("first-change-password");
await page.getByLabel("New workspace password", { exact: true }).fill("second-change-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("second-change-password");
harness.loseNextAccessResponse();
await page.getByRole("button", { name: "Change password" }).click();
await expect(page.getByText("We could not confirm the change. It may have been saved; reload settings before trying again.")).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "password", version: 3, signedIn: false });
await page.reload();
await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
await page.getByLabel("Workspace password").fill("second-change-password");
await page.getByRole("button", { name: "Sign in" }).click();
await page.getByRole("button", { name: "Open workspace settings" }).click();
await page.getByLabel("Current workspace password", { exact: true }).fill("first-change-password");
await page.getByLabel("New workspace password", { exact: true }).fill("second-change-password");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("second-change-password");
await page.getByRole("button", { name: "Change password" }).click();
await expect(page.getByText("Password changed. Your current session remains active.")).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "password", version: 3, signedIn: true, credential: "second-change-password" });
await expect(page.locator("body")).not.toContainText("second-change-password");
});
test("replays a lost disable response after LAN bootstrap", async ({ page }) => {
const harness = await mockWorkspaceAccess(page);
await page.goto("/");
await page.getByRole("button", { name: "Open workspace settings" }).click();
await page.getByLabel("New workspace password", { exact: true }).fill("disable-password-please");
await page.getByLabel("Confirm new workspace password", { exact: true }).fill("disable-password-please");
await page.getByRole("button", { name: "Enable password" }).click();
await expect(page.getByText("Password protection is enabled.")).toBeVisible();
await page.getByLabel("Current workspace password to disable protection", { exact: true }).fill("disable-password-please");
harness.loseNextAccessResponse();
await page.getByRole("button", { name: "Disable password protection" }).click();
await expect(page.getByText("We could not confirm the change. It may have been saved; reload settings before trying again.")).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "lan_open", version: 3, signedIn: false });
await page.reload();
await expect(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible();
expect(harness.state().lanSessionCount).toBeGreaterThanOrEqual(2);
await expect(page.getByRole("form", { name: "Enable workspace password" })).toHaveCount(0);
await expect(page.getByLabel("Current workspace password to retry disabling protection")).toBeVisible();
await page.getByLabel("Current workspace password to retry disabling protection").fill("disable-password-please");
await page.getByRole("button", { name: "Retry disable" }).click();
await expect(page.getByText("Password protection is disabled. BenchLedger is LAN open.")).toBeVisible();
expect(harness.state()).toMatchObject({ mode: "lan_open", version: 3, signedIn: true });
await expect(page.getByRole("button", { name: "Sign out" })).toHaveCount(0);
await expect(page.locator("body")).not.toContainText("disable-password-please");
});