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 project, volume, network, and one-run images. FCM/APNs device registration is
not claimed. not claimed.
- The final regression repeated the 174-test quality/security gate, isolated - The final regression repeated the 174-test quality/security gate, isolated
browser 1/1 bootstrap plus 8/8 Chromium specs, Android debug/staging builds browser 1/1 bootstrap plus 30/30 application scenarios across Chromium,
and 7/7 device tests on API 30, 34, and 37, the 50,000-row database benchmark, authenticated Firefox and WebKit, the public 3/3 full workflow replay and 1/1 PWA replay,
HTTP/LiveView/WebSocket/tracking load, Compose crash/replacement and kind Android debug/staging builds and 7/7 device tests on API 30, 34, and 37, the
rolling drills, alert firing/resolution, encrypted backup/restore failure 50,000-row database benchmark, authenticated HTTP/LiveView/WebSocket/tracking
paths, and the external protocol boundary. Scoped fixtures and one-run load, Compose crash/replacement and kind rolling drills, alert
projects were removed, retained evidence is ignored and non-secret, and Git firing/resolution, encrypted backup/restore failure paths, and the external
was clean at the tested application commit before this documentation update. 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 - The public-facing database backup also passes a full isolated upgrade
rehearsal against application commit `af9018f`: the restored copy advanced rehearsal against application commit `af9018f`: the restored copy advanced
from 8 to all 10 migrations, exposed all 11 valid cursor indexes, ran two web 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 the exact four replacement pod names, and waits for those resources. The fixed
full replay above passed. 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 ## Known work before a public production launch
- Replace the temporary staging origin with the production-owned domain and - Replace the temporary staging origin with the production-owned domain and

View File

@ -3,6 +3,7 @@ import {
captureBrowserFailures, captureBrowserFailures,
clickUntilVisible, clickUntilVisible,
gotoWithTransientRetry, gotoWithTransientRetry,
isTransientNetworkError,
loginWithPassword, loginWithPassword,
projectEmail, projectEmail,
registerAndConfirm, registerAndConfirm,
@ -22,7 +23,10 @@ test("registration, email confirmation, password, and email change work end to e
originalEmail, originalEmail,
"E2E Auth User", "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"); await user.page.goto("/users/settings");
const passwordForm = user.page.locator("#update_password"); 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"), user.page.getByText("A link to confirm your email change has been sent"),
); );
await gotoWithTransientRetry( const emailChangeLink = await waitForApplicationEmailLink(
user.page, request,
await waitForApplicationEmailLink( changedEmail,
request, "/users/settings/confirm-email/",
changedEmail,
"/users/settings/confirm-email/",
),
); );
await expect(user.page.getByText("Email changed successfully.")).toBeVisible(); 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(); await user.context.close();

View File

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