diff --git a/packages/tests-e2e/src/connect/fixtures/BaseTest.ts b/packages/tests-e2e/src/connect/fixtures/BaseTest.ts index 19c5cbb73..bc0688907 100644 --- a/packages/tests-e2e/src/connect/fixtures/BaseTest.ts +++ b/packages/tests-e2e/src/connect/fixtures/BaseTest.ts @@ -1,14 +1,19 @@ import { test as base } from '@playwright/test'; import { BaseModel } from '../models/BaseModel'; +import { CDPSessionManager } from '../utils/CDPSessionManager'; import { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; +import { NetworkRequestBlocker } from '../utils/NetworkRequestBlocker'; export const test = base.extend<{ model: BaseModel }>({ model: async ({ page }, use) => { - const authenticator = new VirtualAuthenticator(); - await authenticator.initializeCDPSession(page); + const cdpManager = new CDPSessionManager(); + await cdpManager.initialize(page); - const model = new BaseModel(page, authenticator); + const authenticator = new VirtualAuthenticator(cdpManager); + const blocker = new NetworkRequestBlocker(cdpManager); + + const model = new BaseModel(page, authenticator, blocker); await use(model); }, diff --git a/packages/tests-e2e/src/connect/models/AppendModel.ts b/packages/tests-e2e/src/connect/models/AppendModel.ts index f17b0c7f9..20efe4530 100644 --- a/packages/tests-e2e/src/connect/models/AppendModel.ts +++ b/packages/tests-e2e/src/connect/models/AppendModel.ts @@ -1,5 +1,7 @@ import type { Page } from '@playwright/test'; +import { ErrorTexts } from '../utils/Constants'; +import { expectError } from '../utils/ExpectScreen'; import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; export class AppendModel { @@ -11,16 +13,22 @@ export class AppendModel { this.authenticator = authenticator; } - async appendPasskey(): Promise { + appendPasskey(complete: boolean) { const operationTrigger = () => this.page.getByRole('button', { name: 'Continue' }).click(); - await this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + if (complete) { + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } else { + return this.authenticator.startAndCancelPasskeyOperation(operationTrigger, () => + expectError(this.page, ErrorTexts.CancelledPasskey), + ); + } } - confirmAppended(): Promise { + confirmAppended() { return this.page.getByRole('button', { name: 'Continue' }).click(); } - skipAppend(): Promise { + skipAppend() { return this.page.locator('.cb-append-skip').click(); } } diff --git a/packages/tests-e2e/src/connect/models/BaseModel.ts b/packages/tests-e2e/src/connect/models/BaseModel.ts index b68e29351..55c8227d4 100644 --- a/packages/tests-e2e/src/connect/models/BaseModel.ts +++ b/packages/tests-e2e/src/connect/models/BaseModel.ts @@ -1,7 +1,9 @@ -import { Page } from '@playwright/test'; +import type { Page } from '@playwright/test'; -import { ErrorTexts, ScreenNames } from '../utils/Constants'; -import { expectScreen, expectError } from '../utils/ExpectScreen'; +import type { ErrorTexts } from '../utils/Constants'; +import { ScreenNames } from '../utils/Constants'; +import { expectError, expectScreen } from '../utils/ExpectScreen'; +import type { NetworkRequestBlocker } from '../utils/NetworkRequestBlocker'; import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; import { AppendModel } from './AppendModel'; import { HomeModel } from './HomeModel'; @@ -12,6 +14,7 @@ import { SignupModel } from './SignupModel'; export class BaseModel { page: Page; authenticator: VirtualAuthenticator; + blocker: NetworkRequestBlocker; signup: SignupModel; login: LoginModel; append: AppendModel; @@ -19,9 +22,10 @@ export class BaseModel { passkeyList: PasskeyListModel; email = ''; - constructor(page: Page, authenticator: VirtualAuthenticator) { + constructor(page: Page, authenticator: VirtualAuthenticator, blocker: NetworkRequestBlocker) { this.page = page; this.authenticator = authenticator; + this.blocker = blocker; this.signup = new SignupModel(page); this.login = new LoginModel(page, authenticator); this.append = new AppendModel(page, authenticator); @@ -29,14 +33,6 @@ export class BaseModel { this.passkeyList = new PasskeyListModel(page, authenticator); } - addWebAuthn() { - return this.authenticator.addWebAuthn(); - } - - removeWebAuthn() { - return this.authenticator.removeWebAuthn(); - } - loadInvitationToken() { return this.page.goto('/login?invitationToken=inv-token-correct'); } @@ -49,6 +45,10 @@ export class BaseModel { return this.page.goto('/login'); } + loadHome() { + return this.page.goto('/home'); + } + expectScreen(screenName: ScreenNames) { return expectScreen(this.page, screenName); } @@ -63,7 +63,7 @@ export class BaseModel { if (invited) { await this.expectScreen(ScreenNames.PasskeyAppend); if (append) { - await this.append.appendPasskey(); + await this.append.appendPasskey(true); await this.expectScreen(ScreenNames.PasskeyAppended); await this.append.confirmAppended(); } else { @@ -90,4 +90,9 @@ export class BaseModel { // // expect(response.ok).toBeTruthy(); // } + + async clearLocalStorageAndCookies() { + await this.page.evaluate(() => localStorage.clear()); + await this.page.context().clearCookies(); + } } diff --git a/packages/tests-e2e/src/connect/models/LoginModel.ts b/packages/tests-e2e/src/connect/models/LoginModel.ts index e31a921b7..bdc9114df 100644 --- a/packages/tests-e2e/src/connect/models/LoginModel.ts +++ b/packages/tests-e2e/src/connect/models/LoginModel.ts @@ -1,9 +1,9 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; -import { expectScreen } from '../utils/ExpectScreen'; import { ScreenNames } from '../utils/Constants'; +import { expectScreen } from '../utils/ExpectScreen'; +import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; export class LoginModel { page: Page; @@ -62,4 +62,8 @@ export class LoginModel { await this.page.getByPlaceholder('Password').fill(password); await this.page.getByRole('button', { name: 'Login' }).click(); } + + submitConditionalUI(operationTrigger: () => Promise) { + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } } diff --git a/packages/tests-e2e/src/connect/models/PasskeyListModel.ts b/packages/tests-e2e/src/connect/models/PasskeyListModel.ts index eab308022..acbc2b1c5 100644 --- a/packages/tests-e2e/src/connect/models/PasskeyListModel.ts +++ b/packages/tests-e2e/src/connect/models/PasskeyListModel.ts @@ -16,24 +16,29 @@ export class PasskeyListModel { return expect(this.page.locator('.cb-passkey-list-item')).toHaveCount(n); } - async deletePasskey(index: number): Promise { + async deletePasskey(index: number) { await this.page.locator('.cb-passkey-list-item-delete-icon').nth(index).click(); await this.page.getByRole('button', { name: 'Delete' }).click(); } - appendPasskey(complete: boolean): Promise { - const operationTrigger: () => Promise = (): Promise => - this.page.getByRole('button', { name: 'Add a passkey' }).click(); + createPasskey(complete: boolean, postOperationCheck: (() => Promise) | null = null) { + const operationTrigger = (): Promise => this.page.getByRole('button', { name: 'Add a passkey' }).click(); if (complete) { - return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + if (postOperationCheck === null) { + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } else { + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger, postOperationCheck); + } } else { - return this.authenticator.startAndCancelPasskeyOperation( - operationTrigger, - (): Promise => - expect(this.page.locator('.cb-notification-text')).toHaveText( - 'You have cancelled setting up your passkey. Please try again.', - ), + return this.authenticator.startAndCancelPasskeyOperation(operationTrigger, () => + expect(this.page.locator('.cb-notification-text')).toHaveText( + 'You have cancelled setting up your passkey. Please try again.', + ), ); } } + + confirmModal() { + return this.page.getByRole('button', { name: 'Okay' }).click(); + } } diff --git a/packages/tests-e2e/src/connect/scenarios/append.spec.ts b/packages/tests-e2e/src/connect/scenarios/append.spec.ts index 9afd56cc3..3c1f032e8 100644 --- a/packages/tests-e2e/src/connect/scenarios/append.spec.ts +++ b/packages/tests-e2e/src/connect/scenarios/append.spec.ts @@ -1,17 +1,48 @@ import { test } from '../fixtures/BaseTest'; -import { ScreenNames } from '../utils/Constants'; -import { loadPasskeyAppend, setupUser, setupVirtualAuthenticator } from './hooks'; +import { password, ScreenNames } from '../utils/Constants'; +import { loadPasskeyAppend, setupNetworkBlocker, setupUser, setupVirtualAuthenticator } from './hooks'; test.describe('append component', () => { setupVirtualAuthenticator(test); + setupNetworkBlocker(test); setupUser(test, true, false); loadPasskeyAppend(test); test('successful passkey append on login', async ({ model }) => { - await model.append.appendPasskey(); + await model.append.appendPasskey(true); await model.expectScreen(ScreenNames.PasskeyAppended); await model.append.confirmAppended(); await model.expectScreen(ScreenNames.Home); }); + + test('failed passkey append on login', async ({ model }) => { + await model.append.appendPasskey(false); + }); + + test('Corbado FAPI unavailable after authentication', async ({ model }) => { + await model.blocker.blockCorbadoFAPIFinishEndpoint(); + + await model.append.appendPasskey(true); + await model.expectScreen(ScreenNames.Home); + }); +}); + +test.describe('skip append component', () => { + setupVirtualAuthenticator(test); + setupNetworkBlocker(test); + setupUser(test, true, false); + + test('Corbado FAPI unavailable', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail(model.email, false); + await model.expectScreen(ScreenNames.InitLoginFallback); + + await model.blocker.blockCorbadoFAPI(); + + await model.login.submitFallbackCredentials(model.email, password, true); + await model.expectScreen(ScreenNames.Home); + }); }); diff --git a/packages/tests-e2e/src/connect/scenarios/hooks.ts b/packages/tests-e2e/src/connect/scenarios/hooks.ts index 8abea9824..5368f699e 100644 --- a/packages/tests-e2e/src/connect/scenarios/hooks.ts +++ b/packages/tests-e2e/src/connect/scenarios/hooks.ts @@ -16,11 +16,22 @@ export function setupVirtualAuthenticator( >, ) { test.beforeEach(async ({ model }) => { - await model.addWebAuthn(); + await model.authenticator.addWebAuthn(); }); test.afterEach(async ({ model }) => { - await model.removeWebAuthn(); + await model.authenticator.removeWebAuthn(); + }); +} + +export function setupNetworkBlocker( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.blocker.enableBlocking(); }); } diff --git a/packages/tests-e2e/src/connect/scenarios/login.spec.ts b/packages/tests-e2e/src/connect/scenarios/login.spec.ts index 8dda5d79d..54fb22bf9 100644 --- a/packages/tests-e2e/src/connect/scenarios/login.spec.ts +++ b/packages/tests-e2e/src/connect/scenarios/login.spec.ts @@ -1,6 +1,6 @@ import { test } from '../fixtures/BaseTest'; import { ErrorTexts, password, ScreenNames } from '../utils/Constants'; -import { loadInvitationToken, setupUser, setupVirtualAuthenticator } from './hooks'; +import { loadInvitationToken, setupNetworkBlocker, setupUser, setupVirtualAuthenticator } from './hooks'; test.describe('login component (without invitation token)', () => { setupUser(test, false); @@ -35,24 +35,35 @@ test.describe('login component (with invitation token, without passkeys)', () => test.describe('login component (with invitation token, with passkeys)', () => { setupVirtualAuthenticator(test); + setupNetworkBlocker(test); setupUser(test, true, true); - test('successful login with passkey (one-tap)', async ({ model }) => { + test('successful login with passkey', async ({ model }) => { await model.home.logout(); await model.expectScreen(ScreenNames.InitLoginOneTap); - await model.login.submitPasskeyButton(true); + await model.login.removePasskeyButton(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail(model.email, true); await model.expectScreen(ScreenNames.Home); }); - test('successful login with passkey', async ({ model }) => { + test('successful login with passkey (conditional UI)', async ({ model }) => { await model.home.logout(); await model.expectScreen(ScreenNames.InitLoginOneTap); - await model.login.removePasskeyButton(); - await model.expectScreen(ScreenNames.InitLogin); + await model.login.submitConditionalUI(async () => { + await model.login.removePasskeyButton(); + }); + await model.expectScreen(ScreenNames.Home); + }); - await model.login.submitEmail(model.email, true); + test('successful login with passkey (one-tap)', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.login.submitPasskeyButton(true); await model.expectScreen(ScreenNames.Home); }); @@ -63,10 +74,66 @@ test.describe('login component (with invitation token, with passkeys)', () => { await model.login.submitPasskeyButton(false); await model.login.repeatedlyFailPasskeyInput(); }); + + test('inaccessible passkey on login', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.authenticator.clearCredentials(); + await model.clearLocalStorageAndCookies(); + await model.loadInvitationToken(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail(model.email, false); + await model.expectScreen(ScreenNames.InitLoginFallback); + }); + + test('Corbado FAPI unavailable after authentication', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.blocker.blockCorbadoFAPIFinishEndpoint(); + + await model.login.submitPasskeyButton(true); + await model.expectScreen(ScreenNames.InitLoginFallback); + }); + + test('passkey signature validation fails', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.authenticator.clearCredentials(); + await model.authenticator.addDummyCredential(); + + await model.login.submitConditionalUI(async () => { + await model.login.removePasskeyButton(); + }); + await model.expectScreen(ScreenNames.InitLoginFallback); + await model.expectError(ErrorTexts.PasskeySignatureValidationFail); + }); + + test('attempt login with server-side deleted passkey', async ({ model }) => { + await model.home.gotoPasskeyList(); + await model.expectScreen(ScreenNames.PasskeyList); + + await model.passkeyList.expectPasskeys(1); + await model.passkeyList.deletePasskey(0); + await model.passkeyList.expectPasskeys(0); + + await model.loadHome(); + await model.expectScreen(ScreenNames.Home); + + await model.login.submitConditionalUI(async () => { + await model.home.logout(); + }); + await model.expectScreen(ScreenNames.InitLoginFallback); + await model.expectError(ErrorTexts.DeletedPasskeyUsed); + }); }); -test.describe('login component (input validation)', () => { +test.describe('login component (without user)', () => { setupVirtualAuthenticator(test); + setupNetworkBlocker(test); loadInvitationToken(test); test('attempt login with incomplete credentials', async ({ model }) => { @@ -84,4 +151,11 @@ test.describe('login component (input validation)', () => { await model.login.submitEmail('unknown-email@corbado.com', false); await model.expectError(ErrorTexts.UnknownEmail); }); + + test('Corbado FAPI unavailable', async ({ model }) => { + await model.blocker.blockCorbadoFAPI(); + + await model.loadLogin(); + await model.expectScreen(ScreenNames.InitLoginFallback); + }); }); diff --git a/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts b/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts index 1d102baae..2a0934014 100644 --- a/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts +++ b/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts @@ -1,16 +1,104 @@ -import { test } from '../fixtures/BaseTest'; -import { loadPasskeyList, setupUser, setupVirtualAuthenticator } from './hooks'; +import { expect, test } from '../fixtures/BaseTest'; +import { ErrorTexts, ScreenNames } from '../utils/Constants'; +import { loadPasskeyList, setupNetworkBlocker, setupUser, setupVirtualAuthenticator } from './hooks'; test.describe('passkey-list component', () => { setupVirtualAuthenticator(test); - setupUser(test, true, true); + setupNetworkBlocker(test); + setupUser(test, true, false); loadPasskeyList(test); - test('list, delete, append passkey', async ({ model }) => { + test('list, delete, create passkey', async ({ model }) => { + await model.passkeyList.expectPasskeys(0); + await model.passkeyList.createPasskey(true); + await model.passkeyList.expectPasskeys(1); + await model.passkeyList.deletePasskey(0); + await model.passkeyList.expectPasskeys(0); + }); + + test('abort passkey creation', async ({ model }) => { + await model.passkeyList.expectPasskeys(0); + await model.passkeyList.createPasskey(false); + await model.passkeyList.expectPasskeys(0); + }); + + test('Connect Token endpoint unavailable during passkey creation', async ({ model }) => { + await model.passkeyList.expectPasskeys(0); + + await model.blocker.blockCorbadoConnectTokenEndpoint(); + + await model.page.getByRole('button', { name: 'Add a passkey' }).click(); + await model.expectError(ErrorTexts.PasskeyCreateFail); + await model.passkeyList.expectPasskeys(0); + }); + + test('Corbado FAPI unavailable during passkey creation', async ({ model }) => { + await model.passkeyList.expectPasskeys(0); + + await model.blocker.blockCorbadoFAPI(); + + await model.page.getByRole('button', { name: 'Add a passkey' }).click(); + await model.expectError(ErrorTexts.PasskeyCreateFail); + await model.passkeyList.expectPasskeys(0); + }); + + test('passkey already registered', async ({ model }) => { + await model.passkeyList.expectPasskeys(0); + await model.passkeyList.createPasskey(true); + await model.passkeyList.expectPasskeys(1); + + await model.passkeyList.createPasskey(true, () => + expect(model.page.getByRole('heading', { name: 'No passkey created' })).toBeVisible(), + ); + await expect(model.page.getByText('No passkey created')).toBeVisible(); + + await model.passkeyList.confirmModal(); await model.passkeyList.expectPasskeys(1); + }); + + test('Connect Token endpoint unavailable during passkey deletion', async ({ model }) => { + await model.passkeyList.expectPasskeys(0); + await model.passkeyList.createPasskey(true); + await model.passkeyList.expectPasskeys(1); + + await model.blocker.blockCorbadoConnectTokenEndpoint(); + await model.passkeyList.deletePasskey(0); + await model.expectError(ErrorTexts.PasskeyDeleteFail); + await model.passkeyList.expectPasskeys(1); + }); + + test('Corbado FAPI unavailable during passkey deletion', async ({ model }) => { await model.passkeyList.expectPasskeys(0); - await model.passkeyList.appendPasskey(true); + await model.passkeyList.createPasskey(true); await model.passkeyList.expectPasskeys(1); + + await model.blocker.blockCorbadoFAPI(); + + await model.passkeyList.deletePasskey(0); + await model.expectError(ErrorTexts.PasskeyDeleteFail); + await model.passkeyList.expectPasskeys(1); + }); +}); + +test.describe('skip passkey-list component', () => { + setupVirtualAuthenticator(test); + setupNetworkBlocker(test); + setupUser(test, true, true); + + test('Connect Token endpoint unavailable', async ({ model }) => { + await model.blocker.blockCorbadoConnectTokenEndpoint(); + + await model.home.gotoPasskeyList(); + await model.expectScreen(ScreenNames.PasskeyList); + await model.expectError(ErrorTexts.PasskeyFetchFail); + }); + + test('Corbado FAPI unavailable', async ({ model }) => { + await model.blocker.blockCorbadoFAPI(); + + await model.home.gotoPasskeyList(); + await model.expectScreen(ScreenNames.PasskeyList); + await model.expectError(ErrorTexts.PasskeyFetchFail); }); }); diff --git a/packages/tests-e2e/src/connect/utils/CDPSessionManager.ts b/packages/tests-e2e/src/connect/utils/CDPSessionManager.ts new file mode 100644 index 000000000..49d28660e --- /dev/null +++ b/packages/tests-e2e/src/connect/utils/CDPSessionManager.ts @@ -0,0 +1,16 @@ +import type { CDPSession, Page } from '@playwright/test'; + +export class CDPSessionManager { + #cdpClient: CDPSession | null = null; + + async initialize(page: Page) { + this.#cdpClient = await page.context().newCDPSession(page); + } + + getClient(): CDPSession { + if (!this.#cdpClient) { + throw new Error('CDP client not initialized'); + } + return this.#cdpClient; + } +} diff --git a/packages/tests-e2e/src/connect/utils/Constants.ts b/packages/tests-e2e/src/connect/utils/Constants.ts index 29159a1f3..2dde0c2e8 100644 --- a/packages/tests-e2e/src/connect/utils/Constants.ts +++ b/packages/tests-e2e/src/connect/utils/Constants.ts @@ -15,6 +15,11 @@ export enum ErrorTexts { EmptyEmail = 'Enter your email address.', UnknownEmail = 'There is no account registered to that email address.', CancelledPasskey = 'You have cancelled setting up your passkey. Please try again.', + PasskeyFetchFail = 'Unable to access passkeys. Check your connection and try again.', + PasskeyCreateFail = 'Passkey creation failed. Please try again later.', + PasskeyDeleteFail = 'Passkey deletion failed. Please try again later.', + DeletedPasskeyUsed = 'You previously deleted this passkey. Use your password to log in instead.', + PasskeySignatureValidationFail = "We couldn't log you in with your passkey due to a system error. Use your password to log in instead.", } export const phone = '+4915121609839'; diff --git a/packages/tests-e2e/src/connect/utils/ExpectScreen.ts b/packages/tests-e2e/src/connect/utils/ExpectScreen.ts index 0befd2de2..76389f231 100644 --- a/packages/tests-e2e/src/connect/utils/ExpectScreen.ts +++ b/packages/tests-e2e/src/connect/utils/ExpectScreen.ts @@ -1,7 +1,8 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import { ErrorTexts, ScreenNames } from './Constants'; +import { ErrorTexts } from './Constants'; +import { ScreenNames } from './Constants'; export const expectScreen = async (page: Page, screenName: ScreenNames): Promise => { switch (screenName) { @@ -59,5 +60,9 @@ export const expectScreen = async (page: Page, screenName: ScreenNames): Promise }; export const expectError = (page: Page, message: ErrorTexts): Promise => { + // This error message isn't a part of cb-container, so it doesn't come as cb-notification-text. + if (message === ErrorTexts.DeletedPasskeyUsed || message === ErrorTexts.PasskeySignatureValidationFail) { + return expect(page.getByText(message)).toBeVisible(); + } return expect(page.locator('.cb-notification-text')).toHaveText(message); }; diff --git a/packages/tests-e2e/src/connect/utils/NetworkRequestBlocker.ts b/packages/tests-e2e/src/connect/utils/NetworkRequestBlocker.ts new file mode 100644 index 000000000..c780a351f --- /dev/null +++ b/packages/tests-e2e/src/connect/utils/NetworkRequestBlocker.ts @@ -0,0 +1,34 @@ +import type { CDPSession } from '@playwright/test'; + +import type { CDPSessionManager } from './CDPSessionManager'; + +export class NetworkRequestBlocker { + #cdpClient: CDPSession; + + constructor(cdpManager: CDPSessionManager) { + this.#cdpClient = cdpManager.getClient(); + } + + enableBlocking() { + return this.#cdpClient.send('Network.enable'); + } + + blockCorbadoFAPI() { + return this.#cdpClient.send('Network.setBlockedURLs', { + urls: ['*.frontendapi.cloud.corbado-staging.io/v2/connect'], + }); + } + + blockCorbadoFAPIFinishEndpoint() { + return this.#cdpClient.send('Network.setBlockedURLs', { + urls: ['*.frontendapi.cloud.corbado-staging.io/v2/connect/*/finish'], + }); + } + + blockCorbadoConnectTokenEndpoint() { + // This is sufficient, as the connectTokens endpoint is called from /passkey-list handler + return this.#cdpClient.send('Network.setBlockedURLs', { + urls: ['*.playground.corbado.io/passkey-list'], + }); + } +} diff --git a/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts b/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts index a771ecc86..dea81b8a4 100644 --- a/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts +++ b/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts @@ -1,20 +1,17 @@ -import type { CDPSession, Page } from '@playwright/test'; +import type { CDPSession } from '@playwright/test'; +import type { CDPSessionManager } from './CDPSessionManager'; import { operationTimeout } from './Constants'; export class VirtualAuthenticator { - #cdpClient: CDPSession | null = null; + #cdpClient: CDPSession; #authenticatorId = ''; - async initializeCDPSession(page: Page) { - this.#cdpClient = await page.context().newCDPSession(page); + constructor(cdpManager: CDPSessionManager) { + this.#cdpClient = cdpManager.getClient(); } async addWebAuthn(passkeySupported = true) { - if (!this.#cdpClient) { - throw new Error('CDP client not initialized'); - } - await this.#cdpClient.send('WebAuthn.enable'); const result = await this.#cdpClient.send('WebAuthn.addVirtualAuthenticator', { options: passkeySupported @@ -34,59 +31,81 @@ export class VirtualAuthenticator { this.#authenticatorId = result.authenticatorId; } - async removeWebAuthn() { - if (!this.#cdpClient) { - throw new Error('CDP client not initialized'); - } - - await this.#cdpClient.send('WebAuthn.removeVirtualAuthenticator', { + removeWebAuthn() { + return this.#cdpClient.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId: this.#authenticatorId, }); } - async startAndCompletePasskeyOperation(operationTrigger: () => Promise) { - if (!this.#cdpClient) { - throw new Error('CDP client not initialized'); + async startAndCompletePasskeyOperation( + operationTrigger: () => Promise, + postOperationCheck: (() => Promise) | null = null, + ) { + let postOperationPromise: Promise; + if (postOperationCheck === null) { + postOperationPromise = new Promise(resolve => { + this.#cdpClient?.on('WebAuthn.credentialAdded', () => resolve()); + this.#cdpClient?.on('WebAuthn.credentialAsserted', () => resolve()); + }); + } else { + postOperationPromise = postOperationCheck(); } - const operationCompleted = new Promise(resolve => { - this.#cdpClient?.on('WebAuthn.credentialAdded', () => resolve()); - this.#cdpClient?.on('WebAuthn.credentialAsserted', () => resolve()); - }); - const wait = new Promise(resolve => setTimeout(resolve, operationTimeout)); - await this.#setWebAuthnUserVerified(this.#cdpClient, this.#authenticatorId, true); - await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, true); + await this.#setWebAuthnUserVerified(this.#authenticatorId, true); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#authenticatorId, true); await operationTrigger(); - await Promise.race([operationCompleted, wait.then(() => Promise.reject('Passkey input timeout'))]); - await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, false); + await Promise.race([postOperationPromise, wait.then(() => Promise.reject('Passkey input timeout'))]); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#authenticatorId, false); } async startAndCancelPasskeyOperation(operationTrigger: () => Promise, postOperationCheck: () => Promise) { - if (!this.#cdpClient) { - throw new Error('CDP client not initialized'); - } - - await this.#setWebAuthnUserVerified(this.#cdpClient, this.#authenticatorId, false); - await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, true); + await this.#setWebAuthnUserVerified(this.#authenticatorId, false); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#authenticatorId, true); await operationTrigger(); await postOperationCheck(); - await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, false); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#authenticatorId, false); + } + + clearCredentials() { + return this.#cdpClient.send('WebAuthn.clearCredentials', { + authenticatorId: this.#authenticatorId, + }); + } + + async addDummyCredential() { + try { + await this.#cdpClient.send('WebAuthn.addCredential', { + authenticatorId: this.#authenticatorId, + credential: { + credentialId: '', // 'WZuSfPDeCfXUMqO3vcVZ6ZYY0w2W4NpLcLzTjMl4qns=', + isResidentCredential: true, + rpId: 'connect-next.playground.corbado.io', + privateKey: + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgz/eSahk8R0fk3Jjpcbd1LPc2gGKyzEG23UFIbFTqSbyhRANCAAQ4a8dJ559cf0cZcg0U7k5oCofmtOzuqXDSwzP8LLhv0InronrySiaWAGuWFpVsbNyOnWSd6VZJU8wiFKSMiDWN', + userHandle: '', // 'TDBlaFVpNnRNQg==', + signCount: 1, + }, + }); + } catch (e) { + console.error(e); + throw e; + } } - #setWebAuthnAutomaticPresenceSimulation(client: CDPSession, authenticatorId: string, automatic: boolean) { - return client.send('WebAuthn.setAutomaticPresenceSimulation', { + #setWebAuthnAutomaticPresenceSimulation(authenticatorId: string, automatic: boolean) { + return this.#cdpClient.send('WebAuthn.setAutomaticPresenceSimulation', { authenticatorId: authenticatorId, enabled: automatic, }); } - #setWebAuthnUserVerified(client: CDPSession, authenticatorId: string, isUserVerified: boolean) { - return client.send('WebAuthn.setUserVerified', { + #setWebAuthnUserVerified(authenticatorId: string, isUserVerified: boolean) { + return this.#cdpClient.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified, });