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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user