diff --git a/tests/e2e/ui/constants.ts b/tests/e2e/ui/constants.ts index 236909384b0..ad20be0bf32 100644 --- a/tests/e2e/ui/constants.ts +++ b/tests/e2e/ui/constants.ts @@ -1,9 +1,19 @@ +// Target base URL for the proxy and its admin UI. Derived from the environment +// so the suite can run against a deployed proxy as well as a local one; the +// previous hardcoded http://localhost:4000 made it local-only. +export const BASE_URL = (process.env.LITELLM_PROXY_URL ?? "http://localhost:4000").replace(/\/+$/, ""); + +// Where Playwright storage state is written. cwd is read-only in the e2e runner +// image (readOnlyRootFilesystem), so allow redirecting it there while keeping +// the repo-relative default for local runs. +export const STATE_DIR = (process.env.PLAYWRIGHT_STATE_DIR ?? ".").replace(/\/+$/, ""); + // Storage state paths for each role -export const ADMIN_STORAGE_PATH = "admin.storageState.json"; -export const ADMIN_VIEWER_STORAGE_PATH = "adminViewer.storageState.json"; -export const INTERNAL_USER_STORAGE_PATH = "internalUser.storageState.json"; -export const INTERNAL_VIEWER_STORAGE_PATH = "internalViewer.storageState.json"; -export const TEAM_ADMIN_STORAGE_PATH = "teamAdmin.storageState.json"; +export const ADMIN_STORAGE_PATH = `${STATE_DIR}/admin.storageState.json`; +export const ADMIN_VIEWER_STORAGE_PATH = `${STATE_DIR}/adminViewer.storageState.json`; +export const INTERNAL_USER_STORAGE_PATH = `${STATE_DIR}/internalUser.storageState.json`; +export const INTERNAL_VIEWER_STORAGE_PATH = `${STATE_DIR}/internalViewer.storageState.json`; +export const TEAM_ADMIN_STORAGE_PATH = `${STATE_DIR}/teamAdmin.storageState.json`; // Seeded user identities (match seed.sql) export const E2E_PROXY_ADMIN_USER_ID = "e2e-proxy-admin"; diff --git a/tests/e2e/ui/fixtures/users.ts b/tests/e2e/ui/fixtures/users.ts index 7d6d356cefb..f82ad07d3fd 100644 --- a/tests/e2e/ui/fixtures/users.ts +++ b/tests/e2e/ui/fixtures/users.ts @@ -1,3 +1,5 @@ +import { STATE_DIR } from "../constants"; + export enum Role { ProxyAdmin = "proxy_admin", ProxyAdminViewer = "proxy_admin_viewer", @@ -30,9 +32,9 @@ export const users: Record = { }; export const STORAGE_PATHS: Record = { - [Role.ProxyAdmin]: "admin.storageState.json", - [Role.ProxyAdminViewer]: "adminViewer.storageState.json", - [Role.InternalUser]: "internalUser.storageState.json", - [Role.InternalUserViewer]: "internalViewer.storageState.json", - [Role.TeamAdmin]: "teamAdmin.storageState.json", + [Role.ProxyAdmin]: `${STATE_DIR}/admin.storageState.json`, + [Role.ProxyAdminViewer]: `${STATE_DIR}/adminViewer.storageState.json`, + [Role.InternalUser]: `${STATE_DIR}/internalUser.storageState.json`, + [Role.InternalUserViewer]: `${STATE_DIR}/internalViewer.storageState.json`, + [Role.TeamAdmin]: `${STATE_DIR}/teamAdmin.storageState.json`, }; diff --git a/tests/e2e/ui/globalSetup.ts b/tests/e2e/ui/globalSetup.ts index ef892870268..2291c543622 100644 --- a/tests/e2e/ui/globalSetup.ts +++ b/tests/e2e/ui/globalSetup.ts @@ -1,7 +1,41 @@ import { chromium, expect, request } from "@playwright/test"; import { users, Role, STORAGE_PATHS } from "./fixtures/users"; +import { BASE_URL } from "./constants"; import * as fs from "fs"; +// The suite historically relied on fixtures/seed.sql, which only exists on a +// locally provisioned database. Create the password users through the proxy API +// instead so the suite can run against any target, seeded or not. +// +// /user/new accepts a `password` field but does not persist it (login then +// reports "User has no password set"), so the password is set in a follow-up +// /user/update. Both calls are tolerant of the user already existing, which +// keeps repeat runs and parallel shards idempotent. +async function ensurePasswordUser( + api: Awaited>, + masterKey: string, + email: string, + password: string, + userRole: string, +) { + const auth = { Authorization: `Bearer ${masterKey}` }; + const userId = `e2e-${email.split("@")[0]}`; + + // 400 here means the user already exists, which is fine. + await api.post(`${BASE_URL}/user/new`, { + headers: auth, + data: { user_id: userId, user_email: email, user_role: userRole }, + }); + + const res = await api.post(`${BASE_URL}/user/update`, { + headers: auth, + data: { user_id: userId, password }, + }); + if (!res.ok()) { + throw new Error(`Setting the password for ${email} failed (${res.status()}): ${await res.text()}`); + } +} + async function globalSetup() { const browser = await chromium.launch(); const rootPath = process.env.SERVER_ROOT_PATH ?? ""; @@ -12,7 +46,7 @@ async function globalSetup() { // the admin UI toggle does; the projects migration smoke needs the link. const masterKey = process.env.LITELLM_MASTER_KEY || "sk-1234"; const api = await request.newContext(); - const settingsRes = await api.patch(`http://localhost:4000${rootPath}/update/ui_settings`, { + const settingsRes = await api.patch(`${BASE_URL}${rootPath}/update/ui_settings`, { headers: { Authorization: `Bearer ${masterKey}` }, data: { enable_projects_ui: true }, }); @@ -21,12 +55,27 @@ async function globalSetup() { } await api.dispose(); + // ProxyAdmin logs in as "admin" with the master key and needs no user row; + // every other role is a real user that must exist with a password. + const seededRoles: Record = { + [Role.ProxyAdminViewer]: "proxy_admin_viewer", + [Role.InternalUser]: "internal_user", + [Role.InternalUserViewer]: "internal_user_viewer", + [Role.TeamAdmin]: "internal_user", + }; + const seedApi = await request.newContext(); + for (const [role, userRole] of Object.entries(seededRoles)) { + const { email, password } = users[role as Role]; + await ensurePasswordUser(seedApi, masterKey, email, password, userRole); + } + await seedApi.dispose(); + for (const role of Object.values(Role)) { const { email, password } = users[role]; const storagePath = STORAGE_PATHS[role]; const page = await browser.newPage(); try { - await page.goto(`http://localhost:4000${rootPath}/ui/login`); + await page.goto(`${BASE_URL}${rootPath}/ui/login`); await page.getByPlaceholder("Enter your username").fill(email); await page.getByPlaceholder("Enter your password").fill(password); await page.getByRole("button", { name: "Login", exact: true }).click(); diff --git a/tests/e2e/ui/playwright.config.ts b/tests/e2e/ui/playwright.config.ts index 8d586ce9503..fe0e1f188d5 100644 --- a/tests/e2e/ui/playwright.config.ts +++ b/tests/e2e/ui/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; +import { BASE_URL } from "./constants"; /** * See https://playwright.dev/docs/test-configuration. @@ -20,7 +21,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://localhost:4000", + baseURL: BASE_URL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry",