import { APIRequestContext, expect, Page, test } from "@playwright/test"; import { captureBrowserFailures, gotoLiveView, latestMessageID, loginWithMagicLink, loginWithPassword, newIsolatedContext, projectEmail, registerAndConfirm, waitForApplicationEmailLink, } from "./helpers"; async function waitForNewEmail( request: APIRequestContext, email: string, previousMessageID: string | undefined, expectedText: string, ): Promise { let messageID: string | undefined; await expect .poll( async () => { messageID = await latestMessageID(request, email); return messageID && messageID !== previousMessageID ? messageID : undefined; }, { message: `waiting for a new application email for ${email}`, timeout: 15_000, }, ) .toBeTruthy(); const response = await request.get( `${process.env.MAILPIT_URL}/api/v1/message/${messageID}`, ); expect(response.ok()).toBeTruthy(); const message = (await response.json()) as { Text?: string; HTML?: string }; expect(`${message.Text ?? ""}\n${message.HTML ?? ""}`).toContain( expectedText, ); } async function submitAuthenticatedSupportRequest( page: Page, requesterEmail: string, kind: "privacy_request" | "data_export", subject: string, details: string, ): Promise { await page.goto("/support"); await page.getByLabel("What do you need help with?").selectOption(kind); await expect(page.getByLabel("Contact email")).toHaveValue(requesterEmail); await expect(page.getByLabel("Contact email")).toHaveAttribute( "readonly", "", ); await page.getByLabel("Subject").fill(subject); await page.getByLabel("Describe the problem").fill(details); await page.getByRole("button", { name: "Send support request" }).click(); await expect(page).toHaveURL(/\/support\/received\?reference=SUP-/); await expect( page.getByRole("heading", { name: "Support request created" }), ).toBeVisible(); } test("support confirmation explains a missing protected fragment", async ({ browser, }) => { const context = await newIsolatedContext(browser); const page = await context.newPage(); const assertBrowserClean = captureBrowserFailures(page); await page.goto( "/support/cases/00000000-0000-4000-8000-000000000001/verify", ); await expect(page.locator("#support-confirmation-fragment-form")).toBeHidden(); await expect( page.locator("#support-confirmation-fragment-invalid"), ).toBeVisible(); await expect( page.getByText("The link is invalid or it has expired."), ).toBeVisible(); await expect(page.getByRole("link", { name: "Back to support" })).toHaveAttribute( "href", "/support", ); assertBrowserClean(); await context.close(); }); test("content-removal confirmation explains a missing protected fragment", async ({ browser, }) => { const context = await newIsolatedContext(browser); const page = await context.newPage(); const assertBrowserClean = captureBrowserFailures(page); await page.goto( "/legal/content-removal/00000000-0000-4000-8000-000000000002/verify", ); await expect( page.locator("#content-removal-confirmation-fragment-form"), ).toBeHidden(); await expect( page.locator("#content-removal-confirmation-fragment-invalid"), ).toBeVisible(); await expect( page.getByText("The link is invalid or it has expired."), ).toBeVisible(); await expect( page .locator("#content-removal-confirmation-fragment-invalid") .getByRole("link", { name: "Report content" }), ).toHaveAttribute("href", "/legal/content-removal"); assertBrowserClean(); await context.close(); }); test("anonymous support confirmation opens from the protected email fragment", async ({ browser, request, }, testInfo) => { const email = projectEmail("anonymous-support", testInfo.project.name); const subject = `Anonymous support confirmation [${testInfo.project.name}]`; const context = await newIsolatedContext(browser); const page = await context.newPage(); const assertBrowserClean = captureBrowserFailures(page); const previousMessageID = await latestMessageID(request, email); await page.goto("/support"); await page .getByLabel("What do you need help with?") .selectOption("technical_issue"); await page.getByLabel("Contact email").fill(email); await page.getByLabel("Subject").fill(subject); await page .getByLabel("Describe the problem") .fill("Browser E2E verifies the protected Phoenix.Token fragment before support sees the request."); await page.getByRole("button", { name: "Send support request" }).click(); await expect(page).toHaveURL(/\/support\/received\?reference=SUP-/); await expect(page.getByRole("heading", { name: "Check your email" })).toBeVisible(); const confirmationLink = await waitForApplicationEmailLink( request, email, "/support/cases/", previousMessageID, ); const confirmationURL = new URL(confirmationLink); expect(confirmationURL.pathname).toMatch(/\/support\/cases\/[0-9a-f-]+\/verify$/); expect(confirmationURL.search).toBe(""); expect(confirmationURL.hash).toMatch(/^#token=SFMyNTY\./); await page.goto(confirmationLink); const confirmationForm = page.locator("#support-confirmation-fragment-form"); await expect(confirmationForm).toBeVisible(); await expect( page.locator("#support-confirmation-fragment-invalid"), ).toBeHidden(); await expect(page.locator("#support-confirmation-fragment-token")).toHaveValue( /^SFMyNTY\./, ); await confirmationForm .getByRole("button", { name: "Confirm support request" }) .click(); await expect(page).toHaveURL(/\/support\/cases\/[0-9a-f-]+\?token=/); await expect( page.getByText("Your email was confirmed and the request was sent to support."), ).toBeVisible(); await expect(page.getByRole("heading", { name: subject })).toBeVisible(); assertBrowserClean(); await context.close(); }); test("anonymous content-removal confirmation opens from the protected email fragment", async ({ browser, request, }, testInfo) => { const email = projectEmail("anonymous-removal", testInfo.project.name); const context = await newIsolatedContext(browser); const page = await context.newPage(); const assertBrowserClean = captureBrowserFailures(page); const previousMessageID = await latestMessageID(request, email); await page.goto("/legal/content-removal"); await page.getByLabel("Reason").selectOption("privacy_violation"); await page .getByLabel("Your name or organisation") .fill("Anonymous removal E2E"); await page.getByLabel("Contact email").fill(email); await page .getByLabel("Your relationship to the affected person or rights holder") .selectOption("self"); await page .getByLabel("Exact content URLs — one per line") .fill(`${process.env.BASE_URL}/requests/anonymous-removal-e2e`); await page .getByLabel("Why do you believe this content should be removed?") .fill("Browser E2E verifies explicit confirmation before legal review."); await page .getByLabel("Electronic signature (type your full name)") .fill("Anonymous removal E2E"); await page .getByLabel(/I believe in good faith that the identified content/) .check(); await page .getByLabel(/I confirm that this notice is accurate and complete/) .check(); await page.getByRole("button", { name: "Submit removal notice" }).click(); await expect(page).toHaveURL( /\/legal\/content-removal\/received\?reference=REM-/, ); const confirmationLink = await waitForApplicationEmailLink( request, email, "/legal/content-removal/", previousMessageID, ); const confirmationURL = new URL(confirmationLink); expect(confirmationURL.pathname).toMatch( /\/legal\/content-removal\/[0-9a-f-]+\/verify$/, ); expect(confirmationURL.search).toBe(""); expect(confirmationURL.hash).toMatch(/^#token=SFMyNTY\./); await page.goto(confirmationLink); const confirmationForm = page.locator( "#content-removal-confirmation-fragment-form", ); await expect(confirmationForm).toBeVisible(); await expect( page.locator("#content-removal-confirmation-fragment-invalid"), ).toBeHidden(); await expect( page.locator("#content-removal-confirmation-fragment-token"), ).toHaveValue(/^SFMyNTY\./); await confirmationForm .getByRole("button", { name: "Confirm content-removal notice" }) .click(); await expect(page).toHaveURL( /\/legal\/content-removal\/[0-9a-f-]+\?token=/, ); await expect( page.getByText("Your email was confirmed and the notice was sent for review."), ).toBeVisible(); await expect( page.getByRole("heading", { name: "Content-removal notice" }), ).toBeVisible(); assertBrowserClean(); await context.close(); }); test("authenticated support and legal notices reach the scoped staff queues", async ({ browser, request, }, testInfo) => { const projectName = testInfo.project.name; const requesterEmail = projectEmail("requester", projectName); const fixturePassword = process.env.E2E_FIXTURE_PASSWORD; const adminEmail = process.env.E2E_ADMIN_EMAIL ?? "e2e-admin@example.invalid"; const supportSubject = `E2E support delivery [${projectName}]`; const supportDetails = "Browser E2E verifies the authenticated private case without a duplicate receipt email."; const supportReply = `E2E support reply [${projectName}]`; const supportFollowUp = `E2E support inbox follow-up [${projectName}]`; const privacySubject = `E2E privacy request [${projectName}]`; const dataExportSubject = `E2E data export [${projectName}]`; const accountDeletionSubject = "Delete my Who Need Help account"; const generalExplanation = "Browser E2E verifies that a signed-in general removal notice reaches the legal queue."; const urgentExplanation = "Browser E2E verifies the urgent workflow without reproducing or uploading any material."; const legalResolution = `E2E legal review completed [${projectName}]`; const requester = fixturePassword ? await loginWithPassword(browser, requesterEmail, fixturePassword) : await registerAndConfirm( browser, request, requesterEmail, "E2E Support Requester", ); const admin = fixturePassword ? await loginWithPassword(browser, adminEmail, fixturePassword) : await loginWithMagicLink(browser, request, adminEmail); const assertRequesterClean = captureBrowserFailures(requester.page); const assertAdminClean = captureBrowserFailures(admin.page); const supportEmailBefore = await latestMessageID(request, requesterEmail); await requester.page.goto("/support"); await requester.page .getByLabel("What do you need help with?") .selectOption("technical_issue"); await expect(requester.page.getByLabel("Contact email")).toHaveValue( requesterEmail, ); await expect(requester.page.getByLabel("Contact email")).toHaveAttribute( "readonly", "", ); await requester.page.getByLabel("Subject").fill(supportSubject); await requester.page.getByLabel("Describe the problem").fill(supportDetails); await requester.page .getByRole("button", { name: "Send support request" }) .click(); await expect(requester.page).toHaveURL(/\/support\/received\?reference=SUP-/); await expect( requester.page.getByRole("heading", { name: "Support request created" }), ).toBeVisible(); await requester.page.waitForTimeout(1_000); expect(await latestMessageID(request, requesterEmail)).toBe( supportEmailBefore, ); await gotoLiveView(admin.page, "/support/operations?queue=support"); await admin.page .locator("#support-case-filters") .getByLabel("Search") .fill(supportSubject); const supportRow = admin.page .locator("main tbody tr") .filter({ hasText: supportSubject }); await expect(supportRow).toHaveCount(1); await supportRow.getByRole("link", { name: "Open" }).click(); await expect( admin.page.getByRole("heading", { name: supportSubject }), ).toBeVisible(); await admin.page.getByLabel("Case status").selectOption("resolved"); await admin.page .getByLabel("Reply to requester (optional)") .fill(supportReply); const replyEmailBefore = await latestMessageID(request, requesterEmail); await admin.page.getByRole("button", { name: "Save and notify" }).click(); await expect(admin.page.getByText("Support request updated.")).toBeVisible(); await waitForNewEmail( request, requesterEmail, replyEmailBefore, "Status: resolved", ); const followUpEmailBefore = await latestMessageID(request, requesterEmail); await admin.page .getByLabel("Reply to requester (optional)") .fill(supportFollowUp); await admin.page.getByRole("button", { name: "Save and notify" }).click(); await expect(admin.page.getByText("Support request updated.")).toBeVisible(); await admin.page.waitForTimeout(1_000); expect(await latestMessageID(request, requesterEmail)).toBe( followUpEmailBefore, ); await requester.page.goto("/support/requests"); const requesterCase = requester.page .locator("main") .getByRole("link") .filter({ hasText: supportSubject }); await expect(requesterCase).toHaveCount(1); await requesterCase.click(); await expect( requester.page.getByText(supportReply, { exact: true }), ).toBeVisible(); await expect( requester.page.getByText(supportFollowUp, { exact: true }), ).toBeVisible(); await submitAuthenticatedSupportRequest( requester.page, requesterEmail, "privacy_request", privacySubject, "Browser E2E verifies that an authenticated privacy request remains private and reaches the scoped support queue.", ); await submitAuthenticatedSupportRequest( requester.page, requesterEmail, "data_export", dataExportSubject, "Browser E2E verifies that an authenticated data-export request reaches the scoped support queue.", ); await requester.page.goto("/account/delete"); await expect(requester.page.getByLabel("Account email")).toHaveValue( requesterEmail, ); await expect(requester.page.getByLabel("Account email")).toHaveAttribute( "readonly", "", ); await requester.page .getByLabel("Additional information") .fill( "Browser E2E verifies that account deletion enters the dedicated support workflow without deleting the fixture account immediately.", ); await requester.page .getByRole("button", { name: "Request account deletion" }) .click(); await expect(requester.page).toHaveURL(/\/support\/received\?reference=SUP-/); await expect( requester.page.getByRole("heading", { name: "Support request created" }), ).toBeVisible(); await gotoLiveView(admin.page, "/support/operations?queue=support"); await admin.page .locator("#support-case-filters") .getByLabel("Search") .fill(requesterEmail); const requesterSupportRows = admin.page.locator("main tbody tr"); await expect(requesterSupportRows).toHaveCount(4); await expect( requesterSupportRows.filter({ hasText: privacySubject }), ).toHaveCount(1); await expect( requesterSupportRows.filter({ hasText: dataExportSubject }), ).toHaveCount(1); await expect( requesterSupportRows.filter({ hasText: accountDeletionSubject }), ).toHaveCount(1); await requester.page.goto("/support/requests"); await expect( requester.page.getByRole("link").filter({ hasText: privacySubject }), ).toHaveCount(1); await expect( requester.page.getByRole("link").filter({ hasText: dataExportSubject }), ).toHaveCount(1); await expect( requester.page.getByRole("link").filter({ hasText: accountDeletionSubject }), ).toHaveCount(1); const generalEmailBefore = await latestMessageID(request, requesterEmail); await requester.page.goto("/legal/content-removal"); await requester.page.getByLabel("Reason").selectOption("privacy_violation"); await requester.page .getByLabel("Your name or organisation") .fill("E2E Requester"); await requester.page .getByLabel("Your relationship to the affected person or rights holder") .selectOption("self"); await requester.page .getByLabel("Exact content URLs — one per line") .fill(`${process.env.BASE_URL}/requests/e2e-reported-content`); await requester.page .getByLabel("Why do you believe this content should be removed?") .fill(generalExplanation); await requester.page .getByLabel("Law, right, or policy involved, if known") .fill("Privacy review requested by the affected account holder."); await requester.page .getByLabel("Electronic signature (type your full name)") .fill("E2E Requester"); await requester.page .getByLabel(/I believe in good faith that the identified content/) .check(); await requester.page .getByLabel(/I confirm that this notice is accurate and complete/) .check(); await requester.page .getByRole("button", { name: "Submit removal notice" }) .click(); await expect(requester.page).toHaveURL( /\/legal\/content-removal\/received\?reference=REM-/, ); await waitForNewEmail( request, requesterEmail, generalEmailBefore, "Status: open", ); const urgentEmailBefore = await latestMessageID(request, requesterEmail); await requester.page.goto("/legal/take-it-down"); await requester.page .getByLabel("Material involved") .selectOption("non_consensual_intimate_media"); await requester.page .getByRole("textbox", { name: "Your full name", exact: true }) .fill("E2E Requester"); await requester.page .getByLabel("Who are you submitting for?") .selectOption("self"); await requester.page .getByLabel("Exact content URLs — one per line") .fill(`${process.env.BASE_URL}/requests/e2e-urgent-reported-content`); await requester.page .getByLabel(/Identify the material without reproducing it/) .fill(urgentExplanation); await requester.page .getByLabel("Electronic signature (type your full name)") .fill("E2E Requester"); await requester.page .getByLabel(/I have a good-faith belief that this intimate visual material/) .check(); await requester.page .getByLabel(/I confirm that the information in this request is accurate/) .check(); await requester.page .getByRole("button", { name: "Submit urgent removal request" }) .click(); await expect(requester.page).toHaveURL( /\/legal\/content-removal\/received\?reference=REM-/, ); await waitForNewEmail( request, requesterEmail, urgentEmailBefore, "Status: urgent_review", ); await gotoLiveView(admin.page, "/support/operations?queue=legal"); await admin.page .locator("#legal-case-filters") .getByLabel("Search") .fill(requesterEmail); await expect(admin.page.locator("main tbody tr")).toHaveCount(2); const urgentRow = admin.page .locator("main tbody tr") .filter({ hasText: "take it down" }); await expect(urgentRow).toHaveCount(1); await urgentRow.getByRole("link", { name: "Open" }).click(); await expect( admin.page.getByText(urgentExplanation, { exact: true }), ).toBeVisible(); await admin.page.getByLabel("Case status").selectOption("actioned"); await admin.page .getByLabel("Decision or information request") .fill(legalResolution); const legalDecisionEmailBefore = await latestMessageID( request, requesterEmail, ); await admin.page.getByRole("button", { name: "Save and notify" }).click(); await expect(admin.page.getByText("Removal notice updated.")).toBeVisible(); await waitForNewEmail( request, requesterEmail, legalDecisionEmailBefore, legalResolution, ); assertRequesterClean(); assertAdminClean(); await requester.context.close(); await admin.context.close(); });