Skip to content

Commit d34f729

Browse files
committed
Test ZITADEL username resubmit guard
1 parent f924cfc commit d34f729

2 files changed

Lines changed: 87 additions & 15 deletions

File tree

tests/integration/assertions/browser.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { basename } from "node:path";
22
import { describe, expect, test } from "bun:test";
3+
import { chromium } from "playwright";
34
import {
5+
__browserTestHooks,
46
bodyContainsAnyMarker,
57
bodyContainsDenialText,
68
bodyContainsHttpErrorText,
@@ -199,4 +201,42 @@ describe("browser assertion helpers", () => {
199201
)
200202
).toBe(false);
201203
});
204+
205+
test("resubmits username only while still on the ZITADEL username route", async () => {
206+
const browser = await chromium.launch({ headless: true });
207+
try {
208+
const page = await browser.newPage();
209+
await page.route("**/*", (route) =>
210+
route.fulfill({
211+
contentType: "text/html",
212+
body: [
213+
"<form>",
214+
'<input data-testid="username-text-input" value="">',
215+
'<button type="submit">Continue</button>',
216+
"</form>",
217+
"<script>",
218+
"window.submits = 0;",
219+
"document.querySelector('form').addEventListener('submit', (event) => { event.preventDefault(); window.submits += 1; });",
220+
"</script>"
221+
].join("")
222+
})
223+
);
224+
225+
await page.goto("http://issuer.example.test/ui/v2/login/loginname?requestId=oidc_123");
226+
await __browserTestHooks.resubmitUsernameIfStillOnUsernameStep(page, "agent@example.test");
227+
expect(await page.locator('[data-testid="username-text-input"]').inputValue()).toBe("agent@example.test");
228+
expect(await page.evaluate(() => (window as unknown as { submits: number }).submits)).toBe(1);
229+
230+
await page.goto("http://issuer.example.test/ui/v2/login/password?requestId=oidc_123");
231+
await page.locator('[data-testid="username-text-input"]').fill("");
232+
await page.evaluate(() => {
233+
(window as unknown as { submits: number }).submits = 0;
234+
});
235+
await __browserTestHooks.resubmitUsernameIfStillOnUsernameStep(page, "agent@example.test");
236+
expect(await page.locator('[data-testid="username-text-input"]').inputValue()).toBe("");
237+
expect(await page.evaluate(() => (window as unknown as { submits: number }).submits)).toBe(0);
238+
} finally {
239+
await browser.close();
240+
}
241+
});
202242
});

tests/integration/assertions/browser.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,9 @@ async function waitForPasswordInputAfterUsernameSubmit(page: Page, usernameSelec
117117

118118
if (isUsernameLoginStep(currentUrl) && isIdentityLoginInputPage(currentUrl, targetHost)) {
119119
const now = Date.now();
120-
if (now - lastResubmit > 5000 && await canResubmitUsername(page, usernameSelector)) {
120+
if (now - lastResubmit > 5000) {
121121
lastResubmit = now;
122-
if (isPasswordLoginStep(page.url())) {
123-
continue;
124-
}
125-
await typeInto(page, usernameSelector, userEmail);
126-
if (isPasswordLoginStep(page.url())) {
127-
continue;
128-
}
129-
await submitIdentityForm(page, usernameSelector, USERNAME_SUBMIT_SELECTORS);
122+
await resubmitUsernameIfStillOnUsernameStep(page, userEmail);
130123
await page.waitForTimeout(1000);
131124
continue;
132125
}
@@ -160,15 +153,54 @@ async function visiblePasswordInputSelector(page: Page, timeoutMs: number): Prom
160153
return undefined;
161154
}
162155

163-
async function canResubmitUsername(page: Page, usernameSelector: string): Promise<boolean> {
164-
if (!isUsernameLoginStep(page.url())) {
165-
return false;
166-
}
156+
async function resubmitUsernameIfStillOnUsernameStep(page: Page, userEmail: string): Promise<void> {
157+
await maybeWithTimeout(
158+
page
159+
.evaluate(
160+
({ selectors, value }) => {
161+
if (!location.pathname.toLowerCase().startsWith("/ui/v2/login/loginname")) {
162+
return;
163+
}
164+
165+
const input = selectors
166+
.map((selector) => document.querySelector(selector))
167+
.find((element): element is HTMLInputElement => element instanceof HTMLInputElement && !element.disabled && !element.readOnly);
168+
if (!input) {
169+
return;
170+
}
167171

168-
const locator = page.locator(usernameSelector).first();
169-
return (await locatorVisible(locator)) && (await waitForEditableInput(locator)) && isUsernameLoginStep(page.url());
172+
input.focus();
173+
input.value = value;
174+
input.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" }));
175+
input.dispatchEvent(new Event("change", { bubbles: true }));
176+
177+
const form = input.closest("form");
178+
if (form && typeof form.requestSubmit === "function") {
179+
form.requestSubmit();
180+
return;
181+
}
182+
183+
const submit = Array.from(document.querySelectorAll("button,input"))
184+
.find((element): element is HTMLButtonElement | HTMLInputElement => {
185+
if (!(element instanceof HTMLButtonElement || element instanceof HTMLInputElement) || element.disabled) {
186+
return false;
187+
}
188+
const text = `${element.textContent ?? ""} ${element.value ?? ""}`.toLowerCase();
189+
return element.type === "submit" || text.includes("continue") || text.includes("next") || text.includes("sign in");
190+
});
191+
submit?.click();
192+
},
193+
{ selectors: USERNAME_INPUT_SELECTORS, value: userEmail }
194+
)
195+
.catch(() => undefined),
196+
3000
197+
);
170198
}
171199

200+
export const __browserTestHooks = {
201+
resubmitUsernameIfStillOnUsernameStep
202+
};
203+
172204
function isUsernameLoginStep(currentUrl: string): boolean {
173205
const parsedUrl = parseBrowserUrl(currentUrl);
174206
return parsedUrl?.pathname.toLowerCase().startsWith("/ui/v2/login/loginname") ?? false;

0 commit comments

Comments
 (0)