test: preserve one-time link semantics on retries
This commit is contained in:
parent
a25a56142b
commit
79e4fef155
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user