Verify support and legal workflows end to end

This commit is contained in:
SimpleTest 2026-08-09 09:09:36 +03:00
parent d8177f38bd
commit ff50a80e48
7 changed files with 344 additions and 4 deletions

View File

@ -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
public/authentication boundaries;
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
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
four public pages in both themes, horizontal overflow at three viewport widths,
an actual locally served raster map tile, LiveView offline/reconnect UI, public

View File

@ -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
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
Run both checks against the single ignored production `.env`. The first reports

View File

@ -178,3 +178,20 @@ regardless.
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
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.

View File

@ -1,8 +1,45 @@
# 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.
## 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
- A headed Chrome audit captured the production home at desktop and 390 × 844

View 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();
});

View File

@ -13,6 +13,23 @@ if [ -f "$target" ]; then
chmod 600 "$target"
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
printf '\nTRAEFIK_RETRY_ATTEMPTS=3\n' >>"$target"
updated=true

View File

@ -257,7 +257,8 @@ docker run --rm \
npx playwright test --project=chromium \
tests/auth-settings.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 "Evidence: $output_dir"