From ccbef9cd4f45ad9cdcbbc343e71b78a027da46ef Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Fri, 21 Aug 2026 01:11:49 +0300 Subject: [PATCH] Verify production support and legal queues --- docs/operations.md | 5 +- docs/public-launch-checklist.md | 12 ++-- docs/verification.md | 23 ++++++++ e2e/tests/production-support-legal.spec.ts | 27 +++++++-- lib/mix/tasks/wnh.staging_full_e2e.ex | 55 ++++++++++++++++--- mix.lock | 2 +- scripts/production-full-e2e.sh | 8 +-- .../staging_full_e2e_cleanup_test.exs | 19 +++++-- 8 files changed, 122 insertions(+), 29 deletions(-) diff --git a/docs/operations.md b/docs/operations.md index 60fbf30..0d3ed77 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -1274,8 +1274,9 @@ WNH_PRODUCTION_E2E_CONFIRM='VALUE_PRINTED_BY_PLAN' \ The run creates six uniquely prefixed synthetic users and only their associated mutual-aid, activity, notification, audit, support, and legal fixture records. -The support and legal records are inserted directly without email jobs and are -only read through the staff UI; the browser does not submit or moderate them. +The support record, general-removal notice, and separate TAKE IT DOWN notice are +inserted directly without email jobs and are only read through the staff UI; +the browser does not submit or moderate them. Cleanup uses the mode-`0600` manifest of exact IDs on success, failure, or interrupt, refuses cross-fixture relationships, deletes only matching jobs and records, and verifies that the run prefix is absent. It never resets the diff --git a/docs/public-launch-checklist.md b/docs/public-launch-checklist.md index 9fed457..caa8d18 100644 --- a/docs/public-launch-checklist.md +++ b/docs/public-launch-checklist.md @@ -197,11 +197,13 @@ as forward-only rather than receiving an invented database rollback. approval, and reporting. - [ ] Support, privacy/data, account-deletion, general content-removal, and TAKE IT DOWN submissions reach the correct production operator queues. - A 2026-08-09 run-scoped check proved one authenticated support case and - one authenticated general content-removal notice, then removed only those - exact records. Privacy/data, account deletion, anonymous verification, - and TAKE IT DOWN still require an exact production smoke before this - combined item can be checked. + A 2026-08-21 run-scoped check proved authenticated support, privacy, + data-export and account-deletion submissions plus separate general + content-removal and TAKE IT DOWN records. Staff found every record in its + permission-scoped production queue, and exact cleanup left zero records + with the run prefix. Anonymous contact verification still requires a + controlled production mail smoke before this combined item can be + checked. - [ ] Database, application, worker, email, push, backup, and edge monitoring are visible to the responsible operator. diff --git a/docs/verification.md b/docs/verification.md index 70acc59..a4d6f97 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -3265,3 +3265,26 @@ promoted. `support_confirmation` deliveries, two successful `mail` queue jobs, and no delivery exception in the selected metric set. These counters describe only the current application process, not historical delivery volume. + +# 2026-08-21 production support and legal queue coverage + +- Temporary production run `codex-support-legal-20260821-r3` passed all three + Chromium scenarios against exact deployed revision + `bb7eb58c8f14d8936cae0e968b50ae721516d213`: activity moderation, the complete + two-person medicine-help flow, and support/legal queue coverage. +- The support/legal scenario submitted authenticated support, privacy, + data-export and account-deletion requests through the public UI. Staff found + the resulting records in the permission-scoped production queue. The same + staff session found and opened distinct run-scoped general-removal and TAKE + IT DOWN records without submitting an operator decision. +- The fixture manifest used schema version 3, recorded the exact support, + general-removal and TAKE IT DOWN UUIDs, and ended with + `cleanup_verified=true`. Post-run inspection found + `fixture_prefix_count=0`; no run-owned fixture user or relationship remained. +- Anonymous contact verification was not submitted in this run because it + intentionally enqueues a real verification email. That path remains an open + controlled production-mail check rather than an inferred success. +- Non-secret evidence is retained under + `output/production-full-e2e/codex-support-legal-20260821-r3/`. The frozen + hackathon test deployment, Caddy, public Git remote and production release + were not changed by this browser run. diff --git a/e2e/tests/production-support-legal.spec.ts b/e2e/tests/production-support-legal.spec.ts index a839e3f..29d43c5 100644 --- a/e2e/tests/production-support-legal.spec.ts +++ b/e2e/tests/production-support-legal.spec.ts @@ -55,6 +55,10 @@ test("run-scoped support intake and legal fixture reach production staff", async const accountDeletionSubject = "Delete my Who Need Help account"; const removalExplanation = "Run-scoped read-only browser fixture for the production legal review queue."; + const takeItDownExplanation = + "Run-scoped read-only browser fixture for the production TAKE IT DOWN queue."; + const removalReference = `REM-E2E-${runID.toUpperCase()}`; + const takeItDownReference = `REM-E2E-TID-${runID.toUpperCase()}`; const requester = await loginWithPassword( browser, @@ -133,14 +137,29 @@ test("run-scoped support intake and legal fixture reach production staff", async await admin.page .locator("#legal-case-filters") .getByLabel("Search") - .fill(requesterEmail); - const legalRow = admin.page.locator("main tbody tr"); - await expect(legalRow).toHaveCount(1); - await legalRow.getByRole("link", { name: "Open" }).click(); + .fill(removalReference); + const generalLegalRow = admin.page.locator("main tbody tr"); + await expect(generalLegalRow).toHaveCount(1); + await expect(generalLegalRow).toContainText("general"); + await generalLegalRow.getByRole("link", { name: "Open" }).click(); await expect( admin.page.getByText(removalExplanation, { exact: true }), ).toBeVisible(); + await gotoLiveView(admin.page, "/support/operations?queue=legal"); + await admin.page + .locator("#legal-case-filters") + .getByLabel("Search") + .fill(takeItDownReference); + const takeItDownRow = admin.page.locator("main tbody tr"); + await expect(takeItDownRow).toHaveCount(1); + await expect(takeItDownRow).toContainText(/take it down/i); + await expect(takeItDownRow).toContainText("urgent review"); + await takeItDownRow.getByRole("link", { name: "Open" }).click(); + await expect( + admin.page.getByText(takeItDownExplanation, { exact: true }), + ).toBeVisible(); + assertRequesterClean(); assertAdminClean(); await requester.context.close(); diff --git a/lib/mix/tasks/wnh.staging_full_e2e.ex b/lib/mix/tasks/wnh.staging_full_e2e.ex index ad7886a..182a3a4 100644 --- a/lib/mix/tasks/wnh.staging_full_e2e.ex +++ b/lib/mix/tasks/wnh.staging_full_e2e.ex @@ -142,7 +142,13 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do password_hash = Bcrypt.hash_pwd_salt(password) now = DateTime.utc_now(:second) - {:ok, %{users: users, support_request: support_request, removal_notice: removal_notice}} = + {:ok, + %{ + users: users, + support_request: support_request, + removal_notice: removal_notice, + take_it_down_notice: take_it_down_notice + }} = Repo.transaction(fn -> users = Map.new(@precreated_roles, fn role -> @@ -167,22 +173,25 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do support_request = insert_support_fixture!(context, requester, now) removal_notice = insert_removal_fixture!(context, requester, now) + take_it_down_notice = insert_take_it_down_fixture!(context, requester, now) %{ users: users, support_request: support_request, - removal_notice: removal_notice + removal_notice: removal_notice, + take_it_down_notice: take_it_down_notice } end) manifest = %{ - "schema_version" => 2, + "schema_version" => 3, "run_id" => context.run_id, "database" => context.database, "precreated_users" => users, "precreated_records" => %{ "support_request" => support_request.id, - "content_removal_notice" => removal_notice.id + "content_removal_notice" => removal_notice.id, + "take_it_down_notice" => take_it_down_notice.id }, "allowed_emails" => context.emails |> Map.values() |> Enum.sort() } @@ -468,15 +477,17 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do case manifest["precreated_records"] do %{ "support_request" => support_request_id, - "content_removal_notice" => removal_notice_id + "content_removal_notice" => removal_notice_id, + "take_it_down_notice" => take_it_down_notice_id } -> - uuid?(support_request_id) and uuid?(removal_notice_id) + uuid?(support_request_id) and uuid?(removal_notice_id) and + uuid?(take_it_down_notice_id) and removal_notice_id != take_it_down_notice_id _other -> false end - unless manifest["schema_version"] == 2 and manifest["run_id"] == context.run_id and + unless manifest["schema_version"] == 3 and manifest["run_id"] == context.run_id and manifest["database"] == context.database and manifest["allowed_emails"] == expected_emails and valid_precreated? and valid_precreated_records? do @@ -499,7 +510,9 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do records = manifest["precreated_records"] unless records["support_request"] in support_request_ids and - records["content_removal_notice"] in removal_notice_ids do + records["content_removal_notice"] in removal_notice_ids and + records["take_it_down_notice"] in removal_notice_ids and + records["content_removal_notice"] != records["take_it_down_notice"] do Mix.raise("full staging E2E precreated records do not match the manifest") end end @@ -810,6 +823,32 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do |> Repo.insert!() end + defp insert_take_it_down_fixture!(context, requester, now) do + %Notice{ + reference: "REM-E2E-TID-#{String.upcase(context.run_id)}", + requester_id: requester["id"], + contact_verified_at: now, + regime: :take_it_down, + status: :urgent_review, + response_due_at: DateTime.add(now, 48, :hour) + } + |> Notice.submission_changeset(%{ + "category" => "non_consensual_intimate_media", + "submitter_name" => "Production E2E Requester", + "contact_email" => requester["email"], + "relationship" => "self", + "content_locations" => + "https://whoneedhelp.com/requests/production-e2e-take-it-down-#{context.run_id}", + "explanation" => + "Run-scoped read-only browser fixture for the production TAKE IT DOWN queue.", + "legal_basis" => "Production E2E fixture only.", + "electronic_signature" => "Production E2E Requester", + "good_faith" => "true", + "accurate_complete" => "true" + }) + |> Repo.insert!() + end + defp emails(run_id) do (@precreated_roles ++ @registered_roles) |> Map.new(fn role -> {role, "#{prefix(run_id)}#{role}@example.invalid"} end) diff --git a/mix.lock b/mix.lock index 7a8eceb..8345496 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "assent": {:hex, :assent, "0.3.1", "7e7b04f4ab5d07b497b80c04f009a0f1efc409787e68b8e086512b9098f3095b", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "3597b31f9eb556d97e64cf60c00d3451f7353d7b465a71d33530b870ebed1ff1"}, - "bandit": {:hex, :bandit, "1.12.4", "10bbab488edf8162318d736c19c5837077b8fca2bf5d95b07b33830387124f62", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "84513318c5752a2a8017664450f889b47fae5d53d64698ddf1e4fb09a7449e8d"}, + "bandit": {:hex, :bandit, "1.12.5", "af205a8e550f304caae09a97d29fd3c79a7f337526ea7cd772d2ff11d2f7c800", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c5684ca062fa407cac115aec3256383f3e2ec9fdced7904d59cf5a7bb7ed6181"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, diff --git a/scripts/production-full-e2e.sh b/scripts/production-full-e2e.sh index 34e0fb6..38bcb43 100755 --- a/scripts/production-full-e2e.sh +++ b/scripts/production-full-e2e.sh @@ -209,8 +209,8 @@ Exact temporary mutation scope: - their activity, participation, group chat, report and category proposal; - one directly inserted support row plus three authenticated privacy, data-export and account-deletion requests submitted through the public UI; - - one directly inserted legal row, read through the staff UI without - submitting or moderating a content-removal notice; + - one directly inserted general-removal row and one separate TAKE IT DOWN + row, read through the staff UI without submitting or moderating either; - their notifications, audit events and associated Oban jobs; - no database reset, migration, real-user role/status change, email delivery, Caddy change, test-project change, payment, iOS, or KYC action. @@ -327,10 +327,10 @@ cleanup() { .cleanup_verified == true and (.cleanup_targets.users | length) >= 6 and (.cleanup_targets.support_requests | length) >= 1 and - (.cleanup_targets.content_removal_notices | length) == 1 and + (.cleanup_targets.content_removal_notices | length) == 2 and (.cleanup_deleted_counts.users | type) == "number" and .cleanup_deleted_counts.support_requests == (.cleanup_targets.support_requests | length) and - .cleanup_deleted_counts.content_removal_notices == 1 and + .cleanup_deleted_counts.content_removal_notices == 2 and .cleanup_verified_at != null ' "$output_dir/fixture.json" >/dev/null; then echo "Production E2E cleanup manifest lacks exact run-scoped verification." >&2 diff --git a/test/who_need_help/staging_full_e2e_cleanup_test.exs b/test/who_need_help/staging_full_e2e_cleanup_test.exs index f2bbfc6..0eff203 100644 --- a/test/who_need_help/staging_full_e2e_cleanup_test.exs +++ b/test/who_need_help/staging_full_e2e_cleanup_test.exs @@ -53,6 +53,12 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do manifest["precreated_records"]["content_removal_notice"] ) + prepared_take_it_down_notice = + Repo.get!( + WhoNeedHelp.ContentRemoval.Notice, + manifest["precreated_records"]["take_it_down_notice"] + ) + browser_created_support_request = %WhoNeedHelp.Support.SupportRequest{ reference: "SUP-BROWSER-#{String.upcase(run_id)}", @@ -69,11 +75,15 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do }) |> Repo.insert!() - assert manifest["schema_version"] == 2 + assert manifest["schema_version"] == 3 assert prepared_support_request.subject == "Production E2E support #{run_id}" assert prepared_support_request.contact_verified_at assert prepared_removal_notice.regime == :general assert prepared_removal_notice.contact_verified_at + assert prepared_take_it_down_notice.regime == :take_it_down + assert prepared_take_it_down_notice.status == :urgent_review + assert prepared_take_it_down_notice.response_due_at + assert prepared_take_it_down_notice.contact_verified_at refute_enqueued( worker: WhoNeedHelp.Mail.SupportOperatorAlertWorker, @@ -108,12 +118,11 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do assert Enum.sort(manifest["cleanup_targets"]["support_requests"]) == Enum.sort([prepared_support_request.id, browser_created_support_request.id]) - assert manifest["cleanup_targets"]["content_removal_notices"] == [ - prepared_removal_notice.id - ] + assert Enum.sort(manifest["cleanup_targets"]["content_removal_notices"]) == + Enum.sort([prepared_removal_notice.id, prepared_take_it_down_notice.id]) assert manifest["cleanup_deleted_counts"]["support_requests"] == 2 - assert manifest["cleanup_deleted_counts"]["content_removal_notices"] == 1 + assert manifest["cleanup_deleted_counts"]["content_removal_notices"] == 2 assert support_job.id in cleaned_job_ids assert legal_job.id in cleaned_job_ids refute unrelated_job.id in cleaned_job_ids