grocery-app/frontend/tests/invite-link-management.spec.ts

304 lines
9.0 KiB
TypeScript

import { expect, test } from "@playwright/test";
function seedAuthStorage(page: import("@playwright/test").Page, role = "admin") {
return page.addInitScript((seedRole) => {
localStorage.setItem("token", "test-token");
localStorage.setItem("userId", "1");
localStorage.setItem("role", seedRole);
localStorage.setItem("username", "manager-user");
}, role);
}
async function mockConfig(page: import("@playwright/test").Page) {
await page.route("**/config", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
maxFileSizeMB: 20,
maxImageDimension: 800,
imageQuality: 85,
}),
});
});
}
test("join household modal accepts invite links but rejects legacy invite codes", async ({ page }) => {
await seedAuthStorage(page);
await mockConfig(page);
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
await page.route("**/api/invite-links/approval-token", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
link: {
token: "approval-token",
status: "ACTIVE",
viewerStatus: null,
active_policy: "APPROVAL_REQUIRED",
group_name: "Approval Home",
},
}),
});
});
await page.goto("/manage");
await page.getByRole("button", { name: "Join Household" }).click();
await page.getByLabel("Invite Link").fill("HABC123");
await page.getByRole("button", { name: "Open Invite" }).click();
await expect(page.getByText("Use a household invite link like /invite/abcd1234.")).toBeVisible();
await page.getByLabel("Invite Link").fill("/invite/approval-token");
await page.getByRole("button", { name: "Open Invite" }).click();
await expect(page).toHaveURL(/\/invite\/approval-token$/);
await expect(page.getByRole("heading", { name: "Join Approval Home" })).toBeVisible();
});
test("household management shows pending invite approvals and can approve them", async ({ page }) => {
await seedAuthStorage(page);
await mockConfig(page);
let members = [
{ id: 1, username: "manager-user", role: "owner" },
];
let pendingRequests = [
{
id: 41,
user_id: 7,
username: "pending-pal",
name: "Pending Pal",
display_name: "",
created_at: "2026-03-31T12:00:00.000Z",
status: "PENDING",
},
];
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Approval Home", role: "owner", invite_code: "ABCD1234" }]),
});
});
await page.route("**/households/1/members", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(members),
});
});
await page.route("**/api/groups/join-policy", async (route) => {
const request = route.request();
if (request.method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ joinPolicy: "APPROVAL_REQUIRED" }),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
});
await page.route("**/api/groups/invites", async (route) => {
const request = route.request();
if (request.method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
links: [
{
id: 9,
token: "invite-token-1234",
policy: "APPROVAL_REQUIRED",
single_use: false,
expires_at: "2030-01-01T00:00:00.000Z",
used_at: null,
revoked_at: null,
},
],
}),
});
return;
}
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify({ link: { id: 10, token: "new-token" } }),
});
});
await page.route("**/api/groups/join-requests", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ requests: pendingRequests }),
});
});
await page.route("**/api/groups/join-requests/decision", async (route) => {
const body = route.request().postDataJSON() as { requestId?: number; decision?: string };
if (body.requestId === 41 && body.decision === "APPROVE") {
pendingRequests = [];
members = [
...members,
{ id: 7, username: "pending-pal", role: "member" },
];
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
request: {
id: 41,
user_id: 7,
status: body.decision === "APPROVE" ? "APPROVED" : "DENIED",
},
}),
});
});
await page.goto("/manage?tab=household");
await expect(page.getByRole("heading", { name: "Invite Links" })).toBeVisible();
await expect(page.getByText("Pending Pal")).toBeVisible();
await expect(page.getByText("Invite Code")).toHaveCount(0);
await page.getByRole("button", { name: "Approve" }).click();
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Approved join request");
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Pending Pal");
await expect(page.getByText("No pending join requests right now.")).toBeVisible();
await expect(page.getByText("Members (2)")).toBeVisible();
});
test("household owner can transfer ownership from household settings", async ({ page }) => {
await seedAuthStorage(page, "owner");
await mockConfig(page);
let households = [{ id: 1, name: "Approval Home", role: "owner", invite_code: "ABCD1234" }];
let members = [
{ id: 1, username: "manager-user", role: "owner" },
{ id: 2, username: "nico-admin", role: "admin" },
];
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(households),
});
});
await page.route("**/households/1/members", async (route) => {
const request = route.request();
if (request.method() === "PATCH") {
const body = request.postDataJSON() as { role?: string };
if (body.role === "owner") {
households = [{ id: 1, name: "Approval Home", role: "admin", invite_code: "ABCD1234" }];
members = [
{ id: 1, username: "manager-user", role: "admin" },
{ id: 2, username: "nico-admin", role: "owner" },
];
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
message: "Household ownership transferred successfully",
member: { user_id: 2, role: body.role || "member" },
}),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(members),
});
});
await page.route("**/api/groups/join-policy", async (route) => {
const request = route.request();
if (request.method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ joinPolicy: "APPROVAL_REQUIRED" }),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
});
await page.route("**/api/groups/invites", async (route) => {
const request = route.request();
if (request.method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ links: [] }),
});
return;
}
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify({ link: { id: 10, token: "new-token" } }),
});
});
await page.route("**/api/groups/join-requests", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ requests: [] }),
});
});
page.on("dialog", async (dialog) => {
await dialog.accept();
});
await page.goto("/manage?tab=household");
await expect(page.getByRole("button", { name: "Make Owner" })).toBeVisible();
await page.getByRole("button", { name: "Make Owner" }).click();
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Transferred household ownership");
await expect(page.locator(".action-toast.action-toast-success")).toContainText("nico-admin");
await expect(page.getByText("👑 Owner")).toContainText("Owner");
await expect(page.getByText("🛠️ Admin")).toContainText("Admin");
await expect(page.getByRole("button", { name: "Make Owner" })).toHaveCount(0);
});