grocery-app/frontend/tests/invite-link-management.spec.ts
Nico a2c08aff45
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m36s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 2s
Build & Deploy Costco Grocery List / deploy (push) Successful in 8s
Build & Deploy Costco Grocery List / notify (push) Successful in 0s
chore: harden reliability checks
2026-05-25 16:20:35 -07:00

294 lines
8.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
collectFailedApiRequests,
confirmSlide,
expectNoFailedApiRequests,
mockConfig,
seedAuthStorage,
} from "./helpers/e2e";
test("join household modal accepts invite links but rejects legacy invite codes", async ({ page }) => {
await seedAuthStorage(page, { username: "manager-user" });
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/approvaltoken", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
link: {
token: "approvaltoken",
status: "ACTIVE",
viewerStatus: null,
active_policy: "APPROVAL_REQUIRED",
group_name: "Approval Home",
},
}),
});
});
await page.goto("/manage");
await page.getByRole("button", { name: "Join Household", exact: true }).click();
await page.getByLabel("Invite Link").fill("HABC123");
await page.getByRole("button", { name: "Open Invite" }).click();
await expect(page.locator(".create-join-modal .error-message")).toHaveText(
"Use a household invite link like /invite/abcd1234."
);
await page.getByLabel("Invite Link").fill("/invite/approvaltoken");
await page.getByRole("button", { name: "Open Invite" }).click();
await expect(page).toHaveURL(/\/invite\/approvaltoken$/);
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, { username: "manager-user" });
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 }) => {
const failedApiRequests = collectFailedApiRequests(page);
await seedAuthStorage(page, { role: "owner", username: "manager-user" });
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) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(members),
});
});
await page.route("**/households/1/members/*/role", async (route) => {
const request = route.request();
if (request.method() !== "PATCH") {
await route.fulfill({ status: 405 });
return;
}
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" },
}),
});
});
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: [] }),
});
});
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.getByText("Transfer ownership to nico-admin?")).toBeVisible();
await confirmSlide(page);
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);
expectNoFailedApiRequests(failedApiRequests);
});