Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions packages/tests-e2e/src/connect/models/BaseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { HomeModel } from './HomeModel';
import { LoginModel } from './LoginModel';
import { PasskeyListModel } from './PasskeyListModel';
import { SignupModel } from './SignupModel';
import { StorageModel } from './StorageModel';
import { WebhookModel } from './WebhookModel';

export class BaseModel {
Expand All @@ -22,6 +23,7 @@ export class BaseModel {
home: HomeModel;
passkeyList: PasskeyListModel;
webhook: WebhookModel;
storage: StorageModel;
email = '';

constructor(page: Page, authenticator: VirtualAuthenticator, blocker: NetworkRequestBlocker) {
Expand All @@ -34,10 +36,7 @@ export class BaseModel {
this.home = new HomeModel(page);
this.passkeyList = new PasskeyListModel(page, authenticator);
this.webhook = new WebhookModel(page);
}

loadInvitationToken() {
return this.page.goto('/login?invitationToken=inv-token-correct');
this.storage = new StorageModel(page);
}

loadSignup() {
Expand Down Expand Up @@ -74,9 +73,4 @@ export class BaseModel {
}
}
}

async clearLocalStorageAndCookies() {
await this.page.evaluate(() => localStorage.clear());
await this.page.context().clearCookies();
}
}
4 changes: 4 additions & 0 deletions packages/tests-e2e/src/connect/models/PasskeyListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class PasskeyListModel {
}
}

checkCreatePasskeyDisabled() {
return expect(this.page.getByRole('button', { name: 'Add a passkey' })).not.toBeVisible();
}

confirmModal() {
return this.page.getByRole('button', { name: 'Okay' }).click();
}
Expand Down
134 changes: 134 additions & 0 deletions packages/tests-e2e/src/connect/models/StorageModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';

import { ScreenNames } from '../utils/Constants';
import { expectScreen } from '../utils/ExpectScreen';

export class StorageModel {
page: Page;

constructor(page: Page) {
this.page = page;
}

async loadInvitationToken() {
await this.page.goto('/login?invitationToken=inv-token-correct');
await expectScreen(this.page, ScreenNames.InitLogin);
}

async checkInvitationToken() {
const cboConnectInvitationRaw = await this.page.evaluate(k => localStorage.getItem(k), 'cbo_connect_invitation');
if (!cboConnectInvitationRaw) {
throw new Error('cbo_connect_invitation not found in local storage');
}
const cboConnectInvitation = JSON.parse(cboConnectInvitationRaw);
expect(cboConnectInvitation.token).toEqual('inv-token-correct');
}

deleteInvitationToken() {
return this.page.evaluate(k => localStorage.removeItem(k), 'cbo_connect_invitation');
}

async getProcessID(): Promise<string> {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
expect(cboConnectProcess.id).not.toBeNull();

return cboConnectProcess.id;
}

async checkProcessID(expectedID: string) {
expect(await this.getProcessID()).toEqual(expectedID);
}

async getLoginLifetime(): Promise<number> {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
return cboConnectProcess.loginData.expiresAt;
}

async setLoginLifetime(newLifetime: number) {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
cboConnectProcess.loginData.expiresAt = newLifetime;
await this.page.evaluate(({ k, p }) => localStorage.setItem(k, JSON.stringify(p)), {
k: key,
p: cboConnectProcess,
});
}

async getAppendLifetime(): Promise<number> {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
return cboConnectProcess.appendData.expiresAt;
}

async setAppendLifetime(newLifetime: number) {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
cboConnectProcess.appendData.expiresAt = newLifetime;
await this.page.evaluate(({ k, p }) => localStorage.setItem(k, JSON.stringify(p)), {
k: key,
p: cboConnectProcess,
});
}

async getManageLifetime(): Promise<number> {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
return cboConnectProcess.manageData.expiresAt;
}

async setManageLifetime(newLifetime: number) {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
cboConnectProcess.manageData.expiresAt = newLifetime;
await this.page.evaluate(({ k, p }) => localStorage.setItem(k, JSON.stringify(p)), {
k: key,
p: cboConnectProcess,
});
}

async checkLoginDataDeleted() {
const key = `cbo_connect_process-${process.env.PLAYWRIGHT_CONNECT_PROJECT_ID}`;
const cboConnectProcessRaw = await this.page.evaluate(k => localStorage.getItem(k), key);
if (!cboConnectProcessRaw) {
throw new Error('cbo_connect_process not found in local storage');
}
const cboConnectProcess = JSON.parse(cboConnectProcessRaw);
expect(cboConnectProcess.loginData).toBeNull();
}

async clearLocalStorageAndCookies() {
await this.page.evaluate(() => localStorage.clear());
await this.page.context().clearCookies();
}
}
16 changes: 16 additions & 0 deletions packages/tests-e2e/src/connect/scenarios/append.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { expect } from '@playwright/test';

import { test } from '../fixtures/BaseTest';
import { password, ScreenNames, WebhookTypes } from '../utils/Constants';
import { loadPasskeyAppend, setupNetworkBlocker, setupUser, setupVirtualAuthenticator, setupWebhooks } from './hooks';
Expand Down Expand Up @@ -63,4 +65,18 @@ test.describe('skip append component', () => {
await model.login.submitFallbackCredentials(model.email, password, true);
await model.expectScreen(ScreenNames.Home);
});

test('expired append lifetime leads to skipped append screen', async ({ model }) => {
await model.home.logout();
await model.expectScreen(ScreenNames.InitLogin);

await model.login.submitEmail(model.email, false);
await model.expectScreen(ScreenNames.InitLoginFallback);
expect(await model.storage.getAppendLifetime()).toBeGreaterThan(Math.floor(Date.now() / 1000));

await model.storage.setAppendLifetime(Math.floor(Date.now() / 1000) - 1);
await model.storage.deleteInvitationToken();
await model.login.submitFallbackCredentials(model.email, password, true);
await model.expectScreen(ScreenNames.Home);
});
});
7 changes: 4 additions & 3 deletions packages/tests-e2e/src/connect/scenarios/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type {
} from '@playwright/test';

import type { BaseModel } from '../models/BaseModel';
import { password, ScreenNames, WebhookTypes } from '../utils/Constants';
import type { WebhookTypes } from '../utils/Constants';
import { password, ScreenNames } from '../utils/Constants';

export function setupVirtualAuthenticator(
test: TestType<
Expand Down Expand Up @@ -58,7 +59,7 @@ export function loadInvitationToken(
>,
) {
test.beforeEach(async ({ model }) => {
await model.loadInvitationToken();
await model.storage.loadInvitationToken();
});
}

Expand All @@ -72,7 +73,7 @@ export function setupUser(
) {
test.beforeEach(async ({ model }) => {
if (invited) {
await model.loadInvitationToken();
await model.storage.loadInvitationToken();
}
await model.loadSignup();
await model.expectScreen(ScreenNames.InitSignup);
Expand Down
42 changes: 40 additions & 2 deletions packages/tests-e2e/src/connect/scenarios/login.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { expect } from '@playwright/test';

import { test } from '../fixtures/BaseTest';
import { ErrorTexts, password, ScreenNames, WebhookTypes } from '../utils/Constants';
import { loadInvitationToken, setupNetworkBlocker, setupUser, setupVirtualAuthenticator, setupWebhooks } from './hooks';
Expand Down Expand Up @@ -80,8 +82,8 @@ test.describe('login component (with invitation token, with passkeys)', () => {
await model.expectScreen(ScreenNames.InitLoginOneTap);

await model.authenticator.clearCredentials();
await model.clearLocalStorageAndCookies();
await model.loadInvitationToken();
await model.storage.clearLocalStorageAndCookies();
await model.storage.loadInvitationToken();
await model.expectScreen(ScreenNames.InitLogin);

await model.login.submitEmail(model.email, false);
Expand Down Expand Up @@ -129,6 +131,19 @@ test.describe('login component (with invitation token, with passkeys)', () => {
await model.expectScreen(ScreenNames.InitLoginFallback);
await model.expectError(ErrorTexts.DeletedPasskeyUsed);
});

// TODO: unskip when loginData reset feature is fixed
test.skip('successful login deletes loginData', async ({ model }) => {
await model.home.logout();
await model.expectScreen(ScreenNames.InitLoginOneTap);

await model.login.removePasskeyButton();
await model.expectScreen(ScreenNames.InitLogin);

await model.login.submitEmail(model.email, true);
await model.expectScreen(ScreenNames.Home);
await model.storage.checkLoginDataDeleted();
});
});

test.describe('login component (without user)', () => {
Expand All @@ -155,6 +170,29 @@ test.describe('login component (without user)', () => {
test('Corbado FAPI unavailable', async ({ model }) => {
await model.blocker.blockCorbadoFAPI();

await model.loadLogin();
// It seems that the InitLogin page is now cached so that email needs to be submitted before reaching the InitLoginFallback screen.
await model.login.submitEmail('dummy-email@corbado.com', false);
await model.expectScreen(ScreenNames.InitLoginFallback);
});

test('invitation token and process id persists after page refresh', async ({ model }) => {
await model.expectScreen(ScreenNames.InitLogin);
await model.storage.checkInvitationToken();
const processId = await model.storage.getProcessID();

await model.loadLogin();
await model.expectScreen(ScreenNames.InitLogin);
await model.storage.checkInvitationToken();
await model.storage.checkProcessID(processId);
});

test('expired login lifetime leads to fallback screen', async ({ model }) => {
await model.expectScreen(ScreenNames.InitLogin);
expect(await model.storage.getLoginLifetime()).toBeGreaterThan(Math.floor(Date.now() / 1000));

await model.storage.setLoginLifetime(Math.floor(Date.now() / 1000) - 1);
await model.storage.deleteInvitationToken();
await model.loadLogin();
await model.expectScreen(ScreenNames.InitLoginFallback);
});
Expand Down
17 changes: 17 additions & 0 deletions packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,21 @@ test.describe('skip passkey-list component', () => {
await model.expectScreen(ScreenNames.PasskeyList);
await model.expectError(ErrorTexts.PasskeyFetchFail);
});

test('expired manage lifetime leads to skipped passkey-list screen', async ({ model }) => {
await model.home.gotoPasskeyList();
await model.expectScreen(ScreenNames.PasskeyList);
await model.passkeyList.expectPasskeys(1);
await model.loadHome();
await model.expectScreen(ScreenNames.Home);
expect(await model.storage.getManageLifetime()).toBeGreaterThan(Math.floor(Date.now() / 1000));

await model.storage.setManageLifetime(Math.floor(Date.now() / 1000) - 1);
await model.storage.deleteInvitationToken();

await model.home.gotoPasskeyList();
await model.expectScreen(ScreenNames.PasskeyList);
await model.passkeyList.expectPasskeys(1);
await model.passkeyList.checkCreatePasskeyDisabled();
});
});
Loading