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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user