Harden launch E2E and record launch gates
This commit is contained in:
parent
8e5073a7a5
commit
1f9c5d74d5
|
|
@ -200,10 +200,11 @@ block publishing changes, including Store Listing, Pricing, and Distribution.
|
|||
- [ ] Tester feedback and fixes documented.
|
||||
- [ ] Production-access questionnaire completed from actual evidence.
|
||||
|
||||
On 2026-08-14 Play Console showed Closed testing locked until the one remaining
|
||||
app-setup task, public contact details, is complete and reported `0 testers
|
||||
currently opted-in`. No Closed or Production release was created during this
|
||||
inspection.
|
||||
On 2026-08-20 Play Console still showed Closed testing locked until the one
|
||||
remaining app-setup task, public contact details, is complete and reported `0
|
||||
testers currently opted-in`. The Console still required at least 12 opted-in
|
||||
testers for at least 14 days. No Closed or Production release was created
|
||||
during this inspection.
|
||||
|
||||
## Publishing
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,21 @@ does not authorize saving fields in Play Console or publishing a release.
|
|||
This was a read-only observation. No Console value, track, release, production
|
||||
deployment, frozen test deployment, or public Git remote was changed.
|
||||
|
||||
## Read-only recheck on 2026-08-20
|
||||
|
||||
- The Dashboard still reports **10 of 11 complete**.
|
||||
- **Select an app category and provide contact details** remains the only
|
||||
incomplete setup task. The category is **Social**. Public email, phone, and
|
||||
website remain empty in Store settings.
|
||||
- Closed testing remains locked until app setup is complete and still reports
|
||||
`0 testers currently opted-in`.
|
||||
- The production-access section currently requires a published closed-testing
|
||||
release, at least 12 opted-in testers, and a closed test lasting at least 14
|
||||
days.
|
||||
|
||||
This recheck was read-only. No Console field, release, track, deployment,
|
||||
frozen test environment, or public Git remote was changed.
|
||||
|
||||
These are direct observations from the authenticated Play Console session on
|
||||
that date. Recheck the Console before applying because its fields and policy
|
||||
requirements can change.
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ The following decisions are intentionally not generated by code:
|
|||
allocation, alerts, and any scaling thresholds.
|
||||
- [ ] Backup ownership, encryption-key custody, off-site destination, restore
|
||||
procedure, and measured recovery objectives have been approved.
|
||||
- [x] The independent stale-backup alert uses the operator-selected maximum
|
||||
age of 36 hours (`129600` seconds). This is an alert threshold for the
|
||||
latest restore-verified heartbeat, not approval of retention, RPO, RTO,
|
||||
capacity, or encryption-key custody.
|
||||
|
||||
Until the retention decision and tested executor exist, account-deletion cases
|
||||
remain an audited operator workflow and automatic erasure stays disabled. The
|
||||
|
|
|
|||
|
|
@ -6,12 +6,35 @@ import {
|
|||
projectEmail,
|
||||
} from "./helpers";
|
||||
|
||||
async function submitSupportRequest(
|
||||
page: import("@playwright/test").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.skip(
|
||||
process.env.E2E_PRODUCTION_READ_ONLY !== "1",
|
||||
"This read-only staff queue check requires the production run-scoped fixture",
|
||||
process.env.E2E_PRODUCTION_RUN_SCOPED !== "1",
|
||||
"This support intake and staff queue check requires the production run-scoped fixture",
|
||||
);
|
||||
|
||||
test("run-scoped support and legal fixtures are visible to production staff", async ({
|
||||
test("run-scoped support intake and legal fixture reach production staff", async ({
|
||||
browser,
|
||||
}, testInfo) => {
|
||||
const runID = process.env.E2E_RUN_ID;
|
||||
|
|
@ -27,13 +50,69 @@ test("run-scoped support and legal fixtures are visible to production staff", as
|
|||
const supportSubject = `Production E2E support ${runID}`;
|
||||
const supportDetails =
|
||||
"Run-scoped read-only browser fixture for the production support queue.";
|
||||
const privacySubject = `Production E2E privacy ${runID}`;
|
||||
const dataExportSubject = `Production E2E data export ${runID}`;
|
||||
const accountDeletionSubject = "Delete my Who Need Help account";
|
||||
const removalExplanation =
|
||||
"Run-scoped read-only browser fixture for the production legal review queue.";
|
||||
|
||||
const requester = await loginWithPassword(
|
||||
browser,
|
||||
requesterEmail,
|
||||
fixturePassword,
|
||||
);
|
||||
const admin = await loginWithPassword(browser, adminEmail, fixturePassword);
|
||||
const assertRequesterClean = captureBrowserFailures(requester.page);
|
||||
const assertAdminClean = captureBrowserFailures(admin.page);
|
||||
|
||||
await submitSupportRequest(
|
||||
requester.page,
|
||||
requesterEmail,
|
||||
"privacy_request",
|
||||
privacySubject,
|
||||
"Run-scoped production browser verification for the authenticated privacy-request intake.",
|
||||
);
|
||||
await submitSupportRequest(
|
||||
requester.page,
|
||||
requesterEmail,
|
||||
"data_export",
|
||||
dataExportSubject,
|
||||
"Run-scoped production browser verification for the authenticated data-export intake.",
|
||||
);
|
||||
|
||||
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(
|
||||
"Run-scoped production browser verification for the account-deletion support workflow.",
|
||||
);
|
||||
await requester.page
|
||||
.getByRole("button", { name: "Request account deletion" })
|
||||
.click();
|
||||
await expect(requester.page).toHaveURL(/\/support\/received\?reference=SUP-/);
|
||||
|
||||
await gotoLiveView(admin.page, "/support/operations?queue=support");
|
||||
await admin.page
|
||||
.locator("#support-case-filters")
|
||||
.getByLabel("Search")
|
||||
.fill(requesterEmail);
|
||||
const supportRows = admin.page.locator("main tbody tr");
|
||||
await expect(supportRows).toHaveCount(4);
|
||||
await expect(supportRows.filter({ hasText: privacySubject })).toHaveCount(1);
|
||||
await expect(supportRows.filter({ hasText: dataExportSubject })).toHaveCount(
|
||||
1,
|
||||
);
|
||||
await expect(
|
||||
supportRows.filter({ hasText: accountDeletionSubject }),
|
||||
).toHaveCount(1);
|
||||
|
||||
await admin.page
|
||||
.locator("#support-case-filters")
|
||||
.getByLabel("Search")
|
||||
|
|
@ -62,6 +141,8 @@ test("run-scoped support and legal fixtures are visible to production staff", as
|
|||
admin.page.getByText(removalExplanation, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
assertRequesterClean();
|
||||
assertAdminClean();
|
||||
await requester.context.close();
|
||||
await admin.context.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -498,8 +498,8 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do
|
|||
defp validate_precreated_records!(manifest, support_request_ids, removal_notice_ids) do
|
||||
records = manifest["precreated_records"]
|
||||
|
||||
unless support_request_ids == [records["support_request"]] and
|
||||
removal_notice_ids == [records["content_removal_notice"]] do
|
||||
unless records["support_request"] in support_request_ids and
|
||||
records["content_removal_notice"] in removal_notice_ids do
|
||||
Mix.raise("full staging E2E precreated records do not match the manifest")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ Usage:
|
|||
./scripts/production-full-e2e.sh run [run-id]
|
||||
|
||||
The run creates only run-scoped synthetic users and records, exercises the
|
||||
two-user help, moderated activity, and read-only staff support/legal browser
|
||||
flows, and removes the exact fixture on success, failure, or interrupt. It
|
||||
never resets the database.
|
||||
two-user help, moderated activity, authenticated privacy/data/deletion intake,
|
||||
and read-only staff support/legal browser flows, and removes the exact fixture
|
||||
on success, failure, or interrupt. It never resets the database.
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ import shlex
|
|||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
wanted = {
|
||||
required = {
|
||||
"COMPOSE_PROJECT_NAME",
|
||||
"DATABASE_MODE",
|
||||
"DEPLOYMENT_ENV",
|
||||
|
|
@ -67,6 +67,8 @@ wanted = {
|
|||
"POSTGRES_DB",
|
||||
"WNH_BASE_URL",
|
||||
}
|
||||
optional = {"SUPPORT_OPERATOR_EMAIL_MODE"}
|
||||
wanted = required | optional
|
||||
values = {}
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for raw_line in handle:
|
||||
|
|
@ -79,12 +81,12 @@ with open(path, encoding="utf-8") as handle:
|
|||
parsed = shlex.split(value, comments=False, posix=True)
|
||||
values[key] = parsed[0] if parsed else ""
|
||||
|
||||
missing = sorted(key for key in wanted if not values.get(key))
|
||||
missing = sorted(key for key in required if not values.get(key))
|
||||
if missing:
|
||||
raise SystemExit("Missing production identity settings: " + ", ".join(missing))
|
||||
|
||||
for key in sorted(wanted):
|
||||
print(f"{key.lower()}={values[key]}")
|
||||
print(f"{key.lower()}={values.get(key, '')}")
|
||||
PY
|
||||
|
||||
commit=$(git -C "$root" rev-parse HEAD)
|
||||
|
|
@ -147,12 +149,15 @@ commit=$(value commit)
|
|||
container=$(value container)
|
||||
database=$(value postgres_db)
|
||||
project=$(value compose_project_name)
|
||||
support_operator_email_mode=$(value support_operator_email_mode)
|
||||
|
||||
if [[ "$(value deployment_env)" != production ]] ||
|
||||
[[ "$(value phx_host)" != whoneedhelp.com ]] ||
|
||||
[[ "$(value wnh_base_url)" != "$BASE_URL" ]] ||
|
||||
[[ "$(value database_mode)" != external ]] ||
|
||||
[[ "$project" != who_need_help_production ]] ||
|
||||
[[ "$(value support_operator_email_mode)" != "" &&
|
||||
"$(value support_operator_email_mode)" != disabled ]] ||
|
||||
[[ "$(value health)" != healthy ]] ||
|
||||
[[ "$(value fixture_prefix_count)" != 0 ]] ||
|
||||
[[ ! "$commit" =~ ^[0-9a-f]{40}$ ]] ||
|
||||
|
|
@ -195,14 +200,17 @@ Verified production target:
|
|||
commit: $commit
|
||||
app container: $container
|
||||
restart count: $(value restart_count)
|
||||
support operator email mode: ${support_operator_email_mode:-disabled}
|
||||
existing run-scoped fixture users: 0
|
||||
|
||||
Exact temporary mutation scope:
|
||||
- six confirmed synthetic users under wnh-staging-e2e-$RUN_ID-*;
|
||||
- their help requests, assignments, chats, positions, handover, reviews;
|
||||
- their activity, participation, group chat, report and category proposal;
|
||||
- one directly inserted support row and one directly inserted legal row,
|
||||
read through the staff UI without submitting or moderating either record;
|
||||
- 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;
|
||||
- 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.
|
||||
|
|
@ -318,10 +326,10 @@ cleanup() {
|
|||
! jq -e '
|
||||
.cleanup_verified == true and
|
||||
(.cleanup_targets.users | length) >= 6 and
|
||||
(.cleanup_targets.support_requests | length) == 1 and
|
||||
(.cleanup_targets.support_requests | length) >= 1 and
|
||||
(.cleanup_targets.content_removal_notices | length) == 1 and
|
||||
(.cleanup_deleted_counts.users | type) == "number" and
|
||||
.cleanup_deleted_counts.support_requests == 1 and
|
||||
.cleanup_deleted_counts.support_requests == (.cleanup_targets.support_requests | length) and
|
||||
.cleanup_deleted_counts.content_removal_notices == 1 and
|
||||
.cleanup_verified_at != null
|
||||
' "$output_dir/fixture.json" >/dev/null; then
|
||||
|
|
@ -431,7 +439,7 @@ docker run --rm \
|
|||
--env "E2E_RUN_ID=$RUN_ID" \
|
||||
--env "E2E_FIXTURE_PASSWORD=$fixture_password" \
|
||||
--env "E2E_ADMIN_EMAIL=wnh-staging-e2e-$RUN_ID-admin@example.invalid" \
|
||||
--env E2E_PRODUCTION_READ_ONLY=1 \
|
||||
--env E2E_PRODUCTION_RUN_SCOPED=1 \
|
||||
--env E2E_DETERMINISTIC_MAP_TILES=1 \
|
||||
--env HOME=/tmp \
|
||||
--volume "$output_dir/browser:/work/output" \
|
||||
|
|
|
|||
|
|
@ -53,6 +53,22 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do
|
|||
manifest["precreated_records"]["content_removal_notice"]
|
||||
)
|
||||
|
||||
browser_created_support_request =
|
||||
%WhoNeedHelp.Support.SupportRequest{
|
||||
reference: "SUP-BROWSER-#{String.upcase(run_id)}",
|
||||
requester_id: prepared_support_request.requester_id,
|
||||
contact_verified_at: DateTime.utc_now(:second),
|
||||
status: :open
|
||||
}
|
||||
|> WhoNeedHelp.Support.SupportRequest.submission_changeset(%{
|
||||
"kind" => "data_export",
|
||||
"contact_email" => prepared_support_request.contact_email,
|
||||
"subject" => "Browser-created production E2E support #{run_id}",
|
||||
"details" =>
|
||||
"This run-owned support record proves cleanup accepts browser-created fixture data."
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
assert manifest["schema_version"] == 2
|
||||
assert prepared_support_request.subject == "Production E2E support #{run_id}"
|
||||
assert prepared_support_request.contact_verified_at
|
||||
|
|
@ -89,13 +105,14 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do
|
|||
manifest = manifest_path |> File.read!() |> Jason.decode!()
|
||||
cleaned_job_ids = manifest["cleanup_targets"]["push_jobs"]
|
||||
|
||||
assert manifest["cleanup_targets"]["support_requests"] == [prepared_support_request.id]
|
||||
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 manifest["cleanup_deleted_counts"]["support_requests"] == 1
|
||||
assert manifest["cleanup_deleted_counts"]["support_requests"] == 2
|
||||
assert manifest["cleanup_deleted_counts"]["content_removal_notices"] == 1
|
||||
assert support_job.id in cleaned_job_ids
|
||||
assert legal_job.id in cleaned_job_ids
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user