Verify support and legal workflows end to end
This commit is contained in:
parent
d8177f38bd
commit
ff50a80e48
|
|
@ -513,9 +513,13 @@ real Mailpit messages and uses the audited one-time bootstrap command inside
|
||||||
only the isolated E2E database to create its moderator. It covers
|
only the isolated E2E database to create its moderator. It covers
|
||||||
public/authentication boundaries;
|
public/authentication boundaries;
|
||||||
the urgent medicine flow through matching, realtime chat, handover, and
|
the urgent medicine flow through matching, realtime chat, handover, and
|
||||||
double-blind reviews; and the Activity flow through join approval, private
|
double-blind reviews; the Activity flow through join approval, private
|
||||||
group chat, message-scoped reporting, blocking, privacy defaults, an unverified
|
group chat, message-scoped reporting, blocking, privacy defaults, an unverified
|
||||||
social link, category moderation, report resolution, and account restriction.
|
social link, category moderation, report resolution, and account restriction;
|
||||||
|
and the support/legal flow through authenticated support, operator reply and
|
||||||
|
resolution, private requester status, general content removal, dedicated TAKE
|
||||||
|
IT DOWN intake, legal-queue processing, and decision email delivery through
|
||||||
|
the isolated Mailpit server.
|
||||||
The same gate checks keyboard skip navigation, WCAG violations and contrast on
|
The same gate checks keyboard skip navigation, WCAG violations and contrast on
|
||||||
four public pages in both themes, horizontal overflow at three viewport widths,
|
four public pages in both themes, horizontal overflow at three viewport widths,
|
||||||
an actual locally served raster map tile, LiveView offline/reconnect UI, public
|
an actual locally served raster map tile, LiveView offline/reconnect UI, public
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ manifest-transport-only shell adjustment then passed `bash -n`, ShellCheck,
|
||||||
and the complete production browser E2E replay. Exact identities and evidence
|
and the complete production browser E2E replay. Exact identities and evidence
|
||||||
paths are recorded in `docs/verification.md`.
|
paths are recorded in `docs/verification.md`.
|
||||||
|
|
||||||
|
A later 2026-08-09 local browser replay passed all 17 Chromium scenarios,
|
||||||
|
including the new end-to-end support and legal-removal workflow. Its focused
|
||||||
|
and full structured results are recorded in `docs/verification.md`. This is
|
||||||
|
local isolated evidence only and does not mark the production support/legal or
|
||||||
|
SMTP checklist items complete.
|
||||||
|
|
||||||
## 2. Verify production configuration without exposing secrets
|
## 2. Verify production configuration without exposing secrets
|
||||||
|
|
||||||
Run both checks against the single ignored production `.env`. The first reports
|
Run both checks against the single ignored production `.env`. The first reports
|
||||||
|
|
|
||||||
|
|
@ -178,3 +178,20 @@ regardless.
|
||||||
The software does not provide emergency response. Threats to life or safety are
|
The software does not provide emergency response. Threats to life or safety are
|
||||||
prioritized in the queue, while every public safety screen continues to direct
|
prioritized in the queue, while every public safety screen continues to direct
|
||||||
people in immediate danger to local emergency services.
|
people in immediate danger to local emergency services.
|
||||||
|
|
||||||
|
## Reproducible browser verification
|
||||||
|
|
||||||
|
The isolated browser topology exercises these queues without using a public
|
||||||
|
deployment or real SMTP provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/e2e-run.sh tests/support-legal.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
The scenario registers a run-scoped requester, creates an isolated
|
||||||
|
administrator, submits authenticated support and both removal regimes,
|
||||||
|
verifies Mailpit messages, processes each permission-scoped queue, and checks
|
||||||
|
requester-visible status and decision delivery. The wrapper removes only its
|
||||||
|
unique Compose project, networks, database volume, and temporary image on
|
||||||
|
success, failure, or interrupt. This test does not establish staffing, legal
|
||||||
|
classification, or production email delivery.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,45 @@
|
||||||
# Who Need Help — implementation verification
|
# Who Need Help — implementation verification
|
||||||
|
|
||||||
Observed through 2026-08-08 in the local workspace. This report separates observed
|
Observed through 2026-08-09 in the local workspace. This report separates observed
|
||||||
results from product limits and unknown production properties.
|
results from product limits and unknown production properties.
|
||||||
|
|
||||||
|
## Local support/legal and complete browser E2E on 2026-08-09
|
||||||
|
|
||||||
|
- The focused isolated Chromium run exercised an authenticated support case,
|
||||||
|
Mailpit acknowledgement, permission-scoped staff discovery and assignment,
|
||||||
|
an operator reply and resolution, the requester's private case view, general
|
||||||
|
content-removal intake, the dedicated TAKE IT DOWN intake, legal-queue
|
||||||
|
processing, and the resulting decision email. Its one Playwright scenario
|
||||||
|
passed in 3.3 seconds. The structured result is retained at
|
||||||
|
`output/e2e/20260809055749-2064536/results.json`.
|
||||||
|
- The complete isolated Chromium suite then passed all 17 scenarios in 1.4
|
||||||
|
minutes. In addition to support and legal operations, it covered public and
|
||||||
|
authenticated responsive/accessibility states, authentication and settings,
|
||||||
|
localisation, mutual-aid matching/chat/tracking/handover/reviews,
|
||||||
|
notifications and data export, request discovery and location privacy,
|
||||||
|
Activity moderation, LiveView reconnect behaviour, and active web-node
|
||||||
|
failover. The structured result is retained at
|
||||||
|
`output/e2e/20260809055914-2110249/results.json`.
|
||||||
|
- Both runs used unique Compose projects and independent PostGIS volumes. After
|
||||||
|
each run, an exact-name inspection found no remaining matching container,
|
||||||
|
network, volume, or image. The ordinary development, production, and frozen
|
||||||
|
hackathon-test projects were not recreated or changed.
|
||||||
|
- The subsequent complete isolated quality run passed ShellCheck, release,
|
||||||
|
rollback and migration drills, Dockerfile and workflow linting, compilation,
|
||||||
|
xref, Credo, Sobelow, Dialyzer, Hex and npm audits, 455 ExUnit tests, Compose,
|
||||||
|
Helm, observability, backup and image checks, and the configured Trivy scans
|
||||||
|
with zero reported HIGH/CRITICAL findings. Its user-systemd unit
|
||||||
|
`codex-heavy-wnh-support-legal-quality-20260809-20260809-090528-2300548.service`
|
||||||
|
completed in 2 minutes 14.077 seconds with a 280.6 MiB observed memory peak.
|
||||||
|
Exact-name inspection after its cleanup found no run-owned Compose resource
|
||||||
|
or image.
|
||||||
|
|
||||||
|
These local results prove the implemented browser paths in the isolated E2E
|
||||||
|
topology. They do not prove production SMTP delivery, production queue routing,
|
||||||
|
or a staging deployment: the public staging runner now includes the same
|
||||||
|
support/legal scenario, but it was intentionally not run against the frozen
|
||||||
|
hackathon-test deployment.
|
||||||
|
|
||||||
## Public/mobile and Android candidate check on 2026-08-03
|
## Public/mobile and Android candidate check on 2026-08-03
|
||||||
|
|
||||||
- A headed Chrome audit captured the production home at desktop and 390 × 844
|
- A headed Chrome audit captured the production home at desktop and 390 × 844
|
||||||
|
|
|
||||||
258
e2e/tests/support-legal.spec.ts
Normal file
258
e2e/tests/support-legal.spec.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
import { APIRequestContext, expect, 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 support receipt, private case, and staff reply.";
|
||||||
|
const supportReply = `E2E support reply [${projectName}]`;
|
||||||
|
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 waitForNewEmail(
|
||||||
|
request,
|
||||||
|
requesterEmail,
|
||||||
|
supportEmailBefore,
|
||||||
|
supportSubject,
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
supportReply,
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
@ -13,6 +13,23 @@ if [ -f "$target" ]; then
|
||||||
chmod 600 "$target"
|
chmod 600 "$target"
|
||||||
updated=false
|
updated=false
|
||||||
|
|
||||||
|
append_default_if_missing() {
|
||||||
|
key=$1
|
||||||
|
value=$2
|
||||||
|
|
||||||
|
if ! grep -q "^${key}=" "$target"; then
|
||||||
|
printf '\n%s=%s\n' "$key" "$value" >>"$target"
|
||||||
|
updated=true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve generated secrets while bringing older ignored runtime files
|
||||||
|
# forward when new required, non-secret deployment selectors are added.
|
||||||
|
append_default_if_missing DEPLOYMENT_TARGET compose
|
||||||
|
append_default_if_missing DEPLOYMENT_ENV development
|
||||||
|
append_default_if_missing APP_TOPOLOGY split
|
||||||
|
append_default_if_missing DATABASE_MODE container
|
||||||
|
|
||||||
if ! grep -q '^TRAEFIK_RETRY_ATTEMPTS=' "$target"; then
|
if ! grep -q '^TRAEFIK_RETRY_ATTEMPTS=' "$target"; then
|
||||||
printf '\nTRAEFIK_RETRY_ATTEMPTS=3\n' >>"$target"
|
printf '\nTRAEFIK_RETRY_ATTEMPTS=3\n' >>"$target"
|
||||||
updated=true
|
updated=true
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,8 @@ docker run --rm \
|
||||||
npx playwright test --project=chromium \
|
npx playwright test --project=chromium \
|
||||||
tests/auth-settings.spec.ts \
|
tests/auth-settings.spec.ts \
|
||||||
tests/mutual-aid.spec.ts \
|
tests/mutual-aid.spec.ts \
|
||||||
tests/activity-moderation.spec.ts
|
tests/activity-moderation.spec.ts \
|
||||||
|
tests/support-legal.spec.ts
|
||||||
|
|
||||||
echo "Full public staging E2E passed: $base_url"
|
echo "Full public staging E2E passed: $base_url"
|
||||||
echo "Evidence: $output_dir"
|
echo "Evidence: $output_dir"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user