test: preserve one-time link semantics on retries

This commit is contained in:
SimpleTest 2026-07-20 02:58:19 +03:00
parent a25a56142b
commit 79e4fef155
4 changed files with 83 additions and 19 deletions

View File

@ -119,13 +119,15 @@ The goal remains open while any row lacks reproducible local evidence.
project, volume, network, and one-run images. FCM/APNs device registration is
not claimed.
- The final regression repeated the 174-test quality/security gate, isolated
browser 1/1 bootstrap plus 8/8 Chromium specs, Android debug/staging builds
and 7/7 device tests on API 30, 34, and 37, the 50,000-row database benchmark, authenticated
HTTP/LiveView/WebSocket/tracking load, Compose crash/replacement and kind
rolling drills, alert firing/resolution, encrypted backup/restore failure
paths, and the external protocol boundary. Scoped fixtures and one-run
projects were removed, retained evidence is ignored and non-secret, and Git
was clean at the tested application commit before this documentation update.
browser 1/1 bootstrap plus 30/30 application scenarios across Chromium,
Firefox and WebKit, the public 3/3 full workflow replay and 1/1 PWA replay,
Android debug/staging builds and 7/7 device tests on API 30, 34, and 37, the
50,000-row database benchmark, authenticated HTTP/LiveView/WebSocket/tracking
load, Compose crash/replacement and kind rolling drills, alert
firing/resolution, encrypted backup/restore failure paths, and the external
protocol boundary. Scoped fixtures and one-run projects were removed,
retained evidence is ignored and non-secret, and Git was clean at the tested
application commit before this documentation update.
- The public-facing database backup also passes a full isolated upgrade
rehearsal against application commit `af9018f`: the restored copy advanced
from 8 to all 10 migrations, exposed all 11 valid cursor indexes, ran two web

View File

@ -768,6 +768,38 @@ initial resource set. The harness now waits for old UIDs to disappear, captures
the exact four replacement pod names, and waits for those resources. The fixed
full replay above passed.
## Final public and browser replay after one-time-link hardening
The final public replay exposed a transport edge case specific to one-time
confirmation links: the server had committed the email change and consumed the
token before Chromium reported `ERR_NETWORK_CHANGED`, while the generic
navigation helper then replayed the already-consumed URL. The browser harness
now opens one-time links at most once. If that single navigation loses its
response to a transient transport reset, it verifies the committed account
state through the idempotent settings page instead of replaying the token.
- The complete public Chromium replay passed 3/3 against
`https://whoneedhelp.imalto.site`: registration and email/password changes;
medicine request, helper matching, two-way chat, browser location tracking,
marker deletion, handover and blind reviews; Activities, privacy, social
links, blocks, reports, category voting and moderation. Its cleanup restored
all 19 recorded application-table counts byte-for-byte and removed the
run-specific Mailpit messages. Evidence is
`output/staging-full-e2e/20260719235055-542603`.
- The fresh isolated browser matrix passed bootstrap 1/1 and 30/30 application
scenarios, 10 each in Chromium, Firefox and WebKit, including realtime
recovery after the serving BEAM node was restarted. The run-scoped
containers, networks and PostgreSQL volume were removed. Evidence is
`output/e2e/20260719235159-569218`.
- The public HTTPS PWA replay passed 1/1, covering manifest and install assets,
service-worker cache update, offline public fallback, and exclusion of
private application pages from the offline cache. Evidence is
`output/staging-pwa-e2e/20260719235520-665802`.
- `./scripts/quality.sh` passed again on the same source state with 174/174
ExUnit tests and all configured source, dependency, Compose, Helm,
observability and container-image security gates. Evidence is
`output/regression/final-single-use-quality-20260720.log`.
## Known work before a public production launch
- Replace the temporary staging origin with the production-owned domain and

View File

@ -3,6 +3,7 @@ import {
captureBrowserFailures,
clickUntilVisible,
gotoWithTransientRetry,
isTransientNetworkError,
loginWithPassword,
projectEmail,
registerAndConfirm,
@ -22,7 +23,10 @@ test("registration, email confirmation, password, and email change work end to e
originalEmail,
"E2E Auth User",
);
const assertBrowserClean = captureBrowserFailures(user.page);
const recoverableOneTimeNavigations = new Set<string>();
const assertBrowserClean = captureBrowserFailures(user.page, {
recoverableOneTimeNavigations,
});
await user.page.goto("/users/settings");
const passwordForm = user.page.locator("#update_password");
@ -40,15 +44,31 @@ test("registration, email confirmation, password, and email change work end to e
user.page.getByText("A link to confirm your email change has been sent"),
);
await gotoWithTransientRetry(
user.page,
await waitForApplicationEmailLink(
const emailChangeLink = await waitForApplicationEmailLink(
request,
changedEmail,
"/users/settings/confirm-email/",
),
);
recoverableOneTimeNavigations.add(emailChangeLink);
try {
await user.page.goto(emailChangeLink);
await expect(user.page.getByText("Email changed successfully.")).toBeVisible();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!isTransientNetworkError(message)) {
throw error;
}
// The server may commit and consume the single-use token before a
// transport reset reaches the browser. Never replay that URL: verify the
// committed account state through an idempotent page instead.
await gotoWithTransientRetry(user.page, "/users/settings");
await expect(user.page.locator("#update_email").getByLabel("Email")).toHaveValue(
changedEmail,
);
}
await user.context.close();

View File

@ -67,7 +67,7 @@ export async function gotoWithTransientRetry(
throw lastError;
}
function isTransientNetworkError(message: string): boolean {
export function isTransientNetworkError(message: string): boolean {
return [
"ERR_NETWORK_CHANGED",
"ERR_CONNECTION_RESET",
@ -147,7 +147,10 @@ export async function markerKinds(page: Page): Promise<string[]> {
return markers.map(({ title }) => title ?? "");
}
export function captureBrowserFailures(page: Page): () => void {
export function captureBrowserFailures(
page: Page,
options: { recoverableOneTimeNavigations?: Set<string> } = {},
): () => void {
const failures: string[] = [];
page.on("console", (message) => {
@ -173,13 +176,20 @@ export function captureBrowserFailures(page: Page): () => void {
cancelled &&
new URL(request.url()).origin === new URL(baseURL).origin &&
["script", "stylesheet"].includes(request.resourceType());
const recoverableOneTimeNavigation =
options.recoverableOneTimeNavigations?.has(request.url()) === true &&
isTransientNetworkError(failure);
// MapLibre cancels in-flight raster tiles when a LiveView navigation
// destroys or re-centres a map. Chromium also aborts the old document's
// static assets when a bounded navigation retry replaces that document.
// Actual tile/network failures retain their engine error and remain test
// failures.
if ((rasterTile && cancelled) || replacedDocumentAsset) {
if (
(rasterTile && cancelled) ||
replacedDocumentAsset ||
recoverableOneTimeNavigation
) {
return;
}