mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
The ui suite has never gotten past globalSetup on stage. It logs in as five roles, and four of them are password users that only ever existed because run_e2e.sh and CircleCI piped fixtures/seed.sql into a database they had direct access to. The packaged e2e image has no such access, so on stage only the master-key admin could log in; the second role sat on /ui/login until waitForURL hit its 30s timeout and the whole run died before a single test. Confirmed against the stage gateway: POST /login as admin returns 303, as adminviewer@test.local returns 401, and /user/info 404s for every seeded id. Create the same budget, organization, users, teams and keys through the management endpoints in globalSetup instead, so the suite seeds whatever deployment it can reach rather than whatever database it can open. seed.sql is gone and the psql steps go with it; the API path is also the more faithful fixture, since seed.sql hand-wrote team rows no product code path produces (populated admins/members arrays, unhashed key tokens). Two behaviors the seeder has to work around, both verified live: /team/new auto-adds the caller as an admin, which would have made the proxy admin a member of the team the suite needs it to be a stranger to, so that membership is removed after each team; and /team/delete and /user/delete are all-or-nothing, so one unknown id in a batch deletes nothing, which means each id gets its own request for the cleanup to survive a partial state. Verified end to end against the stage gateway: seeding runs in ~6s, all five role logins return 303, the four teams come back with exactly their seeded members and no stray admin, all five keys resolve to the right owner and team, and re-running after deleting a seeded team and key restores both.
93 lines
4.3 KiB
TypeScript
93 lines
4.3 KiB
TypeScript
import { chromium, expect, request } from "@playwright/test";
|
|
import { users, Role, STORAGE_PATHS } from "./fixtures/users";
|
|
import { seedFixtures } from "./fixtures/seed";
|
|
import { ARTIFACT_DIR, UI_BASE_URL } from "./constants";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
async function globalSetup() {
|
|
const browser = await chromium.launch();
|
|
const rootPath = process.env.SERVER_ROOT_PATH ?? "";
|
|
|
|
// Create the artifact root before anything writes into it. storageState() does
|
|
// not create missing parents, so pointing E2E_UI_ARTIFACT_DIR at a path that
|
|
// does not exist yet would fail with ENOENT on the first role's snapshot,
|
|
// before any test ran. Playwright creates its own outputDir lazily, so this is
|
|
// the only place that has to do it. recursive:true makes it idempotent, and
|
|
// keeps the default "." a no-op.
|
|
fs.mkdirSync(ARTIFACT_DIR, { recursive: true });
|
|
|
|
// 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(`${UI_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()}`);
|
|
}
|
|
|
|
// The users, teams and keys every spec reads are created here over the
|
|
// management API rather than by loading SQL into the database, so the suite
|
|
// can run against any deployment it can reach (a local proxy, CI, or a remote
|
|
// one whose database it has no direct access to).
|
|
await seedFixtures(api, `${UI_BASE_URL}${rootPath}`, masterKey);
|
|
await api.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(`${UI_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) {
|
|
// Best-effort diagnostics only: this handler must never replace the real
|
|
// failure with its own. Writing the screenshot used to throw ENOENT/EROFS
|
|
// on the read-only cwd in the e2e image, which masked every underlying
|
|
// login error and made the run look like a filesystem bug.
|
|
try {
|
|
const failureDir = path.join(ARTIFACT_DIR, "test-results");
|
|
fs.mkdirSync(failureDir, { recursive: true });
|
|
await page.screenshot({
|
|
path: path.join(failureDir, `global-setup-${role}-failure.png`),
|
|
fullPage: true,
|
|
});
|
|
console.error(`Global setup failed for role ${role}. Screenshot saved. URL: ${page.url()}`);
|
|
} catch (diagnosticError) {
|
|
console.error(
|
|
`Global setup failed for role ${role} at URL: ${page.url()}. ` +
|
|
`Could not save a screenshot: ${diagnosticError}`,
|
|
);
|
|
}
|
|
throw e;
|
|
} finally {
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
export default globalSetup;
|