fix(frontend): mfa test reauth dialogs (#13273)

* fix the stepts that need a password now

* use different selectors to make more robust

* fix popup breaking flow

* support password reauth in reauth
This commit is contained in:
Matthias Mair authored and GitHub committed 2026-11-14 10:07:00 +11:00
1 parent 435cdaafbc
commit de77fc4293
4 files changed
+58 -20

No files matched your search

@@ -25,10 +25,22 @@ import { useServerApiState } from '../../states/ServerApiState';
export function getReauthFlow(err: any): FlowEnum | null {
const flows: any = err.response?.data?.data?.flows ?? [];
if (flows.find((flow: any) => flow.id == FlowEnum.MfaReauthenticate)) {
const mfaFlow = flows.find(
(flow: any) => flow.id == FlowEnum.MfaReauthenticate
);
const mfaSupported = mfaFlow?.types.includes('totp');
const passwordFlow = flows.find(
(flow: any) => flow.id == FlowEnum.Reauthenticate
);
if (mfaSupported) {
return FlowEnum.MfaReauthenticate;
} else if (flows.find((flow: any) => flow.id == FlowEnum.Reauthenticate)) {
// this can happen if you only have webauthn and mail but not totp/recovery
} else if (passwordFlow) {
return FlowEnum.Reauthenticate;
// fallback for other flows
} else if (mfaFlow) {
return FlowEnum.MfaReauthenticate;
} else {
// No match found
return null;
+24
View File
@@ -291,3 +291,27 @@ export const waitForDataOutput = async (
page.off('popup', closePopup);
}
};
/**
* Handle a re-authentication (password) prompt, if one is displayed.
*/
// Handle a re-authentication prompt (if displayed)
export const reauthenticate = async (
page: Page,
password: string,
time = 3000
) => {
const dialog = page.getByRole('dialog', { name: 'Reauthenticate' });
const visible = await dialog
.waitFor({ timeout: time })
.then(() => true)
.catch(() => false);
if (visible) {
await dialog.locator('input[type="password"]').fill(password);
await dialog.getByRole('button', { name: 'Submit', exact: true }).click();
await dialog.waitFor({ state: 'hidden' });
}
return visible;
};
+13 -1
View File
@@ -2,7 +2,7 @@ import type { Locator, Page } from '@playwright/test';
import { TOTP } from 'otpauth';
import { expect, test } from './baseFixtures.js';
import { engineeruser, logoutUrl, mfauser, noaccessuser } from './defaults.js';
import { navigate, openDetailAction } from './helpers.js';
import { navigate, openDetailAction, reauthenticate } from './helpers.js';
import { doLogin } from './login.js';
const stripQueryAndHash = (url: string): string => {
@@ -487,10 +487,21 @@ test('Login - MFA - TOTP', async ({ page }) => {
await page.getByRole('textbox', { name: 'text-input-otp-code' }).fill(token);
await page.getByRole('button', { name: 'Submit', exact: true }).click();
// re-authentication; needs a new login
const submitButton = page.getByRole('button', {
name: 'Submit',
exact: true
});
if (await reauthenticate(page, noaccessuser.testcred ?? '', 5000)) {
await submitButton.click();
}
await page.getByText('TOTP token registered successfully').waitFor();
// View recovery codes
await page.getByRole('button', { name: 'view-recovery-codes' }).click();
await reauthenticate(page, noaccessuser.testcred ?? '');
await page
.getByText('The following one time recovery codes are available')
.waitFor();
@@ -499,6 +510,7 @@ test('Login - MFA - TOTP', async ({ page }) => {
// Remove TOTP token
await page.getByRole('button', { name: 'remove-totp' }).click();
await page.getByRole('button', { name: 'Remove', exact: true }).click();
await reauthenticate(page, noaccessuser.testcred ?? '');
await page.getByText('TOTP token removed successfully').waitFor();
+7 -17
View File
@@ -9,7 +9,7 @@ import type { Page } from '@playwright/test';
import { createApi } from './api.js';
import { expect, generateUUID, test } from './baseFixtures.js';
import { logoutUrl } from './defaults.js';
import { navigate } from './helpers.js';
import { navigate, reauthenticate } from './helpers.js';
import { doLogin } from './login.js';
// Attach a virtual authenticator to the provided page
@@ -51,20 +51,6 @@ const createUser = async (username: string, password: string) => {
return user.pk;
};
// Handle a re-authentication prompt (if displayed)
const reauthenticate = async (page: Page, password: string) => {
const field = page.getByLabel('text-field-password');
const visible = await field
.waitFor({ timeout: 3000 })
.then(() => true)
.catch(() => false);
if (visible) {
await field.fill(password);
await page.getByRole('button', { name: 'Submit' }).click();
}
};
test('Login - MFA - WebAuthn', async ({ page, browserName }) => {
// The virtual authenticator requires the Chrome DevTools Protocol
test.skip(
@@ -97,7 +83,9 @@ test('Login - MFA - WebAuthn', async ({ page, browserName }) => {
.getByRole('button', { name: 'Multi-Factor Authentication' })
.click();
await page.getByRole('button', { name: 'add-factor-webauthn' }).click();
await reauthenticate(page, password);
if (await reauthenticate(page, password)) {
await page.getByRole('button', { name: 'add-factor-webauthn' }).click();
}
await page
.getByText('WebAuthn credential registered successfully')
.waitFor();
@@ -125,7 +113,9 @@ test('Login - MFA - WebAuthn', async ({ page, browserName }) => {
await page.getByRole('button', { name: 'remove-webauthn' }).click();
await page.getByText('Remove WebAuthn Credential').waitFor();
await page.getByRole('button', { name: 'Remove', exact: true }).click();
await reauthenticate(page, password);
if (await reauthenticate(page, password)) {
await page.getByRole('button', { name: 'Remove', exact: true }).click();
}
await page.getByText('WebAuthn credential removed successfully').waitFor();
} finally {
const api = await createApi({});