Skip to content

Commit 3bbc393

Browse files
committed
Fix management browser lifecycle timeout
1 parent f3a5d35 commit 3bbc393

2 files changed

Lines changed: 90 additions & 65 deletions

File tree

tests/integration/assertions/browser.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
bodyContainsAnyMarker,
55
bodyContainsDenialText,
66
bodyContainsHttpErrorText,
7+
browserLifecycleTimeoutForLoginTargets,
78
browserScreenshotPath,
89
formatDeniedTargetRouteFailure,
910
isIdentityLoginInputPage,
@@ -87,6 +88,11 @@ describe("browser assertion helpers", () => {
8788
expect(shouldIgnoreHttpsErrors({})).toBe(false);
8889
});
8990

91+
test("sizes composite browser lifecycles to cover nested login retry budgets", () => {
92+
expect(browserLifecycleTimeoutForLoginTargets(2)).toBeGreaterThan(15 * 60 * 1000);
93+
expect(browserLifecycleTimeoutForLoginTargets(3)).toBeGreaterThan(browserLifecycleTimeoutForLoginTargets(2));
94+
});
95+
9096
test("formats denied-route target failures with final URL and body snippet", () => {
9197
const message = formatDeniedTargetRouteFailure("https://app.example.test/protected", "terrarium-proxy-ok\nfixture reached");
9298

tests/integration/assertions/browser.ts

Lines changed: 84 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type LoginOptions = {
1818
const BROWSER_WAIT_TIMEOUT_MS = 120000;
1919
const BROWSER_FLOW_TIMEOUT_MS = 180000;
2020
const BROWSER_LIFECYCLE_TIMEOUT_MS = 15 * 60 * 1000;
21+
const BROWSER_LOGIN_ATTEMPTS = 3;
2122
const BROWSER_CLOSE_TIMEOUT_MS = 10000;
2223
const BROWSER_CLICK_TIMEOUT_MS = 10000;
2324
const BROWSER_POLL_TIMEOUT_MS = 1000;
@@ -72,6 +73,10 @@ const PASSWORD_SUBMIT_SELECTORS = submitControlSelectors(["Sign in", "Login", "C
7273
export function shouldIgnoreHttpsErrors(options: { resolveHosts?: Record<string, string>; ignoreHTTPSErrors?: boolean }): boolean {
7374
return options.ignoreHTTPSErrors ?? Object.keys(options.resolveHosts ?? {}).length > 0;
7475
}
76+
77+
export function browserLifecycleTimeoutForLoginTargets(targetCount: number): number {
78+
return BROWSER_FLOW_TIMEOUT_MS * BROWSER_LOGIN_ATTEMPTS * targetCount + BROWSER_WAIT_TIMEOUT_MS;
79+
}
7580
const CONSENT_SUBMIT_SELECTORS = ["Allow", "Authorize", "Approve", "Accept", "Continue", "Grant access"].flatMap((label) => [
7681
`button:has-text("${label}")`,
7782
`[role="button"]:has-text("${label}")`,
@@ -779,7 +784,7 @@ async function accountSelectionClickPoint(page: Page, userEmail: string): Promis
779784
export async function withBrowser<T>(
780785
outputDir: string,
781786
runFlow: (browser: Browser) => Promise<T>,
782-
options: { resolveHosts?: Record<string, string>; ignoreHTTPSErrors?: boolean } = {}
787+
options: { resolveHosts?: Record<string, string>; ignoreHTTPSErrors?: boolean; lifecycleTimeoutMs?: number } = {}
783788
): Promise<T> {
784789
mkdirSync(outputDir, { recursive: true });
785790
const browser = await withTimeout(
@@ -792,7 +797,7 @@ export async function withBrowser<T>(
792797
"browser launch"
793798
);
794799
try {
795-
return await withTimeout(runFlow(browser), BROWSER_LIFECYCLE_TIMEOUT_MS, "browser lifecycle");
800+
return await withTimeout(runFlow(browser), options.lifecycleTimeoutMs ?? BROWSER_LIFECYCLE_TIMEOUT_MS, "browser lifecycle");
796801
} catch (error) {
797802
const diagnostics = await captureBrowserLifecycleDiagnostics(browser, outputDir);
798803
throw new Error(`${error instanceof Error ? error.message : String(error)}${diagnostics}`);
@@ -970,12 +975,12 @@ async function loginThroughZitadelWithBrowserRetry(
970975
options: LoginOptions
971976
): Promise<{ finalUrl: string; screenshotPath: string; bodyText: string; title: string }> {
972977
let lastError: unknown;
973-
for (let attempt = 1; attempt <= 3; attempt += 1) {
978+
for (let attempt = 1; attempt <= BROWSER_LOGIN_ATTEMPTS; attempt += 1) {
974979
try {
975980
return await loginThroughZitadelWithBrowser(browser, url, user, options);
976981
} catch (error) {
977982
lastError = error;
978-
if (attempt >= 3 || !isRetryableBlankNavigationError(error)) {
983+
if (attempt >= BROWSER_LOGIN_ATTEMPTS || !isRetryableBlankNavigationError(error)) {
979984
throw error;
980985
}
981986
}
@@ -1074,31 +1079,38 @@ export async function expectManagementUi(
10741079
: {})
10751080
};
10761081
const ignoreHTTPSErrors = shouldIgnoreHttpsErrors({ resolveHosts });
1077-
await withBrowser(outputDir, async (browser) => {
1078-
const cockpit = await loginThroughZitadelWithBrowserRetry(browser, manageUrl, user, {
1079-
outputDir,
1080-
postLoginBodyMarkers: COCKPIT_TEXT_MARKERS,
1081-
postLoginLabel: "Cockpit",
1082-
ignoreHTTPSErrors
1083-
});
1084-
const cockpitFinal = new URL(cockpit.finalUrl);
1085-
const cockpitTarget = new URL(manageUrl);
1086-
if (cockpitFinal.host !== cockpitTarget.host) {
1087-
throw new Error(`unexpected post-login cockpit host: ${cockpit.finalUrl}`);
1088-
}
1089-
assertUserFacingPageBody(`${cockpit.title}\n${cockpit.bodyText}`, COCKPIT_TEXT_MARKERS, "Cockpit");
1090-
1091-
const proxy = await loginThroughZitadelWithBrowserRetry(browser, proxyUrl, user, {
1092-
outputDir,
1093-
postLoginBodyMarkers: TRAEFIK_TEXT_MARKERS,
1094-
postLoginLabel: "Traefik dashboard",
1095-
ignoreHTTPSErrors
1096-
});
1097-
if (!proxy.finalUrl.includes("/dashboard")) {
1098-
throw new Error(`unexpected Traefik dashboard URL: ${proxy.finalUrl}`);
1082+
await withBrowser(
1083+
outputDir,
1084+
async (browser) => {
1085+
const cockpit = await loginThroughZitadelWithBrowserRetry(browser, manageUrl, user, {
1086+
outputDir,
1087+
postLoginBodyMarkers: COCKPIT_TEXT_MARKERS,
1088+
postLoginLabel: "Cockpit",
1089+
ignoreHTTPSErrors
1090+
});
1091+
const cockpitFinal = new URL(cockpit.finalUrl);
1092+
const cockpitTarget = new URL(manageUrl);
1093+
if (cockpitFinal.host !== cockpitTarget.host) {
1094+
throw new Error(`unexpected post-login cockpit host: ${cockpit.finalUrl}`);
1095+
}
1096+
assertUserFacingPageBody(`${cockpit.title}\n${cockpit.bodyText}`, COCKPIT_TEXT_MARKERS, "Cockpit");
1097+
1098+
const proxy = await loginThroughZitadelWithBrowserRetry(browser, proxyUrl, user, {
1099+
outputDir,
1100+
postLoginBodyMarkers: TRAEFIK_TEXT_MARKERS,
1101+
postLoginLabel: "Traefik dashboard",
1102+
ignoreHTTPSErrors
1103+
});
1104+
if (!proxy.finalUrl.includes("/dashboard")) {
1105+
throw new Error(`unexpected Traefik dashboard URL: ${proxy.finalUrl}`);
1106+
}
1107+
assertUserFacingPageBody(`${proxy.title}\n${proxy.bodyText}`, TRAEFIK_TEXT_MARKERS, "Traefik dashboard");
1108+
},
1109+
{
1110+
lifecycleTimeoutMs: browserLifecycleTimeoutForLoginTargets(2),
1111+
resolveHosts: Object.keys(resolveHosts).length > 0 ? resolveHosts : undefined
10991112
}
1100-
assertUserFacingPageBody(`${proxy.title}\n${proxy.bodyText}`, TRAEFIK_TEXT_MARKERS, "Traefik dashboard");
1101-
}, { resolveHosts: Object.keys(resolveHosts).length > 0 ? resolveHosts : undefined });
1113+
);
11021114
}
11031115

11041116
/** Verifies the Cockpit, Traefik, and LXD management surfaces in one browser lifecycle. */
@@ -1121,44 +1133,51 @@ export async function expectManagementSurfaces(
11211133
: {})
11221134
};
11231135
const ignoreHTTPSErrors = shouldIgnoreHttpsErrors({ resolveHosts });
1124-
await withBrowser(outputDir, async (browser) => {
1125-
const cockpit = await loginThroughZitadelWithBrowserRetry(browser, manageUrl, user, {
1126-
outputDir,
1127-
postLoginBodyMarkers: COCKPIT_TEXT_MARKERS,
1128-
postLoginLabel: "Cockpit",
1129-
ignoreHTTPSErrors
1130-
});
1131-
const cockpitFinal = new URL(cockpit.finalUrl);
1132-
const cockpitTarget = new URL(manageUrl);
1133-
if (cockpitFinal.host !== cockpitTarget.host) {
1134-
throw new Error(`unexpected post-login cockpit host: ${cockpit.finalUrl}`);
1135-
}
1136-
assertUserFacingPageBody(`${cockpit.title}\n${cockpit.bodyText}`, COCKPIT_TEXT_MARKERS, "Cockpit");
1137-
1138-
const proxy = await loginThroughZitadelWithBrowserRetry(browser, proxyUrl, user, {
1139-
outputDir,
1140-
postLoginBodyMarkers: TRAEFIK_TEXT_MARKERS,
1141-
postLoginLabel: "Traefik dashboard",
1142-
ignoreHTTPSErrors
1143-
});
1144-
if (!proxy.finalUrl.includes("/dashboard")) {
1145-
throw new Error(`unexpected Traefik dashboard URL: ${proxy.finalUrl}`);
1146-
}
1147-
assertUserFacingPageBody(`${proxy.title}\n${proxy.bodyText}`, TRAEFIK_TEXT_MARKERS, "Traefik dashboard");
1148-
1149-
const lxd = await loginThroughZitadelWithBrowserRetry(browser, lxdUrl, user, {
1150-
outputDir,
1151-
postLoginBodyMarkers: LXD_TEXT_MARKERS,
1152-
postLoginLabel: "LXD UI",
1153-
ignoreHTTPSErrors
1154-
});
1155-
const lxdFinal = new URL(lxd.finalUrl);
1156-
const lxdTarget = new URL(lxdUrl);
1157-
if (lxdFinal.host !== lxdTarget.host) {
1158-
throw new Error(`unexpected post-login LXD host: ${lxd.finalUrl}`);
1136+
await withBrowser(
1137+
outputDir,
1138+
async (browser) => {
1139+
const cockpit = await loginThroughZitadelWithBrowserRetry(browser, manageUrl, user, {
1140+
outputDir,
1141+
postLoginBodyMarkers: COCKPIT_TEXT_MARKERS,
1142+
postLoginLabel: "Cockpit",
1143+
ignoreHTTPSErrors
1144+
});
1145+
const cockpitFinal = new URL(cockpit.finalUrl);
1146+
const cockpitTarget = new URL(manageUrl);
1147+
if (cockpitFinal.host !== cockpitTarget.host) {
1148+
throw new Error(`unexpected post-login cockpit host: ${cockpit.finalUrl}`);
1149+
}
1150+
assertUserFacingPageBody(`${cockpit.title}\n${cockpit.bodyText}`, COCKPIT_TEXT_MARKERS, "Cockpit");
1151+
1152+
const proxy = await loginThroughZitadelWithBrowserRetry(browser, proxyUrl, user, {
1153+
outputDir,
1154+
postLoginBodyMarkers: TRAEFIK_TEXT_MARKERS,
1155+
postLoginLabel: "Traefik dashboard",
1156+
ignoreHTTPSErrors
1157+
});
1158+
if (!proxy.finalUrl.includes("/dashboard")) {
1159+
throw new Error(`unexpected Traefik dashboard URL: ${proxy.finalUrl}`);
1160+
}
1161+
assertUserFacingPageBody(`${proxy.title}\n${proxy.bodyText}`, TRAEFIK_TEXT_MARKERS, "Traefik dashboard");
1162+
1163+
const lxd = await loginThroughZitadelWithBrowserRetry(browser, lxdUrl, user, {
1164+
outputDir,
1165+
postLoginBodyMarkers: LXD_TEXT_MARKERS,
1166+
postLoginLabel: "LXD UI",
1167+
ignoreHTTPSErrors
1168+
});
1169+
const lxdFinal = new URL(lxd.finalUrl);
1170+
const lxdTarget = new URL(lxdUrl);
1171+
if (lxdFinal.host !== lxdTarget.host) {
1172+
throw new Error(`unexpected post-login LXD host: ${lxd.finalUrl}`);
1173+
}
1174+
assertUserFacingPageBody(`${lxd.title}\n${lxd.bodyText}`, LXD_TEXT_MARKERS, "LXD UI");
1175+
},
1176+
{
1177+
lifecycleTimeoutMs: browserLifecycleTimeoutForLoginTargets(3),
1178+
resolveHosts: Object.keys(resolveHosts).length > 0 ? resolveHosts : undefined
11591179
}
1160-
assertUserFacingPageBody(`${lxd.title}\n${lxd.bodyText}`, LXD_TEXT_MARKERS, "LXD UI");
1161-
}, { resolveHosts: Object.keys(resolveHosts).length > 0 ? resolveHosts : undefined });
1180+
);
11621181
}
11631182

11641183
/** Verifies that the public LXD UI completes OIDC login and renders an authenticated management view. */

0 commit comments

Comments
 (0)