diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 0fbe55ea..51ee2e60 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -112,7 +112,8 @@ export default function Login({ else setErrorMessage(errorMessages[res.error] || t("something_went_wrong")); }) .catch(() => setErrorMessage(errorMessages[ErrorCode.InternalServerError])); - }}> + }} + data-testid="login-form">
diff --git a/apps/web/pages/auth/logout.tsx b/apps/web/pages/auth/logout.tsx index eb7b326a..a57a02c6 100644 --- a/apps/web/pages/auth/logout.tsx +++ b/apps/web/pages/auth/logout.tsx @@ -1,5 +1,6 @@ import { CheckIcon } from "@heroicons/react/outline"; import { GetServerSidePropsContext } from "next"; +import { useSession, signOut } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect } from "react"; @@ -16,6 +17,8 @@ import { ssrInit } from "@server/lib/ssr"; type Props = inferSSRProps; export default function Logout(props: Props) { + const { data: session, status } = useSession(); + if (status === "authenticated") signOut({ redirect: false }); const router = useRouter(); useEffect(() => { if (props.query?.survey === "true") { diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts new file mode 100644 index 00000000..dd8e83aa --- /dev/null +++ b/apps/web/playwright/fixtures/users.ts @@ -0,0 +1,96 @@ +import type { Page } from "@playwright/test"; +import { UserPlan } from "@prisma/client"; +import type Prisma from "@prisma/client"; + +import { hashPassword } from "@calcom/lib/auth"; +import { prisma } from "@calcom/prisma"; + +export interface UsersFixture { + create: (opts?: CustomUserOpts) => Promise; + get: () => UserFixture[]; + logout: () => Promise; +} + +interface UserFixture { + id: number; + self: () => Promise; + login: () => Promise; + debug: (message: Record) => Promise; +} + +// An alias for the hard to remember timezones strings +export enum TimeZoneE { + USA = "America/Phoenix", + UK = "Europe/London", +} + +// creates a user fixture instance and stores the collection +export const createUsersFixture = (page: Page): UsersFixture => { + let store = { users: [], page } as { users: UserFixture[]; page: typeof page }; + return { + create: async (opts) => { + const user = await prisma.user.create({ + data: await createUser(opts), + }); + const userFixture = createUserFixture(user, store.page!); + store.users.push(userFixture); + return userFixture; + }, + get: () => store.users, + logout: async () => { + await page.goto("/auth/logout"); + }, + }; +}; + +// creates the single user fixture +const createUserFixture = (user: Prisma.User, page: Page): UserFixture => { + const store = { user, page }; + + // self is a reflective method that return the Prisma object that references this fixture. + const self = async () => (await prisma.user.findUnique({ where: { id: store.user.id } }))!; + return { + id: user.id, + self, + login: async () => login(await self(), store.page), + // ths is for developemnt only aimed to inject debugging messages in the metadata field of the user + debug: async (message) => { + await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } }); + }, + }; +}; + +type CustomUserOptsKeys = "username" | "plan" | "completedOnboarding" | "locale"; +type CustomUserOpts = Partial> & { timeZone?: TimeZoneE }; + +// creates the actual user in the db. +const createUser = async (opts?: CustomUserOpts) => { + // build a unique name for our user + const uname = + (opts?.username ?? opts?.plan?.toLocaleLowerCase() ?? UserPlan.PRO.toLowerCase()) + "-" + Date.now(); + return { + username: uname, + name: (opts?.username ?? opts?.plan ?? UserPlan.PRO).toUpperCase(), + plan: opts?.plan ?? UserPlan.PRO, + email: `${uname}@example.com`, + password: await hashPassword(uname), + emailVerified: new Date(), + completedOnboarding: opts?.completedOnboarding ?? true, + timeZone: opts?.timeZone ?? TimeZoneE.UK, + locale: opts?.locale ?? "en", + }; +}; + +// login using a replay of an E2E routine. +async function login(user: Prisma.User, page: Page) { + await page.goto("/auth/logout"); + await page.goto("/"); + await page.click('input[name="email"]'); + await page.fill('input[name="email"]', user.email); + await page.press('input[name="email"]', "Tab"); + await page.fill('input[name="password"]', user.username!); + await page.press('input[name="password"]', "Enter"); + + // 2 seconds of delay before returning to help the session loading well + await page.waitForTimeout(2000); +} diff --git a/apps/web/playwright/lib/fixtures.ts b/apps/web/playwright/lib/fixtures.ts new file mode 100644 index 00000000..5fb1a6cc --- /dev/null +++ b/apps/web/playwright/lib/fixtures.ts @@ -0,0 +1,17 @@ +import { test as base } from "@playwright/test"; + +import { createUsersFixture } from "../fixtures/users"; +import type { UsersFixture } from "../fixtures/users"; + +interface Fixtures { + users: UsersFixture; +} + +export const test = base.extend({ + users: async ({ page }, use, testInfo) => { + // instantiate the fixture + const usersFixture = createUsersFixture(page); + // use the fixture within the test + await use(usersFixture); + }, +}); diff --git a/apps/web/playwright/login.test.ts b/apps/web/playwright/login.test.ts index f89f2f64..9523932a 100644 --- a/apps/web/playwright/login.test.ts +++ b/apps/web/playwright/login.test.ts @@ -1,13 +1,28 @@ -import { test } from "@playwright/test"; +import { expect } from "@playwright/test"; + +import { test } from "./lib/fixtures"; test.describe("Login tests", () => { // Using logged in state from globalSteup test.use({ storageState: "playwright/artifacts/proStorageState.json" }); - test("login with pro@example.com", async ({ page }) => { + test("Login with pro@example.com", async ({ page }) => { // Try to go homepage await page.goto("/"); // It should redirect you to the event-types page await page.waitForSelector("[data-testid=event-types]"); }); + + test("Should logout using the logout path", async ({ page, users }) => { + // creates a user and login + const pro = await users.create(); + await pro.login(); + + // users.logout() action uses the logout route "/auth/logout" to clear the session + await users.logout(); + + // check if we are at the login page + await page.goto("/"); + await expect(page.locator(`[data-testid=login-form]`)).toBeVisible(); + }); });