mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
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.
110 lines
4.7 KiB
TypeScript
110 lines
4.7 KiB
TypeScript
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 ?? "";
|
|
|
|
// The Projects sidebar item is hidden unless the enterprise-gated
|
|
// enable_projects_ui setting is on, and the seeded DB starts with it off.
|
|
// The proxy runs with LITELLM_LICENSE in CI, so enable it the same way
|
|
// 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(`${BASE_URL}${rootPath}/update/ui_settings`, {
|
|
headers: { Authorization: `Bearer ${masterKey}` },
|
|
data: { enable_projects_ui: true },
|
|
});
|
|
if (!settingsRes.ok()) {
|
|
throw new Error(`Enabling enable_projects_ui failed (${settingsRes.status()}): ${await settingsRes.text()}`);
|
|
}
|
|
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(`${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();
|
|
await page.waitForURL((url) => url.pathname.startsWith(`${rootPath}/ui`) && !url.pathname.includes("/login"), {
|
|
timeout: 30_000,
|
|
});
|
|
await expect(page.locator("a", { hasText: "Virtual Keys" })).toBeVisible({ timeout: 30_000 });
|
|
// Dismiss feedback popup if present
|
|
const dismiss = page.getByText("Don't ask me again");
|
|
if (await dismiss.isVisible({ timeout: 1_500 }).catch(() => false)) {
|
|
await dismiss.click();
|
|
}
|
|
// The login flow stores a post-login return URL in the litellm_return_url
|
|
// cookie. If the snapshot captures it before the app consumes it, every
|
|
// test inheriting this storageState gets yanked to that stale URL the
|
|
// first time it mounts a page (the e2e suite's main flake source).
|
|
await page.context().clearCookies({ name: "litellm_return_url" });
|
|
await page.context().storageState({ path: storagePath });
|
|
} catch (e) {
|
|
fs.mkdirSync("test-results", { recursive: true });
|
|
await page.screenshot({ path: `test-results/global-setup-${role}-failure.png`, fullPage: true });
|
|
console.error(`Global setup failed for role ${role}. Screenshot saved. URL: ${page.url()}`);
|
|
throw e;
|
|
} finally {
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
export default globalSetup;
|