Browse Source

Merge 88ab51443a into d6a3d539ed

pull/7248/merge
Zaid Marji 1 week ago
committed by GitHub
parent
commit
94614b5e06
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      .gitignore
  2. 3
      playwright/.env.template
  3. 12
      playwright/compose/warden/Dockerfile
  4. 2
      playwright/global-utils.ts
  5. 1
      playwright/playwright.config.ts
  6. 3
      playwright/test.env
  7. 21
      playwright/tests/login.smtp.spec.ts
  8. 19
      playwright/tests/login.spec.ts
  9. 13
      playwright/tests/organization.smtp.spec.ts
  10. 134
      playwright/tests/setups/2fa.ts
  11. 20
      playwright/tests/setups/orgs.ts
  12. 32
      playwright/tests/setups/sso.ts
  13. 82
      playwright/tests/setups/user.ts
  14. 2
      playwright/tests/sso_login.smtp.spec.ts
  15. 2
      playwright/tests/sso_login.spec.ts
  16. 9
      playwright/tests/sso_organization.smtp.spec.ts
  17. 2
      playwright/tests/sso_organization.spec.ts

6
.gitignore

@ -14,3 +14,9 @@ data
# Web vault
web-vault
# Playwright artifacts (the suite is invoked from playwright/ so its
# own .gitignore catches in-tree paths; this catches stray artifacts
# that end up at the repo root when playwright is invoked elsewhere).
test-results
playwright-report

3
playwright/.env.template

@ -39,7 +39,8 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
######################
ROCKET_ADDRESS=0.0.0.0
ROCKET_PORT=8000
DOMAIN=http://localhost:${ROCKET_PORT}
ROCKET_TLS={certs="/certs/cert.pem",key="/certs/key.pem"}
DOMAIN=https://localhost:${ROCKET_PORT}
LOG_LEVEL=info,oidcwarden::sso=debug
I_REALLY_WANT_VOLATILE_STORAGE=true

12
playwright/compose/warden/Dockerfile

@ -29,6 +29,18 @@ RUN mkdir /data && \
openssl && \
rm -rf /var/lib/apt/lists/*
# Self-signed TLS cert for the test server. The bundled web vault refuses
# to submit registration/login over HTTP ("Insecure URL not allowed");
# Rocket needs a cert+key to serve HTTPS. Self-contained layer so cert
# tweaks don't bust the apt-install layer above.
RUN mkdir /certs && \
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout /certs/key.pem \
-out /certs/cert.pem \
-days 3650 \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
WORKDIR /

2
playwright/global-utils.ts

@ -38,7 +38,7 @@ export async function waitFor(url: String, browser: Browser) {
do {
try {
context = await browser.newContext();
context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
await page.waitForTimeout(500);
const result = await page.goto(url);

1
playwright/playwright.config.ts

@ -35,6 +35,7 @@ export default defineConfig({
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.DOMAIN,
browserName: 'firefox',
ignoreHTTPSErrors: true,
locale: 'en-GB',
timezoneId: 'Europe/London',

3
playwright/test.env

@ -52,7 +52,8 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}
# Vaultwarden Config #
######################
ROCKET_PORT=8003
DOMAIN=http://localhost:${ROCKET_PORT}
ROCKET_TLS={certs="/certs/cert.pem",key="/certs/key.pem"}
DOMAIN=https://localhost:${ROCKET_PORT}
LOG_LEVEL=info,oidcwarden::sso=debug
LOGIN_RATELIMIT_MAX_BURST=100
ADMIN_TOKEN=admin

21
playwright/tests/login.smtp.spec.ts

@ -3,7 +3,7 @@ import { MailDev } from 'maildev';
const utils = require('../global-utils');
import { createAccount, logUser } from './setups/user';
import { activateEmail, retrieveEmailCode, disableEmail } from './setups/2fa';
import { activateEmail, disableEmail } from './setups/2fa';
let users = utils.loadEnv();
@ -41,7 +41,7 @@ test('Account creation', async ({ page }) => {
test('Login', async ({ context, page }) => {
const mailBuffer = mailserver.buffer(users.user1.email);
await logUser(test, page, users.user1, mailBuffer);
await logUser(test, page, users.user1, { mailBuffer });
await test.step('verify email', async () => {
await page.getByText('Verify your account\'s email').click();
@ -78,24 +78,13 @@ test('Activate 2fa', async ({ page }) => {
test('2fa', async ({ page }) => {
const emails = mailserver.buffer(users.user1.email);
await test.step('login', async () => {
await page.goto('/');
await page.getByLabel(/Email address/).fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
const code = await retrieveEmailCode(test, page, emails);
await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click();
await logUser(test, page, users.user1, { twoFactor: { kind: 'mail2fa', mailBuffer: emails }, mailBuffer: emails });
await test.step('Dismiss extension prompts', async () => {
await page.getByRole('button', { name: 'Add it later' }).click();
await page.getByRole('link', { name: 'Skip to web app' }).click();
await expect(page).toHaveTitle(/Vaults/);
})
});
await disableEmail(test, page, users.user1);

19
playwright/tests/login.spec.ts

@ -1,5 +1,4 @@
import { test, expect, type Page, type TestInfo } from '@playwright/test';
import * as OTPAuth from "otpauth";
import { test, type TestInfo } from '@playwright/test';
import * as utils from "../global-utils";
import { createAccount, logUser } from './setups/user';
@ -31,21 +30,7 @@ test('Authenticator 2fa', async ({ page }) => {
await utils.logout(test, page, users.user1);
await test.step('login', async () => {
let timestamp = Date.now(); // Needed to use the next token
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
await page.getByLabel(/Email address/).fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page).toHaveTitle(/Vaultwarden Web/);
});
await logUser(test, page, users.user1, { twoFactor: { kind: 'totp', totp } });
await disableTOTP(test, page, users.user1);
});

13
playwright/tests/organization.smtp.spec.ts

@ -3,7 +3,7 @@ import { MailDev } from 'maildev';
import * as utils from '../global-utils';
import * as orgs from './setups/orgs';
import { createAccount, logUser } from './setups/user';
import { createAccount, fillNewMasterPassword, logUser } from './setups/user';
let users = utils.loadEnv();
@ -57,12 +57,11 @@ test('invited with new account', async ({ page }) => {
await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);
//await page.getByLabel('Name').fill(users.user2.name);
await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);
await page.getByLabel('Confirm master password (').fill(users.user2.password);
await fillNewMasterPassword(page, users.user2.password);
await page.getByRole('button', { name: 'Create account' }).click();
await utils.checkNotification(page, 'Your new account has been created');
await utils.checkNotification(page, 'Invitation accepted');
await utils.checkNotification(page, 'Successfully accepted your invitation');
await utils.ignoreExtension(page);
// Redirected to the vault
@ -93,7 +92,7 @@ test('invited with existing account', async ({ page }) => {
await page.getByLabel('Master password').fill(users.user3.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
await utils.checkNotification(page, 'Invitation accepted');
await utils.checkNotification(page, 'Successfully accepted your invitation');
await utils.ignoreExtension(page);
// We are now in the default vault page
@ -104,7 +103,7 @@ test('invited with existing account', async ({ page }) => {
});
test('Confirm invited user', async ({ page }) => {
await logUser(test, page, users.user1, mail1Buffer);
await logUser(test, page, users.user1, { mailBuffer: mail1Buffer });
await orgs.members(test, page, 'Test');
await orgs.confirm(test, page, 'Test', users.user2.email);
@ -113,7 +112,7 @@ test('Confirm invited user', async ({ page }) => {
});
test('Organization is visible', async ({ page }) => {
await logUser(test, page, users.user2, mail2Buffer);
await logUser(test, page, users.user2, { mailBuffer: mail2Buffer });
await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
});

134
playwright/tests/setups/2fa.ts

@ -3,39 +3,106 @@ import { type MailBuffer } from 'maildev';
import * as OTPAuth from "otpauth";
import * as utils from '../../global-utils';
import { openAvatarMenu, submitMasterPasswordVerification } from './user';
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
/**
* A 2FA challenge factor used by the login helpers. Discriminated by `kind`
* so each variant carries only the state it needs:
* - `totp` TOTP code generator (authenticator app)
* - `mail2fa` email OTP; the helper retrieves the code from `mailBuffer`
* - `fido2` WebAuthn-as-2FA (the bundled web vault labels this
* provider "FIDO2 WebAuthn" in en_GB / "Passkey" in en).
* Currently unimplemented; `submitTwoFactor` throws.
*/
export type TwoFactor =
| { kind: 'totp', totp: OTPAuth.TOTP }
| { kind: 'mail2fa', mailBuffer: MailBuffer }
| { kind: 'fido2' };
/**
* Satisfy the /#/2fa challenge for the given `TwoFactor`. Asserts the
* "Verify your Identity" heading is shown, then dispatches per `kind`:
* - `totp` / `mail2fa`: fill the verification code, click Continue.
* - `fido2`: throws Not Implemented.
*
* For TOTP, the code is generated for the *next* period boundary to avoid
* server-side expiry races when the test submits near a 30-second tick.
*/
export async function submitTwoFactor(test: Test, page: Page, twoFactor: TwoFactor): Promise<void> {
await test.step(`Submit 2FA (${twoFactor.kind})`, async () => {
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
switch (twoFactor.kind) {
case 'totp': {
const { totp } = twoFactor;
const nowSec = Math.floor(Date.now() / 1000);
const timestamp = (nowSec + totp.period - (nowSec % totp.period) + 1) * 1000;
await page.getByLabel(/Verification code/).fill(totp.generate({ timestamp }));
await page.getByRole('button', { name: 'Continue' }).click();
break;
}
case 'mail2fa': {
const code = await retrieveEmailCode(test, page, twoFactor.mailBuffer);
await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click();
break;
}
case 'fido2':
throw new Error('Not Implemented');
}
});
}
/**
* Navigate to the two-step-login provider list under Settings Security.
* Centralised here so a future web-vault nav restructure only touches one
* call chain.
*/
export async function gotoTwoStepLogin(page: Page, userName: string) {
await openAvatarMenu(page, userName);
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click();
}
/**
* Click the "Manage" button on a 2FA provider row identified by a substring
* of its label (e.g. /Authenticator app/, /Passkey/, 'Email'). The Manage
* dialog typically opens with the user-verification gate as its first step.
*/
export async function clickTwoFactorProviderManage(page: Page, providerLabel: string | RegExp) {
await page.locator('bit-item').filter({ hasText: providerLabel }).first().getByRole('button').first().click();
}
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): Promise<OTPAuth.TOTP> {
return await test.step('Activate TOTP 2FA', async () => {
await page.getByRole('button', { name: user.name }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click();
const secret = await page.getByLabel('Key').innerText();
await gotoTwoStepLogin(page, user.name);
await clickTwoFactorProviderManage(page, /Authenticator app/);
await submitMasterPasswordVerification(page, user.password);
// `getByLabel('Key')` alone is ambiguous: the providers list also
// has a Yubico SVG with aria-label "Yubico OTP security key" that
// matches "Key" via substring. Anchor with exact match.
const secret = (await page.getByLabel('Key', { exact: true }).innerText()).replace(/\s+/g, '');
let totp = new OTPAuth.TOTP({ secret, period: 30 });
await page.getByLabel(/Verification code/).fill(totp.generate());
await page.getByRole('button', { name: 'Turn on' }).click();
await page.getByRole('heading', { name: 'Turned on', exact: true });
await page.getByLabel('Close').click();
// Wait for the activation request to complete. The current
// bundled web vault uses an asynchronous Turn-on flow; we don't
// try to assert the exact success-heading text (it varies across
// vault versions) — instead we wait for network to settle, then
// the dialog closes itself.
await page.waitForLoadState('networkidle');
return totp;
})
}
export async function disableTOTP(test: Test, page: Page, user: { password: string }) {
export async function disableTOTP(test: Test, page: Page, user: { name: string, password: string }) {
await test.step('Disable TOTP 2FA', async () => {
await page.getByRole('button', { name: 'Test' }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
await page.getByLabel('Master password (required)').click();
await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click();
await gotoTwoStepLogin(page, user.name);
await clickTwoFactorProviderManage(page, /Authenticator app/);
await submitMasterPasswordVerification(page, user.password);
await page.getByRole('button', { name: 'Turn off' }).click();
await page.getByRole('button', { name: 'Yes' }).click();
await utils.checkNotification(page, 'Two-step login provider turned off');
@ -44,13 +111,9 @@ export async function disableTOTP(test: Test, page: Page, user: { password: stri
export async function activateEmail(test: Test, page: Page, user: { name: string, password: string }, mailBuffer: MailBuffer) {
await test.step('Activate Email 2FA', async () => {
await page.getByRole('button', { name: user.name }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click();
await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click();
await gotoTwoStepLogin(page, user.name);
await clickTwoFactorProviderManage(page, 'Enter a code sent to your email');
await submitMasterPasswordVerification(page, user.password);
await page.getByRole('button', { name: 'Send email' }).click();
});
@ -63,7 +126,7 @@ export async function activateEmail(test: Test, page: Page, user: { name: string
});
}
export async function retrieveEmailCode(test: Test, page: Page, mailBuffer: MailBuffer): string {
export async function retrieveEmailCode(test: Test, page: Page, mailBuffer: MailBuffer): Promise<string> {
return await test.step('retrieve code', async () => {
const codeMail = await mailBuffer.expect((mail) => mail.subject.includes("Login Verification Code"));
const page2 = await page.context().newPage();
@ -74,16 +137,11 @@ export async function retrieveEmailCode(test: Test, page: Page, mailBuffer: Mail
});
}
export async function disableEmail(test: Test, page: Page, user: { password: string }) {
export async function disableEmail(test: Test, page: Page, user: { name: string, password: string }) {
await test.step('Disable Email 2FA', async () => {
await page.getByRole('button', { name: 'Test' }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click();
await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
await page.getByLabel('Master password (required)').click();
await page.getByLabel('Master password (required)').fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click();
await gotoTwoStepLogin(page, user.name);
await clickTwoFactorProviderManage(page, 'Email');
await submitMasterPasswordVerification(page, user.password);
await page.getByRole('button', { name: 'Turn off' }).click();
await page.getByRole('button', { name: 'Yes' }).click();

20
playwright/tests/setups/orgs.ts

@ -4,7 +4,11 @@ import * as utils from '../../global-utils';
export async function create(test, page: Page, name: string) {
await test.step('Create Org', async () => {
await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();
// The product-switch nav links are icon-only (accessible name set on
// the link, no text content), so `filter({hasText})` no longer matches
// on the current bundled web vault — use the accessible-name role
// selector instead.
await page.getByRole('link', { name: 'Password Manager' }).click();
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
await page.getByRole('link', { name: 'New organisation' }).click();
await page.getByLabel('Organisation name (required)').fill(name);
@ -16,9 +20,11 @@ export async function create(test, page: Page, name: string) {
export async function policies(test, page: Page, name: string) {
await test.step(`Navigate to ${name} policies`, async () => {
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
await page.getByRole('link', { name: 'Admin Console' }).first().click();
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
// The org row in the switcher has a hover tooltip that intercepts the
// click on the current bundled web vault; force-click past it.
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click({ force: true });
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
await page.getByRole('link', { name: 'Policies' }).click();
@ -28,11 +34,13 @@ export async function policies(test, page: Page, name: string) {
export async function members(test, page: Page, name: string) {
await test.step(`Navigate to ${name} members`, async () => {
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
await page.getByRole('link', { name: 'Admin Console' }).first().click();
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
// The org row in the switcher has a hover tooltip that intercepts the
// click on the current bundled web vault; force-click past it.
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click({ force: true });
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
await page.getByRole('link', { name: 'Members' }).click();
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();
});

32
playwright/tests/setups/sso.ts

@ -1,9 +1,9 @@
import { expect, type Page, Test } from '@playwright/test';
import { type MailBuffer, MailServer } from 'maildev';
import * as OTPAuth from "otpauth";
import * as utils from '../../global-utils';
import { retrieveEmailCode } from './2fa';
import { submitTwoFactor, type TwoFactor } from './2fa';
import { fillNewMasterPassword } from './user';
/**
* If a MailBuffer is passed it will be used and consume the expected emails
@ -33,8 +33,7 @@ export async function logNewUser(
await test.step('Create Vault account', async () => {
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);
await page.getByLabel('Confirm master password (').fill(user.password);
await fillNewMasterPassword(page, user.password);
await page.getByRole('button', { name: 'Create account' }).click();
});
@ -66,9 +65,8 @@ export async function logUser(
page: Page,
user: { email: string, password: string },
options: {
mailBuffer ?: MailBuffer,
totp?: OTPAuth.TOTP,
mail2fa?: boolean,
mailBuffer?: MailBuffer,
twoFactor?: TwoFactor,
} = {}
) {
let mailBuffer = options.mailBuffer;
@ -90,24 +88,8 @@ export async function logUser(
await page.getByRole('button', { name: 'Sign In' }).click();
});
if( options.totp || options.mail2fa ){
let code;
await test.step('2FA check', async () => {
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
if( options.totp ) {
const totp = options.totp;
let timestamp = Date.now(); // Needed to use the next token
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
code = totp.generate({timestamp});
} else if( options.mail2fa ){
code = await retrieveEmailCode(test, page, mailBuffer);
}
await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click();
});
if( options.twoFactor ){
await submitTwoFactor(test, page, options.twoFactor);
}
await test.step('Unlock vault', async () => {

82
playwright/tests/setups/user.ts

@ -3,6 +3,56 @@ import { expect, type Browser, Page } from '@playwright/test';
import { type MailBuffer } from 'maildev';
import * as utils from '../../global-utils';
import { submitTwoFactor, type TwoFactor } from './2fa';
/**
* Open the account/avatar menu in the web vault header. The button's
* accessible name is the user's display name; centralising the locator here
* insulates callers from web-vault changes to that element's structure.
*
* Note: cipher rows also expose `aria-haspopup="menu"` ellipsis buttons, so
* naïve `aria-haspopup` selectors mis-target on the vault page. Anchor on the
* accessible-name (`{ exact: true }` to avoid substring matches against any
* cipher whose name happens to start with the user's display name).
*/
export async function openAvatarMenu(page: Page, userName: string) {
await page.getByRole('button', { name: userName, exact: true }).click();
}
/**
* Fill the registration / change-master-password form's "new" + "confirm new"
* master-password fields. Anchored by `formcontrolname` rather than label
* the current bundled web-vault renders three labels matching "Master
* password" via Playwright's case-insensitive substring matching ("Master
* password (required)", "Confirm master password (required)", and "Master
* password hint"), so a label-based locator is ambiguous.
*/
export async function fillNewMasterPassword(page: Page, password: string) {
await page.locator('input[formcontrolname="newPassword"]').fill(password);
await page.locator('input[formcontrolname="newPasswordConfirm"]').fill(password);
}
/**
* Submit the in-dialog user-verification (`app-user-verification`) master-
* password gate that the bundled web vault renders before any sensitive
* operation (2FA enrol/disable, passkey enrol/remove, key rotation, KDF
* change).
*
* Pressing Enter inside the password input submits the form unambiguously
* the surrounding page often has multiple `Continue` buttons (dialog action,
* stale settings header), so a button-text click is brittle.
*
* Note: the user-verification component falls back to email-OTP verification
* when the master password isn't "fresh" in the current session (e.g. after a
* passkey login). Callers reaching this helper from a post-passkey-login
* state must arrange a recent MP entry first.
*/
export async function submitMasterPasswordVerification(page: Page, masterPassword: string) {
const mpInput = page.locator('input#masterPassword');
await mpInput.waitFor({ state: 'visible' });
await mpInput.fill(masterPassword);
await mpInput.press('Enter');
}
export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {
await test.step(`Create user ${user.name}`, async () => {
@ -16,9 +66,7 @@ export async function createAccount(test, page: Page, user: { email: string, nam
await page.getByLabel('Name').fill(user.name);
await page.getByRole('button', { name: 'Continue' }).click();
// Vault finish Creation
await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);
await page.getByLabel('Confirm master password (').fill(user.password);
await fillNewMasterPassword(page, user.password);
await page.getByRole('button', { name: 'Create account' }).click();
await utils.checkNotification(page, 'Your new account has been created')
@ -35,7 +83,29 @@ export async function createAccount(test, page: Page, user: { email: string, nam
});
}
export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {
/**
* Master-password login.
*
* When the account has 2FA enabled, pass `options.twoFactor` a
* `TwoFactor` discriminated union carrying the factor's own state (TOTP
* generator, mail buffer, ). The helper then drives the /#/2fa challenge
* inline and lands the user in `/vault`. Mirrors `setups/sso.ts:logUser`.
*
* `options.mailBuffer` (independent of `twoFactor`) consumes the expected
* "New Device Logged In" mail at the end of the flow, when the test wants
* to assert that login emails went out.
*/
export async function logUser(
test,
page: Page,
user: { email: string, password: string },
options: {
mailBuffer?: MailBuffer,
twoFactor?: TwoFactor,
} = {},
) {
let mailBuffer = options.mailBuffer;
await test.step(`Log user ${user.email}`, async () => {
await utils.cleanLanding(page);
@ -46,6 +116,10 @@ export async function logUser(test, page: Page, user: { email: string, password:
await page.getByLabel('Master password').fill(user.password);
await page.getByRole('button', { name: 'Log in with master password' }).click();
if( options.twoFactor ){
await submitTwoFactor(test, page, options.twoFactor);
}
await utils.ignoreExtension(page);
// We are now in the default vault page

2
playwright/tests/sso_login.smtp.spec.ts

@ -45,7 +45,7 @@ test('Create and activate 2FA', async ({ page }) => {
test('Log and disable', async ({ page }) => {
const mailBuffer = mailserver.buffer(users.user1.email);
await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true});
await logUser(test, page, users.user1, { mailBuffer, twoFactor: { kind: 'mail2fa', mailBuffer } });
await disableEmail(test, page, users.user1);

2
playwright/tests/sso_login.spec.ts

@ -45,7 +45,7 @@ test('SSO login with TOTP 2fa', async ({ page }) => {
let totp = await activateTOTP(test, page, users.user1);
await logUser(test, page, users.user1, { totp });
await logUser(test, page, users.user1, { twoFactor: { kind: 'totp', totp } });
await disableTOTP(test, page, users.user1);
});

9
playwright/tests/sso_organization.smtp.spec.ts

@ -4,6 +4,7 @@ import { MailDev } from 'maildev';
import * as utils from "../global-utils";
import * as orgs from './setups/orgs';
import { logNewUser, logUser } from './setups/sso';
import { fillNewMasterPassword } from './setups/user';
let users = utils.loadEnv();
@ -67,8 +68,7 @@ test('invited with new account', async ({ page }) => {
await test.step('Create Vault account', async () => {
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);
await page.getByLabel('Confirm master password (').fill(users.user2.password);
await fillNewMasterPassword(page, users.user2.password);
await page.getByRole('button', { name: 'Create account' }).click();
await utils.checkNotification(page, 'Account successfully created!');
@ -95,6 +95,9 @@ test('invited with existing account', async ({ page }) => {
await test.step('Redirect to Keycloak', async () => {
await page.goto(link);
// Existing accounts land on the email-prefilled login form rather
// than auto-redirecting; click through to Keycloak explicitly.
await page.getByRole('button', { name: 'Use single sign-on' }).click();
});
await test.step('Keycloak login', async () => {
@ -109,7 +112,7 @@ test('invited with existing account', async ({ page }) => {
await page.getByLabel('Master password').fill(users.user3.password);
await page.getByRole('button', { name: 'Unlock' }).click();
await utils.checkNotification(page, 'Invitation accepted');
await utils.checkNotification(page, 'Successfully accepted your invitation');
await utils.ignoreExtension(page);
});

2
playwright/tests/sso_organization.spec.ts

@ -68,7 +68,7 @@ test('Enforce password policy', async ({ page }) => {
await page.locator("input[type=email].vw-email-sso").fill(users.user1.email);
await page.getByRole('button', { name: 'Use single sign-on' }).click();
await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
await page.getByLabel('Master password').fill(users.user1.password);
await page.getByRole('button', { name: 'Unlock' }).click();
await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();

Loading…
Cancel
Save