mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
test(e2e/ui): derive the target from the environment and self-seed its users
tests/e2e/ui could only ever run against a locally provisioned stack, so it has never run in the e2e pod. Three things pinned it there. playwright.config.ts hardcoded baseURL http://localhost:4000 and globalSetup.ts hardcoded the same host for /update/ui_settings and /ui/login. Both now derive from LITELLM_PROXY_URL, falling back to localhost:4000 so local runs are unchanged. Storage state paths were repo-relative, so globalSetup wrote admin.storageState.json into cwd. That is read-only in the runner image under readOnlyRootFilesystem. They now sit under PLAYWRIGHT_STATE_DIR, defaulting to "." for local runs. The per-role constants the specs import directly were switched over too, so writer and readers stay in agreement. globalSetup logged in as five roles whose users only exist if fixtures/seed.sql was applied, and it throws when a login fails, so against an unseeded target the whole suite died before a single spec ran. It now creates those users through the proxy API first. ProxyAdmin is untouched since it logs in as "admin" with the master key and needs no user row. Verified against a live proxy that the seeding path works: /user/new returns 200, /user/update sets the password, and /v2/login then authenticates as that user. Worth knowing that /user/new accepts a password field but does not persist it ("User has no password set" on login), which is why the password is applied in a second call. All 25 spec files and 86 tests still collect, and the resolved values are correct (trailing slash stripped, storage state redirected). Teams, keys and the org from seed.sql are not self-seeded yet, so specs that reference those ids still need them present on the target.
This commit is contained in:
parent
c572983422
commit
12b3d163b6
4 changed files with 75 additions and 13 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<Role, { email: string; password: string }> = {
|
|||
};
|
||||
|
||||
export const STORAGE_PATHS: Record<Role, string> = {
|
||||
[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`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof request.newContext>>,
|
||||
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<string, string> = {
|
||||
[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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue