diff --git a/apps/web/src/app/auth/emergency-access/emergency-access.play.spec.ts b/apps/web/src/app/auth/emergency-access/emergency-access.play.spec.ts new file mode 100644 index 00000000000..d796446e1bd --- /dev/null +++ b/apps/web/src/app/auth/emergency-access/emergency-access.play.spec.ts @@ -0,0 +1,126 @@ +import { test as base, expect, Page } from "@playwright/test"; + +import { + EmergencyAccessInviteRecipe, + Play, + Scene, + SingleUserRecipe, +} from "@bitwarden/playwright-helpers"; + +async function authenticate(page: Page, email: string) { + using scene = await Play.scene(new SingleUserRecipe({ email, premium: true }), { noDown: true }); + + await page.goto("/#/login"); + await page.getByRole("textbox", { name: "Email address (required)" }).click(); + await page.getByRole("textbox", { name: "Email address (required)" }).fill(scene.mangle(email)); + await page.getByRole("textbox", { name: "Email address (required)" }).press("Enter"); + await page.getByRole("textbox", { name: "Master password (required)" }).click(); + await page + .getByRole("textbox", { name: "Master password (required)" }) + .fill(scene.mangle("asdfasdfasdf")); + await page.getByRole("button", { name: "Log in with master password" }).click(); + await expect(page.getByRole("button", { name: "Add it later" })).toBeVisible(); + await page.getByRole("button", { name: "Add it later" }).click(); + await expect(page.locator("bit-simple-dialog")).toContainText( + "You can't autofill passwords without the browser extension", + ); + await page.getByRole("link", { name: "Skip to web app" }).click(); + await expect(page.locator("app-vault")).toContainText("There are no items to list. New item"); + + return scene; +} + +type MyFixtures = { + grantee: MyFixture2; + grantor: MyFixture2; +}; + +type MyFixture2 = { + page: Page; + scene: Scene; +}; + +const test = base.extend({ + // Person getting access + grantee: async ({ browser }, use) => { + const context = await browser.newContext(); + const page = await context.newPage(); + const scene = await authenticate(page, "grantee@bitwarden.com"); + await use({ page, scene }); + //await context.close(); + }, + // Person giving access + grantor: async ({ browser }, use) => { + const context = await browser.newContext(); + const page = await context.newPage(); + const scene = await authenticate(page, "grantor@bitwarden.com"); + await use({ page, scene }); + //await context.close(); + }, +}); + +base.describe("Emergency Access", () => { + test("Account takeover", async ({ grantee, grantor }) => { + test.setTimeout(120_000); + + const granteeEmail = grantee.scene.mangle("grantee@bitwarden.com"); + + // Add a new emergency contact + await grantor.page.goto("/#/settings/emergency-access"); + await grantor.page.getByRole("button", { name: "Add emergency contact" }).click(); + await expect(grantor.page.getByText("Invite emergency contact")).toBeVisible(); + await grantor.page.getByRole("textbox", { name: "Email (required)" }).fill(granteeEmail); + await grantor.page.getByRole("radio", { name: "Takeover" }).check(); + await grantor.page.getByRole("button", { name: "Save" }).click(); + + await expect(await grantor.page.getByRole("cell", { name: granteeEmail })).toBeVisible(); + + // Grab the invite link from the server directly since intercepting email is hard + const recipe = new EmergencyAccessInviteRecipe({ email: granteeEmail }); + const result = (await recipe.up()) as unknown as string[]; // FIXME: Recipe does not only return mangle map. + const inviteUrl = result[0]; + await grantee.page.goto(`/#${inviteUrl}`); + + // Confirm the invite + await grantor.page.goto("/#"); + await grantor.page.goto("/#/settings/emergency-access"); + await expect(await grantor.page.getByRole("cell", { name: granteeEmail })).toBeVisible(); + await grantor.page.getByRole("button", { name: "Options" }).click(); + await grantor.page.getByRole("menuitem", { name: "Confirm" }).click(); + await grantor.page.getByRole("button", { name: "Confirm" }).click(); + + // Request access + await grantee.page.goto("/#/settings/emergency-access"); + await grantee.page.getByRole("button", { name: "Options" }).click(); + await grantee.page.getByRole("menuitem", { name: "Request Access" }).click(); + await grantee.page.getByRole("button", { name: "Request Access" }).click(); + + // Approve access + await grantor.page.goto("/#"); + await grantor.page.goto("/#/settings/emergency-access"); + await grantor.page.getByRole("button", { name: "Options" }).click(); + await grantor.page.getByRole("menuitem", { name: "Approve" }).click(); + await grantor.page.getByRole("button", { name: "Approve" }).click(); + + // Initiate takeover + await grantee.page.goto("/#"); + await grantee.page.goto("/#/settings/emergency-access"); + await grantee.page.getByRole("button", { name: "Options" }).click(); + await grantee.page.getByRole("menuitem", { name: "Takeover" }).click(); + + await grantee.page + .getByRole("textbox", { name: "New master password (required)", exact: true }) + .fill("qwertyqwerty"); + await grantee.page + .getByRole("textbox", { name: "Confirm new master password" }) + .fill("qwertyqwerty"); + await grantee.page.getByRole("button", { name: "Save" }).click(); + await grantee.page.getByRole("button", { name: "Yes" }).click(); + + await grantee.page.pause(); + + // TODO: Confirm the new password works by logging out and back in + + // await new Promise(() => {}); + }); +}); diff --git a/libs/playwright-helpers/src/recipes/emergency-access-invite.recipe.ts b/libs/playwright-helpers/src/recipes/emergency-access-invite.recipe.ts new file mode 100644 index 00000000000..b5184f70a3b --- /dev/null +++ b/libs/playwright-helpers/src/recipes/emergency-access-invite.recipe.ts @@ -0,0 +1,7 @@ +import { Recipe } from "./recipe"; + +export class EmergencyAccessInviteRecipe extends Recipe<{ + email: string; +}> { + template: string = "EmergencyAccessInviteRecipe"; +} diff --git a/libs/playwright-helpers/src/recipes/index.ts b/libs/playwright-helpers/src/recipes/index.ts index d5034ed297a..55940943dab 100644 --- a/libs/playwright-helpers/src/recipes/index.ts +++ b/libs/playwright-helpers/src/recipes/index.ts @@ -2,5 +2,6 @@ * Recipes represent server-side seed recipes to create data for tests. */ +export * from "./emergency-access-invite.recipe"; export * from "./organization-with-users.recipe"; export * from "./single-user.recipe"; diff --git a/libs/playwright-helpers/src/recipes/single-user.recipe.ts b/libs/playwright-helpers/src/recipes/single-user.recipe.ts index 3034f14d86f..7fd5a504903 100644 --- a/libs/playwright-helpers/src/recipes/single-user.recipe.ts +++ b/libs/playwright-helpers/src/recipes/single-user.recipe.ts @@ -2,6 +2,8 @@ import { Recipe } from "./recipe"; export class SingleUserRecipe extends Recipe<{ email: string; + emailVerified?: boolean; + premium?: boolean; }> { template: string = "SingleUserRecipe"; }