From f1f1bf27e84320a4f170e3c6d26256b770c26466 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 10:49:04 +0100 Subject: [PATCH 01/15] test(e2e): add for onboarding and lldap authorization --- .dockerignore | 3 +- .gitignore | 2 + e2e/lldap.spec.ts | 101 ++++++++++++++++ e2e/onboarding/onboarding-steps.ts | 167 ++++++++++++++++++++++++++ e2e/onboarding/onboarding.spec.ts | 92 ++++++++++++++ e2e/shared/create-homarr-container.ts | 36 ++++-- e2e/shared/e2e-db.ts | 32 +++++ pnpm-lock.yaml | 77 ++++++++---- 8 files changed, 477 insertions(+), 33 deletions(-) create mode 100644 e2e/lldap.spec.ts create mode 100644 e2e/onboarding/onboarding-steps.ts create mode 100644 e2e/onboarding/onboarding.spec.ts create mode 100644 e2e/shared/e2e-db.ts diff --git a/.dockerignore b/.dockerignore index 7f15c4204..9269854cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ README.md .next .git dev -.build \ No newline at end of file +.build +e2e \ No newline at end of file diff --git a/.gitignore b/.gitignore index bdc55c860..7e1d14c68 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ apps/websocket/wssServer.cjs apps/nextjs/.million/ packages/cli/cli.cjs +# e2e mounts +e2e/shared/tmp #personal backgrounds apps/nextjs/public/images/background.png \ No newline at end of file diff --git a/e2e/lldap.spec.ts b/e2e/lldap.spec.ts new file mode 100644 index 000000000..092c3857d --- /dev/null +++ b/e2e/lldap.spec.ts @@ -0,0 +1,101 @@ +import { chromium } from "playwright"; +import { GenericContainer } from "testcontainers"; +import { describe, expect, test } from "vitest"; + +import * as sqliteSchema from "../packages/db/schema/sqlite"; +import { createHomarrContainer, withLogs } from "./shared/create-homarr-container"; +import { createSqliteDbFileAsync } from "./shared/e2e-db"; + +const defaultCredentials = { + username: "admin", + password: "password", + email: "admin@homarr.dev", + group: "lldap_admin", +}; + +const ldapBase = "dc=example,dc=com"; + +describe("LLDAP authorization", () => { + test("Authorize with LLDAP successfully", async () => { + // Arrange + const lldapContainer = await createLldapContainer().start(); + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + environment: { + AUTH_PROVIDERS: "ldap", + AUTH_LDAP_URI: `ldap://host.docker.internal:${lldapContainer.getMappedPort(3890)}`, + AUTH_LDAP_BASE: ldapBase, + AUTH_LDAP_BIND_DN: `uid=${defaultCredentials.username},ou=People,${ldapBase}`, + AUTH_LDAP_BIND_PASSWORD: defaultCredentials.password, + }, + mounts: { + "/appdata": localMountPath, + }, + }).start(); + + // Skip onboarding + await db.update(sqliteSchema.onboarding).set({ + step: "finish", + }); + await db.insert(sqliteSchema.groups).values({ + id: "1", + name: defaultCredentials.group, + }); + + // Act + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Login + page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); + await page.getByLabel("Username").fill(defaultCredentials.username); + await page.getByLabel("Password").fill(defaultCredentials.password); + await page.locator("css=button[type='submit']").click(); + + // Wait for redirect + await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`, { + timeout: 10000, + }); + + // Assert + const users = await db.query.users.findMany({ + with: { + groups: { + with: { + group: true, + }, + }, + }, + }); + expect(users).toHaveLength(1); + const user = users[0]!; + expect(user).toEqual( + expect.objectContaining({ + name: defaultCredentials.username, + email: defaultCredentials.email, + provider: "ldap", + }), + ); + + const groups = user.groups.map((g) => g.group.name); + expect(groups).toContain(defaultCredentials.group); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + await lldapContainer.stop(); + }, 120_000); +}); + +const createLldapContainer = () => { + return withLogs( + new GenericContainer("lldap/lldap:stable").withExposedPorts(3890).withEnvironment({ + LLDAP_JWT_SECRET: "REPLACE_WITH_RANDOM", + LLDAP_KEY_SEED: "REPLACE_WITH_RANDOM", + LLDAP_LDAP_BASE_DN: ldapBase, + LLDAP_LDAP_USER_PASS: defaultCredentials.password, + LLDAP_LDAP_USER_EMAIL: defaultCredentials.email, + }), + ); +}; diff --git a/e2e/onboarding/onboarding-steps.ts b/e2e/onboarding/onboarding-steps.ts new file mode 100644 index 000000000..4f2c9a7b8 --- /dev/null +++ b/e2e/onboarding/onboarding-steps.ts @@ -0,0 +1,167 @@ +import { eq } from "drizzle-orm"; +import { Page } from "playwright"; +import { expect } from "vitest"; + +import * as sqliteSchema from "../../packages/db/schema/sqlite"; +import { credentialsAdminGroup, OnboardingStep } from "../../packages/definitions/src"; +import { SqliteDatabase } from "../shared/e2e-db"; + +const buttonTexts = { + fromScratch: "scratch", + oldmarrImport: "before 1.0", +}; + +class OnboardingStartStep { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + public async pressButtonAsync(button: keyof typeof buttonTexts) { + await this.page + .locator("button", { + hasText: buttonTexts[button], + }) + .click(); + } +} + +class OnboardingUserStep { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + public async waitUntilReadyAsync() { + await this.page.waitForSelector("text=administrator user"); + } + + public async fillFormAsync(input: { username: string; password: string; confirmPassword: string }) { + await this.page.getByLabel("Username").fill(input.username); + await this.page.getByLabel("Password", { exact: true }).fill(input.password); + await this.page.getByLabel("Confirm password").fill(input.confirmPassword); + } + + public async submitAsync() { + await this.page.locator("css=button[type='submit']").click(); + } + + public async assertUserAndAdminGroupInsertedAsync(db: SqliteDatabase, expectedUsername: string) { + const users = await db.query.users.findMany({ + with: { + groups: { + with: { + group: { + with: { + permissions: true, + }, + }, + }, + }, + }, + }); + expect(users).toHaveLength(1); + const user = users[0]!; + expect(user).toEqual( + expect.objectContaining({ + name: expectedUsername, + provider: "credentials", + }), + ); + expect(user.password).not.toBeNull(); + expect(user.salt).not.toBeNull(); + + const groups = user.groups.map((g) => g.group); + expect(groups).toHaveLength(1); + expect(groups[0].name).toEqual(credentialsAdminGroup); + expect(groups[0].permissions).toEqual([expect.objectContaining({ permission: "admin" })]); + } +} + +class OnboardingGroupStep { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + public async waitUntilReadyAsync() { + await this.page.waitForSelector("text=external provider"); + } + + public async fillGroupAsync(groupName: string) { + await this.page.locator("input").fill(groupName); + } + + public async submitAsync() { + await this.page.locator("css=button[type='submit']").click(); + } + + public async assertGroupInsertedAsync(db: SqliteDatabase, expectedGroupName: string) { + const group = await db.query.groups.findFirst({ + where: eq(sqliteSchema.groups.name, expectedGroupName), + with: { + permissions: true, + }, + }); + expect(group).not.toBeUndefined(); + expect(group?.permissions).toEqual([expect.objectContaining({ permission: "admin" })]); + } +} + +class OnboardingSettingsStep { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + public async waitUntilReadyAsync() { + await this.page.waitForSelector("text=Analytics"); + } + + public async submitAsync() { + await this.page.locator("css=button[type='submit']").click(); + } +} + +class OnboardingFinishStep { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + public async waitUntilReadyAsync() { + await this.page.waitForSelector("text=completed the setup"); + } +} + +export class Onboarding { + private readonly page: Page; + public readonly steps: { + start: OnboardingStartStep; + user: OnboardingUserStep; + group: OnboardingGroupStep; + settings: OnboardingSettingsStep; + finish: OnboardingFinishStep; + }; + + constructor(page: Page) { + this.page = page; + this.steps = { + start: new OnboardingStartStep(this.page), + user: new OnboardingUserStep(this.page), + group: new OnboardingGroupStep(this.page), + settings: new OnboardingSettingsStep(this.page), + finish: new OnboardingFinishStep(this.page), + }; + } + + public async assertOnboardingStepAsync(db: SqliteDatabase, expectedStep: OnboardingStep) { + const onboarding = await db.query.onboarding.findFirst(); + expect(onboarding?.step).toEqual(expectedStep); + } +} diff --git a/e2e/onboarding/onboarding.spec.ts b/e2e/onboarding/onboarding.spec.ts new file mode 100644 index 000000000..eca09b6e7 --- /dev/null +++ b/e2e/onboarding/onboarding.spec.ts @@ -0,0 +1,92 @@ +import { chromium } from "playwright"; +import { describe, test } from "vitest"; + +import { createHomarrContainer } from "../shared/create-homarr-container"; +import { createSqliteDbFileAsync } from "../shared/e2e-db"; +import { Onboarding } from "./onboarding-steps"; + +describe("Onboarding", () => { + test("Credentials onboarding should be successful", async () => { + // Arrange + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + mounts: { + "/appdata": localMountPath, + }, + }).start(); + + // Act + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + const onboarding = new Onboarding(page); + + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); + await onboarding.steps.start.pressButtonAsync("fromScratch"); + + await onboarding.steps.user.waitUntilReadyAsync(); + await onboarding.steps.user.fillFormAsync({ + username: "admin", + password: "Comp(exP4sswOrd", + confirmPassword: "Comp(exP4sswOrd", + }); + await onboarding.steps.user.submitAsync(); + + await onboarding.steps.settings.waitUntilReadyAsync(); + await onboarding.steps.settings.submitAsync(); + + await onboarding.steps.finish.waitUntilReadyAsync(); + + // Assert + await onboarding.steps.user.assertUserAndAdminGroupInsertedAsync(db, "admin"); + await onboarding.assertOnboardingStepAsync(db, "finish"); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + }, 120_000); + + test("External provider onboarding setup should be successful", async () => { + // Arrange + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + environment: { + AUTH_PROVIDERS: "ldap", + AUTH_LDAP_URI: "ldap://host.docker.internal:3890", + AUTH_LDAP_BASE: "", + AUTH_LDAP_BIND_DN: "", + AUTH_LDAP_BIND_PASSWORD: "", + }, + mounts: { + "/appdata": localMountPath, + }, + }).start(); + const externalGroupName = "oidc-admins"; + + // Act + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + const onboarding = new Onboarding(page); + + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); + await onboarding.steps.start.pressButtonAsync("fromScratch"); + + await onboarding.steps.group.waitUntilReadyAsync(); + await onboarding.steps.group.fillGroupAsync(externalGroupName); + await onboarding.steps.group.submitAsync(); + + await onboarding.steps.settings.waitUntilReadyAsync(); + await onboarding.steps.settings.submitAsync(); + + await onboarding.steps.finish.waitUntilReadyAsync(); + + // Assert + await onboarding.steps.group.assertGroupInsertedAsync(db, externalGroupName); + await onboarding.assertOnboardingStepAsync(db, "finish"); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + }, 120_000); +}); diff --git a/e2e/shared/create-homarr-container.ts b/e2e/shared/create-homarr-container.ts index 3183e66ca..a1a6cec05 100644 --- a/e2e/shared/create-homarr-container.ts +++ b/e2e/shared/create-homarr-container.ts @@ -1,18 +1,36 @@ import { GenericContainer, Wait } from "testcontainers"; +import { Environment } from "testcontainers/build/types"; -export const createHomarrContainer = () => { +export const createHomarrContainer = ( + options: { + environment?: Environment; + mounts?: { + "/appdata"?: string; + "/var/run/docker.sock"?: string; + }; + } = {}, +) => { if (!process.env.CI) { throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'"); } - return withLogs( - new GenericContainer("homarr-e2e") - .withExposedPorts(7575) - .withEnvironment({ - SECRET_ENCRYPTION_KEY: "0".repeat(64), - }) - .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)), - ); + const container = new GenericContainer("homarr-e2e") + .withExposedPorts(7575) + .withEnvironment({ + ...options.environment, + SECRET_ENCRYPTION_KEY: "0".repeat(64), + }) + .withBindMounts( + Object.entries(options.mounts ?? {}) + .filter((item) => item?.[0] !== undefined) + .map(([container, local]) => ({ + source: local, + target: container, + })), + ) + .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)); + + return withLogs(container); }; export const withLogs = (container: GenericContainer) => { diff --git a/e2e/shared/e2e-db.ts b/e2e/shared/e2e-db.ts new file mode 100644 index 000000000..0abc87823 --- /dev/null +++ b/e2e/shared/e2e-db.ts @@ -0,0 +1,32 @@ +import { mkdir } from "fs/promises"; +import path from "path"; +import { createId } from "@paralleldrive/cuid2"; +import Database from "better-sqlite3"; +import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; + +import * as sqliteSchema from "../../packages/db/schema/sqlite"; + +export const createSqliteDbFileAsync = async () => { + const localMountPath = path.join(__dirname, "tmp", createId()); + await mkdir(path.join(localMountPath, "db"), { recursive: true }); + + const localDbUrl = path.join(localMountPath, "db", "db.sqlite"); + + const connection = new Database(localDbUrl); + const db = drizzle(connection, { + schema: sqliteSchema, + casing: "snake_case", + }); + + await migrate(db, { + migrationsFolder: path.join(__dirname, "..", "..", "packages", "db", "migrations", "sqlite"), + }); + + return { + db, + localMountPath, + }; +}; + +export type SqliteDatabase = BetterSQLite3Database; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0d5decae..7e5ab4ef5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,13 +183,13 @@ importers: version: 5.62.11(@tanstack/react-query@5.62.11(react@19.0.0))(react@19.0.0) '@tanstack/react-query-next-experimental': specifier: 5.62.11 - version: 5.62.11(@tanstack/react-query@5.62.11(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) + version: 5.62.11(@tanstack/react-query@5.62.11(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) '@trpc/client': specifier: next version: 11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2) '@trpc/next': specifier: next - version: 11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2) + version: 11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2) '@trpc/react-query': specifier: next version: 11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2) @@ -231,7 +231,7 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) postcss-preset-mantine: specifier: ^1.17.0 version: 1.17.0(postcss@8.4.47) @@ -556,7 +556,7 @@ importers: version: 4.5.0 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -626,10 +626,10 @@ importers: version: 7.3.0 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) next-auth: specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) + version: 5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -706,7 +706,7 @@ importers: version: 1.11.13 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1195,7 +1195,7 @@ importers: version: 1.11.13 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1290,7 +1290,7 @@ importers: version: 0.5.16 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1530,7 +1530,7 @@ importers: version: 2.11.0(@types/react@18.3.13)(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1573,10 +1573,10 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) next-intl: specifier: 3.26.3 - version: 3.26.3(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) + version: 3.26.3(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1631,7 +1631,7 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1804,7 +1804,7 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -3341,6 +3341,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -7577,11 +7582,21 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + playwright@1.49.0: resolution: {integrity: sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==} engines: {node: '>=18'} hasBin: true + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -10772,6 +10787,11 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + optional: true + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -11335,10 +11355,10 @@ snapshots: '@tanstack/react-query': 5.62.11(react@19.0.0) react: 19.0.0 - '@tanstack/react-query-next-experimental@5.62.11(@tanstack/react-query@5.62.11(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0)': + '@tanstack/react-query-next-experimental@5.62.11(@tanstack/react-query@5.62.11(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0)': dependencies: '@tanstack/react-query': 5.62.11(react@19.0.0) - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 '@tanstack/react-query@5.62.11(react@19.0.0)': @@ -11582,11 +11602,11 @@ snapshots: '@trpc/server': 11.0.0-rc.682(typescript@5.7.2) typescript: 5.7.2 - '@trpc/next@11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2)': + '@trpc/next@11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.11(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2)': dependencies: '@trpc/client': 11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2) '@trpc/server': 11.0.0-rc.682(typescript@5.7.2) - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) typescript: 5.7.2 @@ -15219,21 +15239,21 @@ snapshots: netmask@2.0.2: {} - next-auth@5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): + next-auth@5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): dependencies: '@auth/core': 0.37.2 - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 - next-intl@3.26.3(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): + next-intl@3.26.3(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): dependencies: '@formatjs/intl-localematcher': 0.5.5 negotiator: 1.0.0 - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 use-intl: 3.26.3(react@19.0.0) - next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0): + next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0): dependencies: '@next/env': 14.2.22 '@swc/helpers': 0.5.5 @@ -15254,6 +15274,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.22 '@next/swc-win32-ia32-msvc': 14.2.22 '@next/swc-win32-x64-msvc': 14.2.22 + '@playwright/test': 1.49.1 sass: 1.83.0 transitivePeerDependencies: - '@babel/core' @@ -15687,12 +15708,22 @@ snapshots: playwright-core@1.49.0: {} + playwright-core@1.49.1: + optional: true + playwright@1.49.0: dependencies: playwright-core: 1.49.0 optionalDependencies: fsevents: 2.3.2 + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + optional: true + possible-typed-array-names@1.0.0: {} postcss-js@4.0.1(postcss@8.4.47): From bdbff0d90213b2fecc924e48a8c4531229f6ccc2 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 10:57:45 +0100 Subject: [PATCH 02/15] ci: add playwright chrome installation to e2e test --- .github/workflows/code-quality.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index a93cfe2f2..92efa0d03 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -91,6 +91,8 @@ jobs: network: host env: SKIP_ENV_VALIDATION: true + - name: Install playwright browsers + run: pnpm exec playwright install chromium - name: Run E2E Tests shell: bash run: pnpm test:e2e From 4f261fe26e2564df986870fc09eae52f67c728d9 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 11:53:09 +0100 Subject: [PATCH 03/15] fix(e2e): timeout between lldap login redirect to short --- e2e/lldap.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/e2e/lldap.spec.ts b/e2e/lldap.spec.ts index 092c3857d..f5991d7b4 100644 --- a/e2e/lldap.spec.ts +++ b/e2e/lldap.spec.ts @@ -54,9 +54,7 @@ describe("LLDAP authorization", () => { await page.locator("css=button[type='submit']").click(); // Wait for redirect - await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`, { - timeout: 10000, - }); + await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); // Assert const users = await db.query.users.findMany({ From 4355c932c2be308d7bd4054c34d330d03d1c1f96 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 11:54:13 +0100 Subject: [PATCH 04/15] test(e2e): add oidc azure test --- .github/workflows/code-quality.yml | 7 ++ e2e/env.mjs | 22 ++++++ e2e/oidc.spec.ts | 106 +++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 e2e/env.mjs create mode 100644 e2e/oidc.spec.ts diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 92efa0d03..3b54af4b3 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -95,6 +95,13 @@ jobs: run: pnpm exec playwright install chromium - name: Run E2E Tests shell: bash + env: + E2E_AZURE_OIDC_CLIENT_ID: ${{vars.E2E_AZURE_OIDC_CLIENT_ID}} + E2E_AZURE_OIDC_CLIENT_SECRET: ${{secrets.E2E_AZURE_OIDC_CLIENT_SECRET}} + E2E_AZURE_OIDC_TENANT_ID: ${{vars.E2E_AZURE_OIDC_TENANT_ID}} + E2E_AZURE_OIDC_PASSWORD: ${{secrets.E2E_AZURE_OIDC_PASSWORD}} + E2E_AZURE_OIDC_EMAIL: ${{vars.E2E_AZURE_OIDC_EMAIL}} + E2E_AZURE_OIDC_NAME: ${{vars.E2E_AZURE_OIDC_NAME}} run: pnpm test:e2e build: diff --git a/e2e/env.mjs b/e2e/env.mjs new file mode 100644 index 000000000..46df1b879 --- /dev/null +++ b/e2e/env.mjs @@ -0,0 +1,22 @@ +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; + +export const e2eEnv = createEnv({ + shared: { + E2E_AZURE_OIDC_CLIENT_ID: z.string().nonempty(), + E2E_AZURE_OIDC_CLIENT_SECRET: z.string().nonempty(), + E2E_AZURE_OIDC_TENANT_ID: z.string().nonempty(), + E2E_AZURE_OIDC_PASSWORD: z.string().nonempty(), + E2E_AZURE_OIDC_EMAIL: z.string().nonempty(), + E2E_AZURE_OIDC_NAME: z.string().nonempty(), + }, + runtimeEnv: { + E2E_AZURE_OIDC_CLIENT_ID: process.env.E2E_AZURE_OIDC_CLIENT_ID, + E2E_AZURE_OIDC_CLIENT_SECRET: process.env.E2E_AZURE_OIDC_CLIENT_SECRET, + E2E_AZURE_OIDC_TENANT_ID: process.env.E2E_AZURE_OIDC_TENANT_ID, + E2E_AZURE_OIDC_PASSWORD: process.env.E2E_AZURE_OIDC_PASSWORD, + E2E_AZURE_OIDC_EMAIL: process.env.E2E_AZURE_OIDC_EMAIL, + E2E_AZURE_OIDC_NAME: process.env.E2E_AZURE_OIDC_NAME, + }, + isServer: true, +}); diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts new file mode 100644 index 000000000..5bfca03b9 --- /dev/null +++ b/e2e/oidc.spec.ts @@ -0,0 +1,106 @@ +import { chromium } from "playwright"; +import { describe, expect, test } from "vitest"; + +import * as sqliteSchema from "../packages/db/schema/sqlite"; +import { e2eEnv } from "./env.mjs"; +import { createHomarrContainer } from "./shared/create-homarr-container"; +import { createSqliteDbFileAsync } from "./shared/e2e-db"; + +describe("OIDC authorization", () => { + test("Authorize with OIDC Azure app registration successfully", async () => { + // Arrange + const azureRole = "AzureAdmin"; + + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + environment: { + AUTH_PROVIDERS: "oidc", + AUTH_OIDC_ISSUER: `https://login.microsoftonline.com/${e2eEnv.E2E_AZURE_OIDC_TENANT_ID}/v2.0`, + AUTH_OIDC_CLIENT_SECRET: e2eEnv.E2E_AZURE_OIDC_CLIENT_SECRET, + AUTH_OIDC_CLIENT_ID: e2eEnv.E2E_AZURE_OIDC_CLIENT_ID, + AUTH_OIDC_SCOPE_OVERWRITE: "openid profile email", + AUTH_OIDC_GROUPS_ATTRIBUTE: "roles", + }, + mounts: { + "/appdata": localMountPath, + }, + }).start(); + + // Skip onboarding + await db.update(sqliteSchema.onboarding).set({ + step: "finish", + }); + await db.insert(sqliteSchema.groups).values({ + id: "1", + name: azureRole, + }); + + // Act + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Login + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); + await page.locator("css=button").click(); + + await page.waitForURL("https://login.microsoftonline.com/**"); + await page.locator("css=input[type='email']").fill(e2eEnv.E2E_AZURE_OIDC_EMAIL); + await page.locator("css=input[type='submit']").click(); + + await page.waitForSelector("text=Password"); + await page.locator("css=input[type='password']").fill(e2eEnv.E2E_AZURE_OIDC_PASSWORD); + await page.locator("css=[type='submit']").click(); + + // Will probably not work because of 'VERIFY YOUR IDENTITY' page (MFA) maybe with fake account can be tested? + try { + await page.waitForSelector("text=Stay signed in?", { timeout: 5000 }); + await page + .locator("button", { + hasText: "No", + }) + .click({ timeout: 1000 }); + } catch (e) { + console.log("Stay signed in not requested"); + } + + try { + await page.waitForSelector("text=Permissions requested", { timeout: 5000 }); + await page.locator("css=[type='submit']").click({ timeout: 1000 }); + } catch (e) { + console.log("Permissions not requested"); + } + + // Wait for redirect + await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`, { + timeout: 10000, + }); + + // Assert + const users = await db.query.users.findMany({ + with: { + groups: { + with: { + group: true, + }, + }, + }, + }); + expect(users).toHaveLength(1); + const user = users[0]!; + expect(user).toEqual( + expect.objectContaining({ + name: e2eEnv.E2E_AZURE_OIDC_NAME, + email: e2eEnv.E2E_AZURE_OIDC_EMAIL, + provider: "oidc", + }), + ); + + const groups = user.groups.map((g) => g.group.name); + expect(groups).toContain(azureRole); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + }, 60000); +}); From ece679eac6e4b5c9a9805576d8b8d454bd63248d Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 12:21:13 +0100 Subject: [PATCH 05/15] fix(e2e): lldap test fails --- e2e/lldap.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/lldap.spec.ts b/e2e/lldap.spec.ts index f5991d7b4..9dccf7097 100644 --- a/e2e/lldap.spec.ts +++ b/e2e/lldap.spec.ts @@ -48,7 +48,7 @@ describe("LLDAP authorization", () => { const page = await context.newPage(); // Login - page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); await page.getByLabel("Username").fill(defaultCredentials.username); await page.getByLabel("Password").fill(defaultCredentials.password); await page.locator("css=button[type='submit']").click(); From e33657d07a755c739e03b22031b4bd01acba1243 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 12:29:32 +0100 Subject: [PATCH 06/15] wip: add temporary error log for failed ldap server connection --- .../providers/credentials/authorization/ldap-authorization.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/auth/providers/credentials/authorization/ldap-authorization.ts b/packages/auth/providers/credentials/authorization/ldap-authorization.ts index 529d26ff2..c64184fc9 100644 --- a/packages/auth/providers/credentials/authorization/ldap-authorization.ts +++ b/packages/auth/providers/credentials/authorization/ldap-authorization.ts @@ -21,8 +21,9 @@ export const authorizeWithLdapCredentialsAsync = async ( distinguishedName: env.AUTH_LDAP_BIND_DN, password: env.AUTH_LDAP_BIND_PASSWORD, }) - .catch(() => { + .catch((error) => { logger.error("Failed to connect to LDAP server"); + logger.error(error); throw new CredentialsSignin(); }); From c6001516a27a713f67c3ca82fac9556717dd9801 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 12:43:54 +0100 Subject: [PATCH 07/15] fix(e2e): github actions don't support host.docker.internal --- e2e/shared/create-homarr-container.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/e2e/shared/create-homarr-container.ts b/e2e/shared/create-homarr-container.ts index a1a6cec05..9b13cb3fd 100644 --- a/e2e/shared/create-homarr-container.ts +++ b/e2e/shared/create-homarr-container.ts @@ -28,7 +28,13 @@ export const createHomarrContainer = ( target: container, })), ) - .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)); + .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)) + .withExtraHosts([ + { + host: "host.docker.internal", + ipAddress: "host-gateway", + }, + ]); return withLogs(container); }; From 525a9f50253516b0ff67b834567340b190dbdcea Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 15:03:23 +0100 Subject: [PATCH 08/15] chore: address pull request feedback --- e2e/oidc.spec.ts | 1 - e2e/shared/create-homarr-container.ts | 1 + .../providers/credentials/authorization/ldap-authorization.ts | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts index 5bfca03b9..470dad15b 100644 --- a/e2e/oidc.spec.ts +++ b/e2e/oidc.spec.ts @@ -52,7 +52,6 @@ describe("OIDC authorization", () => { await page.locator("css=input[type='password']").fill(e2eEnv.E2E_AZURE_OIDC_PASSWORD); await page.locator("css=[type='submit']").click(); - // Will probably not work because of 'VERIFY YOUR IDENTITY' page (MFA) maybe with fake account can be tested? try { await page.waitForSelector("text=Stay signed in?", { timeout: 5000 }); await page diff --git a/e2e/shared/create-homarr-container.ts b/e2e/shared/create-homarr-container.ts index 9b13cb3fd..7e5bcd5c4 100644 --- a/e2e/shared/create-homarr-container.ts +++ b/e2e/shared/create-homarr-container.ts @@ -31,6 +31,7 @@ export const createHomarrContainer = ( .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)) .withExtraHosts([ { + // This enabled the usage of host.docker.internal as hostname in the container host: "host.docker.internal", ipAddress: "host-gateway", }, diff --git a/packages/auth/providers/credentials/authorization/ldap-authorization.ts b/packages/auth/providers/credentials/authorization/ldap-authorization.ts index c64184fc9..529d26ff2 100644 --- a/packages/auth/providers/credentials/authorization/ldap-authorization.ts +++ b/packages/auth/providers/credentials/authorization/ldap-authorization.ts @@ -21,9 +21,8 @@ export const authorizeWithLdapCredentialsAsync = async ( distinguishedName: env.AUTH_LDAP_BIND_DN, password: env.AUTH_LDAP_BIND_PASSWORD, }) - .catch((error) => { + .catch(() => { logger.error("Failed to connect to LDAP server"); - logger.error(error); throw new CredentialsSignin(); }); From b0a19824be1a6b9100d4f8c2028341aacc9c15b9 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 15:50:29 +0100 Subject: [PATCH 09/15] refactor(e2e): move onboarding steps to onboarding actions and assertions --- e2e/lldap.spec.ts | 23 +-- e2e/oidc.spec.ts | 21 +-- e2e/{onboarding => }/onboarding.spec.ts | 59 +++---- e2e/onboarding/onboarding-steps.ts | 167 ------------------ e2e/shared/actions/onboarding-actions.ts | 53 ++++++ .../assertions/onboarding-assertions.ts | 62 +++++++ 6 files changed, 155 insertions(+), 230 deletions(-) rename e2e/{onboarding => }/onboarding.spec.ts (55%) delete mode 100644 e2e/onboarding/onboarding-steps.ts create mode 100644 e2e/shared/actions/onboarding-actions.ts create mode 100644 e2e/shared/assertions/onboarding-assertions.ts diff --git a/e2e/lldap.spec.ts b/e2e/lldap.spec.ts index 9dccf7097..f9015af55 100644 --- a/e2e/lldap.spec.ts +++ b/e2e/lldap.spec.ts @@ -2,7 +2,7 @@ import { chromium } from "playwright"; import { GenericContainer } from "testcontainers"; import { describe, expect, test } from "vitest"; -import * as sqliteSchema from "../packages/db/schema/sqlite"; +import { OnboardingActions } from "./shared/actions/onboarding-actions"; import { createHomarrContainer, withLogs } from "./shared/create-homarr-container"; import { createSqliteDbFileAsync } from "./shared/e2e-db"; @@ -33,30 +33,23 @@ describe("LLDAP authorization", () => { }, }).start(); - // Skip onboarding - await db.update(sqliteSchema.onboarding).set({ - step: "finish", - }); - await db.insert(sqliteSchema.groups).values({ - id: "1", - name: defaultCredentials.group, - }); - - // Act const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); - // Login + const onboardingActions = new OnboardingActions(page, db); + await onboardingActions.skipOnboardingAsync({ + group: defaultCredentials.group, + }); + + // Act await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); await page.getByLabel("Username").fill(defaultCredentials.username); await page.getByLabel("Password").fill(defaultCredentials.password); await page.locator("css=button[type='submit']").click(); - // Wait for redirect - await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); - // Assert + await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); const users = await db.query.users.findMany({ with: { groups: { diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts index 470dad15b..6152bc9a2 100644 --- a/e2e/oidc.spec.ts +++ b/e2e/oidc.spec.ts @@ -1,8 +1,8 @@ import { chromium } from "playwright"; import { describe, expect, test } from "vitest"; -import * as sqliteSchema from "../packages/db/schema/sqlite"; import { e2eEnv } from "./env.mjs"; +import { OnboardingActions } from "./shared/actions/onboarding-actions"; import { createHomarrContainer } from "./shared/create-homarr-container"; import { createSqliteDbFileAsync } from "./shared/e2e-db"; @@ -26,21 +26,14 @@ describe("OIDC authorization", () => { }, }).start(); - // Skip onboarding - await db.update(sqliteSchema.onboarding).set({ - step: "finish", - }); - await db.insert(sqliteSchema.groups).values({ - id: "1", - name: azureRole, - }); - - // Act const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); - // Login + const onboardingActions = new OnboardingActions(page, db); + await onboardingActions.skipOnboardingAsync({ group: azureRole }); + + // Act await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); await page.locator("css=button").click(); @@ -70,12 +63,10 @@ describe("OIDC authorization", () => { console.log("Permissions not requested"); } - // Wait for redirect + // Assert await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`, { timeout: 10000, }); - - // Assert const users = await db.query.users.findMany({ with: { groups: { diff --git a/e2e/onboarding/onboarding.spec.ts b/e2e/onboarding.spec.ts similarity index 55% rename from e2e/onboarding/onboarding.spec.ts rename to e2e/onboarding.spec.ts index eca09b6e7..061f5e6d8 100644 --- a/e2e/onboarding/onboarding.spec.ts +++ b/e2e/onboarding.spec.ts @@ -1,9 +1,10 @@ import { chromium } from "playwright"; import { describe, test } from "vitest"; -import { createHomarrContainer } from "../shared/create-homarr-container"; -import { createSqliteDbFileAsync } from "../shared/e2e-db"; -import { Onboarding } from "./onboarding-steps"; +import { OnboardingActions } from "./shared/actions/onboarding-actions"; +import { OnboardingAssertions } from "./shared/assertions/onboarding-assertions"; +import { createHomarrContainer } from "./shared/create-homarr-container"; +import { createSqliteDbFileAsync } from "./shared/e2e-db"; describe("Onboarding", () => { test("Credentials onboarding should be successful", async () => { @@ -15,36 +16,31 @@ describe("Onboarding", () => { }, }).start(); - // Act const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); - const onboarding = new Onboarding(page); + const actions = new OnboardingActions(page, db); + const assertions = new OnboardingAssertions(page, db); + // Act await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); - await onboarding.steps.start.pressButtonAsync("fromScratch"); - - await onboarding.steps.user.waitUntilReadyAsync(); - await onboarding.steps.user.fillFormAsync({ + await actions.startOnboardingAsync("scratch"); + await actions.processUserStepAsync({ username: "admin", password: "Comp(exP4sswOrd", confirmPassword: "Comp(exP4sswOrd", }); - await onboarding.steps.user.submitAsync(); - - await onboarding.steps.settings.waitUntilReadyAsync(); - await onboarding.steps.settings.submitAsync(); - - await onboarding.steps.finish.waitUntilReadyAsync(); + await actions.processSettingsStepAsync(); // Assert - await onboarding.steps.user.assertUserAndAdminGroupInsertedAsync(db, "admin"); - await onboarding.assertOnboardingStepAsync(db, "finish"); + await assertions.assertFinishStepVisibleAsync(); + await assertions.assertUserAndAdminGroupInsertedAsync("admin"); + await assertions.assertDbOnboardingStepAsync("finish"); // Cleanup await browser.close(); await homarrContainer.stop(); - }, 120_000); + }, 60_000); test("External provider onboarding setup should be successful", async () => { // Arrange @@ -63,30 +59,27 @@ describe("Onboarding", () => { }).start(); const externalGroupName = "oidc-admins"; - // Act const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); - const onboarding = new Onboarding(page); + const actions = new OnboardingActions(page, db); + const assertions = new OnboardingAssertions(page, db); + // Act await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); - await onboarding.steps.start.pressButtonAsync("fromScratch"); - - await onboarding.steps.group.waitUntilReadyAsync(); - await onboarding.steps.group.fillGroupAsync(externalGroupName); - await onboarding.steps.group.submitAsync(); - - await onboarding.steps.settings.waitUntilReadyAsync(); - await onboarding.steps.settings.submitAsync(); - - await onboarding.steps.finish.waitUntilReadyAsync(); + await actions.startOnboardingAsync("scratch"); + await actions.processExternalGroupStepAsync({ + name: externalGroupName, + }); + await actions.processSettingsStepAsync(); // Assert - await onboarding.steps.group.assertGroupInsertedAsync(db, externalGroupName); - await onboarding.assertOnboardingStepAsync(db, "finish"); + await assertions.assertFinishStepVisibleAsync(); + await assertions.assertExternalGroupInsertedAsync(externalGroupName); + await assertions.assertDbOnboardingStepAsync("finish"); // Cleanup await browser.close(); await homarrContainer.stop(); - }, 120_000); + }, 60_000); }); diff --git a/e2e/onboarding/onboarding-steps.ts b/e2e/onboarding/onboarding-steps.ts deleted file mode 100644 index 4f2c9a7b8..000000000 --- a/e2e/onboarding/onboarding-steps.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { eq } from "drizzle-orm"; -import { Page } from "playwright"; -import { expect } from "vitest"; - -import * as sqliteSchema from "../../packages/db/schema/sqlite"; -import { credentialsAdminGroup, OnboardingStep } from "../../packages/definitions/src"; -import { SqliteDatabase } from "../shared/e2e-db"; - -const buttonTexts = { - fromScratch: "scratch", - oldmarrImport: "before 1.0", -}; - -class OnboardingStartStep { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - public async pressButtonAsync(button: keyof typeof buttonTexts) { - await this.page - .locator("button", { - hasText: buttonTexts[button], - }) - .click(); - } -} - -class OnboardingUserStep { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - public async waitUntilReadyAsync() { - await this.page.waitForSelector("text=administrator user"); - } - - public async fillFormAsync(input: { username: string; password: string; confirmPassword: string }) { - await this.page.getByLabel("Username").fill(input.username); - await this.page.getByLabel("Password", { exact: true }).fill(input.password); - await this.page.getByLabel("Confirm password").fill(input.confirmPassword); - } - - public async submitAsync() { - await this.page.locator("css=button[type='submit']").click(); - } - - public async assertUserAndAdminGroupInsertedAsync(db: SqliteDatabase, expectedUsername: string) { - const users = await db.query.users.findMany({ - with: { - groups: { - with: { - group: { - with: { - permissions: true, - }, - }, - }, - }, - }, - }); - expect(users).toHaveLength(1); - const user = users[0]!; - expect(user).toEqual( - expect.objectContaining({ - name: expectedUsername, - provider: "credentials", - }), - ); - expect(user.password).not.toBeNull(); - expect(user.salt).not.toBeNull(); - - const groups = user.groups.map((g) => g.group); - expect(groups).toHaveLength(1); - expect(groups[0].name).toEqual(credentialsAdminGroup); - expect(groups[0].permissions).toEqual([expect.objectContaining({ permission: "admin" })]); - } -} - -class OnboardingGroupStep { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - public async waitUntilReadyAsync() { - await this.page.waitForSelector("text=external provider"); - } - - public async fillGroupAsync(groupName: string) { - await this.page.locator("input").fill(groupName); - } - - public async submitAsync() { - await this.page.locator("css=button[type='submit']").click(); - } - - public async assertGroupInsertedAsync(db: SqliteDatabase, expectedGroupName: string) { - const group = await db.query.groups.findFirst({ - where: eq(sqliteSchema.groups.name, expectedGroupName), - with: { - permissions: true, - }, - }); - expect(group).not.toBeUndefined(); - expect(group?.permissions).toEqual([expect.objectContaining({ permission: "admin" })]); - } -} - -class OnboardingSettingsStep { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - public async waitUntilReadyAsync() { - await this.page.waitForSelector("text=Analytics"); - } - - public async submitAsync() { - await this.page.locator("css=button[type='submit']").click(); - } -} - -class OnboardingFinishStep { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - public async waitUntilReadyAsync() { - await this.page.waitForSelector("text=completed the setup"); - } -} - -export class Onboarding { - private readonly page: Page; - public readonly steps: { - start: OnboardingStartStep; - user: OnboardingUserStep; - group: OnboardingGroupStep; - settings: OnboardingSettingsStep; - finish: OnboardingFinishStep; - }; - - constructor(page: Page) { - this.page = page; - this.steps = { - start: new OnboardingStartStep(this.page), - user: new OnboardingUserStep(this.page), - group: new OnboardingGroupStep(this.page), - settings: new OnboardingSettingsStep(this.page), - finish: new OnboardingFinishStep(this.page), - }; - } - - public async assertOnboardingStepAsync(db: SqliteDatabase, expectedStep: OnboardingStep) { - const onboarding = await db.query.onboarding.findFirst(); - expect(onboarding?.step).toEqual(expectedStep); - } -} diff --git a/e2e/shared/actions/onboarding-actions.ts b/e2e/shared/actions/onboarding-actions.ts new file mode 100644 index 000000000..8362a46a5 --- /dev/null +++ b/e2e/shared/actions/onboarding-actions.ts @@ -0,0 +1,53 @@ +import { createId } from "@paralleldrive/cuid2"; +import type { Page } from "playwright"; + +import * as sqliteSchema from "../../../packages/db/schema/sqlite"; +import type { SqliteDatabase } from "../e2e-db"; + +export class OnboardingActions { + private readonly page: Page; + private readonly db: SqliteDatabase; + + constructor(page: Page, db: SqliteDatabase) { + this.page = page; + this.db = db; + } + + public async skipOnboardingAsync(input?: { group?: string }) { + await this.db.update(sqliteSchema.onboarding).set({ + step: "finish", + }); + + if (input?.group) { + await this.db.insert(sqliteSchema.groups).values({ + id: createId(), + name: input.group, + }); + } + } + + public async startOnboardingAsync(type: "scratch" | "before 1.0") { + await this.page.locator("button", { hasText: type }).click(); + } + + public async processUserStepAsync(input: { username: string; password: string; confirmPassword: string }) { + await this.page.waitForSelector("text=administrator user"); + + await this.page.getByLabel("Username").fill(input.username); + await this.page.getByLabel("Password", { exact: true }).fill(input.password); + await this.page.getByLabel("Confirm password").fill(input.confirmPassword); + + await this.page.locator("css=button[type='submit']").click(); + } + + public async processExternalGroupStepAsync(input: { name: string }) { + await this.page.waitForSelector("text=external provider"); + await this.page.locator("input").fill(input.name); + await this.page.locator("css=button[type='submit']").click(); + } + + public async processSettingsStepAsync() { + await this.page.waitForSelector("text=Analytics"); + await this.page.locator("css=button[type='submit']").click(); + } +} diff --git a/e2e/shared/assertions/onboarding-assertions.ts b/e2e/shared/assertions/onboarding-assertions.ts new file mode 100644 index 000000000..a2f9ccb2f --- /dev/null +++ b/e2e/shared/assertions/onboarding-assertions.ts @@ -0,0 +1,62 @@ +import { eq } from "drizzle-orm"; +import type { Page } from "playwright"; +import { expect } from "vitest"; + +import * as sqliteSchema from "../../../packages/db/schema/sqlite"; +import { OnboardingStep } from "../../../packages/definitions/src"; +import { credentialsAdminGroup } from "../../../packages/definitions/src/group"; +import type { SqliteDatabase } from "../e2e-db"; + +export class OnboardingAssertions { + private readonly page: Page; + private readonly db: SqliteDatabase; + + constructor(page: Page, db: SqliteDatabase) { + this.page = page; + this.db = db; + } + + public async assertDbOnboardingStepAsync(expectedStep: OnboardingStep) { + const onboarding = await this.db.query.onboarding.findFirst(); + expect(onboarding?.step).toEqual(expectedStep); + } + + public async assertUserAndAdminGroupInsertedAsync(expectedUsername: string) { + const users = await this.db.query.users.findMany({ + with: { + groups: { + with: { + group: { + with: { + permissions: true, + }, + }, + }, + }, + }, + }); + + const user = users.find((u) => u.name === expectedUsername); + expect(user).toBeDefined(); + + const adminGroup = user!.groups.find((g) => g.group.name === credentialsAdminGroup); + expect(adminGroup).toBeDefined(); + expect(adminGroup!.group.permissions).toEqual([expect.objectContaining({ permission: "admin" })]); + } + + public async assertExternalGroupInsertedAsync(expectedGroupName: string) { + const group = await this.db.query.groups.findFirst({ + where: eq(sqliteSchema.groups.name, expectedGroupName), + with: { + permissions: true, + }, + }); + + expect(group).toBeDefined(); + expect(group!.permissions).toEqual([expect.objectContaining({ permission: "admin" })]); + } + + public async assertFinishStepVisibleAsync() { + await this.page.waitForSelector("text=completed the setup", { timeout: 5000 }); + } +} From c5264a507c792131753d7e264b5df15bb3edb83f Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 16:35:01 +0100 Subject: [PATCH 10/15] fix(e2e): increase timeout for navigating back from azure login --- e2e/oidc.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts index 6152bc9a2..00fbebbe9 100644 --- a/e2e/oidc.spec.ts +++ b/e2e/oidc.spec.ts @@ -64,9 +64,7 @@ describe("OIDC authorization", () => { } // Assert - await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`, { - timeout: 10000, - }); + await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); const users = await db.query.users.findMany({ with: { groups: { From 349fb02752e88a7ceec1d47513dd0aa8f227c9cb Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 16:45:16 +0100 Subject: [PATCH 11/15] fix: wait for url network changed error --- e2e/oidc.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts index 00fbebbe9..755e174e3 100644 --- a/e2e/oidc.spec.ts +++ b/e2e/oidc.spec.ts @@ -37,7 +37,7 @@ describe("OIDC authorization", () => { await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); await page.locator("css=button").click(); - await page.waitForURL("https://login.microsoftonline.com/**"); + await page.waitForSelector("text=No account?"); await page.locator("css=input[type='email']").fill(e2eEnv.E2E_AZURE_OIDC_EMAIL); await page.locator("css=input[type='submit']").click(); From ed297649b2825130bc683fce6929cb98aeb94e8a Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 16:51:30 +0100 Subject: [PATCH 12/15] fix: revert to wait for url --- e2e/oidc.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts index 755e174e3..00fbebbe9 100644 --- a/e2e/oidc.spec.ts +++ b/e2e/oidc.spec.ts @@ -37,7 +37,7 @@ describe("OIDC authorization", () => { await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); await page.locator("css=button").click(); - await page.waitForSelector("text=No account?"); + await page.waitForURL("https://login.microsoftonline.com/**"); await page.locator("css=input[type='email']").fill(e2eEnv.E2E_AZURE_OIDC_EMAIL); await page.locator("css=input[type='submit']").click(); From f9106f0baa763f4e477dc386ec44278c5bc7373e Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 18:31:29 +0100 Subject: [PATCH 13/15] fix(e2e): remove oidc test --- e2e/oidc.spec.ts | 94 ------------------------------------------------ 1 file changed, 94 deletions(-) delete mode 100644 e2e/oidc.spec.ts diff --git a/e2e/oidc.spec.ts b/e2e/oidc.spec.ts deleted file mode 100644 index 00fbebbe9..000000000 --- a/e2e/oidc.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { chromium } from "playwright"; -import { describe, expect, test } from "vitest"; - -import { e2eEnv } from "./env.mjs"; -import { OnboardingActions } from "./shared/actions/onboarding-actions"; -import { createHomarrContainer } from "./shared/create-homarr-container"; -import { createSqliteDbFileAsync } from "./shared/e2e-db"; - -describe("OIDC authorization", () => { - test("Authorize with OIDC Azure app registration successfully", async () => { - // Arrange - const azureRole = "AzureAdmin"; - - const { db, localMountPath } = await createSqliteDbFileAsync(); - const homarrContainer = await createHomarrContainer({ - environment: { - AUTH_PROVIDERS: "oidc", - AUTH_OIDC_ISSUER: `https://login.microsoftonline.com/${e2eEnv.E2E_AZURE_OIDC_TENANT_ID}/v2.0`, - AUTH_OIDC_CLIENT_SECRET: e2eEnv.E2E_AZURE_OIDC_CLIENT_SECRET, - AUTH_OIDC_CLIENT_ID: e2eEnv.E2E_AZURE_OIDC_CLIENT_ID, - AUTH_OIDC_SCOPE_OVERWRITE: "openid profile email", - AUTH_OIDC_GROUPS_ATTRIBUTE: "roles", - }, - mounts: { - "/appdata": localMountPath, - }, - }).start(); - - const browser = await chromium.launch(); - const context = await browser.newContext(); - const page = await context.newPage(); - - const onboardingActions = new OnboardingActions(page, db); - await onboardingActions.skipOnboardingAsync({ group: azureRole }); - - // Act - await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); - await page.locator("css=button").click(); - - await page.waitForURL("https://login.microsoftonline.com/**"); - await page.locator("css=input[type='email']").fill(e2eEnv.E2E_AZURE_OIDC_EMAIL); - await page.locator("css=input[type='submit']").click(); - - await page.waitForSelector("text=Password"); - await page.locator("css=input[type='password']").fill(e2eEnv.E2E_AZURE_OIDC_PASSWORD); - await page.locator("css=[type='submit']").click(); - - try { - await page.waitForSelector("text=Stay signed in?", { timeout: 5000 }); - await page - .locator("button", { - hasText: "No", - }) - .click({ timeout: 1000 }); - } catch (e) { - console.log("Stay signed in not requested"); - } - - try { - await page.waitForSelector("text=Permissions requested", { timeout: 5000 }); - await page.locator("css=[type='submit']").click({ timeout: 1000 }); - } catch (e) { - console.log("Permissions not requested"); - } - - // Assert - await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); - const users = await db.query.users.findMany({ - with: { - groups: { - with: { - group: true, - }, - }, - }, - }); - expect(users).toHaveLength(1); - const user = users[0]!; - expect(user).toEqual( - expect.objectContaining({ - name: e2eEnv.E2E_AZURE_OIDC_NAME, - email: e2eEnv.E2E_AZURE_OIDC_EMAIL, - provider: "oidc", - }), - ); - - const groups = user.groups.map((g) => g.group.name); - expect(groups).toContain(azureRole); - - // Cleanup - await browser.close(); - await homarrContainer.stop(); - }, 60000); -}); From 2fd5994dfb0f78cdd1cadfe319cc7d6cd974046e Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 18:33:19 +0100 Subject: [PATCH 14/15] refactor(e2e): remove env validation --- e2e/env.mjs | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 e2e/env.mjs diff --git a/e2e/env.mjs b/e2e/env.mjs deleted file mode 100644 index 46df1b879..000000000 --- a/e2e/env.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod"; - -export const e2eEnv = createEnv({ - shared: { - E2E_AZURE_OIDC_CLIENT_ID: z.string().nonempty(), - E2E_AZURE_OIDC_CLIENT_SECRET: z.string().nonempty(), - E2E_AZURE_OIDC_TENANT_ID: z.string().nonempty(), - E2E_AZURE_OIDC_PASSWORD: z.string().nonempty(), - E2E_AZURE_OIDC_EMAIL: z.string().nonempty(), - E2E_AZURE_OIDC_NAME: z.string().nonempty(), - }, - runtimeEnv: { - E2E_AZURE_OIDC_CLIENT_ID: process.env.E2E_AZURE_OIDC_CLIENT_ID, - E2E_AZURE_OIDC_CLIENT_SECRET: process.env.E2E_AZURE_OIDC_CLIENT_SECRET, - E2E_AZURE_OIDC_TENANT_ID: process.env.E2E_AZURE_OIDC_TENANT_ID, - E2E_AZURE_OIDC_PASSWORD: process.env.E2E_AZURE_OIDC_PASSWORD, - E2E_AZURE_OIDC_EMAIL: process.env.E2E_AZURE_OIDC_EMAIL, - E2E_AZURE_OIDC_NAME: process.env.E2E_AZURE_OIDC_NAME, - }, - isServer: true, -}); From 620e0c727657618d125a16ffb624528f3a1bc21d Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Jan 2025 18:33:49 +0100 Subject: [PATCH 15/15] ci: remove azure oidc env variables --- .github/workflows/code-quality.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 3b54af4b3..92efa0d03 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -95,13 +95,6 @@ jobs: run: pnpm exec playwright install chromium - name: Run E2E Tests shell: bash - env: - E2E_AZURE_OIDC_CLIENT_ID: ${{vars.E2E_AZURE_OIDC_CLIENT_ID}} - E2E_AZURE_OIDC_CLIENT_SECRET: ${{secrets.E2E_AZURE_OIDC_CLIENT_SECRET}} - E2E_AZURE_OIDC_TENANT_ID: ${{vars.E2E_AZURE_OIDC_TENANT_ID}} - E2E_AZURE_OIDC_PASSWORD: ${{secrets.E2E_AZURE_OIDC_PASSWORD}} - E2E_AZURE_OIDC_EMAIL: ${{vars.E2E_AZURE_OIDC_EMAIL}} - E2E_AZURE_OIDC_NAME: ${{vars.E2E_AZURE_OIDC_NAME}} run: pnpm test:e2e build: