diff --git a/package.json b/package.json index c091c4a35c..96b3d35c96 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "turbo run build --filter=next-auth --filter=@auth/* --no-deps", "test": "turbo run test --concurrency=1 --filter=[HEAD^1] --filter=./packages/* --filter=!*app* --filter=!*dynamo* --filter=!*edgedb* --filter=!*hasura* --filter=!*mikro* --filter=!*supabase* --filter=!*upstash* --filter=!*xata*", "test:e2e": "turbo run test:e2e", + "test:e2e:watch": "turbo run test:e2e -- --ui", "clean": "turbo run clean --no-cache", "dev:example": "turbo run dev --parallel --continue --filter=nextjs-example-app... --filter=!./packages/adapter-*", "dev:db": "turbo run dev --parallel --continue --filter=next-auth-app...", diff --git a/packages/core/test/e2e/fixtures/auth.ts b/packages/core/test/e2e/fixtures/auth.ts new file mode 100644 index 0000000000..bc87191055 --- /dev/null +++ b/packages/core/test/e2e/fixtures/auth.ts @@ -0,0 +1,26 @@ +export type AuthFixture = { + environmentUrl: string + loginUser: string + loginPassword: string +} +export function createAuthFixture(): AuthFixture { + return { + get environmentUrl() { + const url = process.env.ENVIRONMENT_URL ?? "http://localhost:3000" + + return url + }, + get loginUser() { + const username = process.env.TEST_KEYCLOAK_USERNAME + if (!username) throw new Error("Keycloak username is empty") + + return username + }, + get loginPassword() { + const password = process.env.TEST_KEYCLOAK_PASSWORD + if (!password) throw new Error("Keycloak password is empty") + + return password + }, + } +} diff --git a/packages/core/test/e2e/fixtures/webApp.ts b/packages/core/test/e2e/fixtures/webApp.ts new file mode 100644 index 0000000000..5c33c990b0 --- /dev/null +++ b/packages/core/test/e2e/fixtures/webApp.ts @@ -0,0 +1,104 @@ +import { + expect, + type BrowserContext, + type Locator, + type Page, +} from "@playwright/test" +import { AuthFixture } from "./auth" +import { KeycloakLoginPom } from "../poms/keycloakLoginPom" + +/** + * This fixture provides utility methods for logging in, + * navigating and clicking common elements. + */ +export class WebApp { + #auth: AuthFixture + private keycloak: KeycloakLoginPom + + public page: Page + public context: BrowserContext + + public isLoggedIn = false + + // locators + public signinButton: Locator + public session: Record + + constructor({ + page, + context, + auth, + keycloak, + }: { + page: Page + context: BrowserContext + auth: AuthFixture + keycloak: KeycloakLoginPom + }) { + this.#auth = auth + + this.keycloak = keycloak + this.page = page + this.context = context + + this.signinButton = page + .getByRole("banner") + .getByRole("button", { name: "Sign in" }) + + this.session = {} + } + + async login({ + environmentUrl = this.#auth.environmentUrl, + username, + password, + }: { + environmentUrl?: string + username?: string + password?: string + } = {}) { + if (this.isLoggedIn) return + + // Go to homepage + await this.page.goto(environmentUrl) + await this.signinButton.click() + + // On built-in signin page, select Keycloak + await this.page.getByText("Keycloak").click() + + // Use keycloak POM to login + await this.keycloak.login({ username, password }) + + // Ensure we've landed back at the dev app logged in + const session = await this.page.locator("pre").textContent() + + expect(JSON.parse(session ?? "{}")).toEqual({ + user: { + email: "bob@alice.com", + name: "Bob Alice", + image: "https://avatars.githubusercontent.com/u/67470890?s=200&v=4", + }, + expires: expect.any(String), + }) + + this.isLoggedIn = true + } + + async getSession({ + environmentUrl = this.#auth.environmentUrl, + }: { + environmentUrl?: string + } = {}) { + if (!this.isLoggedIn) return + + try { + const sessionRes = await fetch(`${environmentUrl}/auth/session`) + + if (sessionRes.ok) { + this.session = await sessionRes.json() + } + } catch (error) { + console.error(error) + } + } +} diff --git a/packages/core/test/e2e/helpers/authTest.ts b/packages/core/test/e2e/helpers/authTest.ts new file mode 100644 index 0000000000..d90b0ed6a4 --- /dev/null +++ b/packages/core/test/e2e/helpers/authTest.ts @@ -0,0 +1,24 @@ +import { test as base } from "@playwright/test" +import { AuthFixture, createAuthFixture } from "../fixtures/auth" +import { WebApp } from "../fixtures/webApp" +import { KeycloakLoginPom } from "../poms/keycloakLoginPom" + +type AuthJsWebappFixtures = { + auth: AuthFixture + keycloak: KeycloakLoginPom + webapp: WebApp +} + +export const test = base.extend({ + auth: async ({}, use) => { + await use(createAuthFixture()) + }, + keycloak: async ({ page, auth }, use) => { + await use(new KeycloakLoginPom({ page, auth })) + }, + webapp: async ({ page, context, auth, keycloak }, use) => { + await use(new WebApp({ page, context, auth, keycloak })) + }, +}) + +export { expect } from "@playwright/test" diff --git a/packages/core/test/e2e/poms/keycloakLoginPom.ts b/packages/core/test/e2e/poms/keycloakLoginPom.ts new file mode 100644 index 0000000000..843c71d5fd --- /dev/null +++ b/packages/core/test/e2e/poms/keycloakLoginPom.ts @@ -0,0 +1,45 @@ +import { expect, type Locator, type Page } from "@playwright/test" +import type { AuthFixture } from "../fixtures/auth" + +export class KeycloakLoginPom { + usernameInput: Locator + passwordInput: Locator + signinButton: Locator + + #auth: AuthFixture + + constructor({ page, auth }: { page: Page; auth: AuthFixture }) { + this.#auth = auth + + this.usernameInput = page.getByLabel("Username or email") + this.passwordInput = page.locator("#password") + + this.signinButton = page.getByRole("button", { name: "Sign In" }) + } + + async login({ + username = this.#auth.loginUser, + password = this.#auth.loginPassword, + }: { + username?: string + password?: string + } = {}) { + if (!username) throw new Error("Keycloak username missing") + if (!password) throw new Error("Keycloak password missing") + + await this.isVisible() + + await this.usernameInput.fill(username) + await this.passwordInput.fill(password) + + return this.signinButton.click() + } + + isVisible() { + return Promise.all([ + expect(this.usernameInput).toBeVisible(), + expect(this.passwordInput).toBeVisible(), + expect(this.signinButton).toBeVisible(), + ]) + } +} diff --git a/packages/core/test/e2e/user.spec.ts b/packages/core/test/e2e/user.spec.ts new file mode 100644 index 0000000000..21b0697b50 --- /dev/null +++ b/packages/core/test/e2e/user.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from "./helpers/authTest" + +test.describe("user profile", () => { + test("profile values", async ({ page, webapp }) => { + try { + await webapp.login() + + const logoutBtn = page + .getByRole("banner") + .getByRole("button", { name: "Sign out" }) + + expect(await logoutBtn.textContent()).toBe("Sign out") + + await webapp.getSession() + console.log("session", webapp.session) + } catch (error) { + console.error(error) + } + }) +})