362 lines
13 KiB
TypeScript
362 lines
13 KiB
TypeScript
import { APIRequestContext, expect, Page, test } from "@playwright/test";
|
|
import {
|
|
captureBrowserFailures,
|
|
gotoLiveView,
|
|
latestMessageID,
|
|
loginWithMagicLink,
|
|
loginWithPassword,
|
|
projectEmail,
|
|
registerAndConfirm,
|
|
} from "./helpers";
|
|
|
|
async function waitForNewEmail(
|
|
request: APIRequestContext,
|
|
email: string,
|
|
previousMessageID: string | undefined,
|
|
expectedText: string,
|
|
): Promise<void> {
|
|
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<void> {
|
|
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("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();
|
|
});
|