fix(e2e): onboard dashboard fixtures through invitations

This commit is contained in:
Yuneng Jiang 2026-09-15 17:44:18 -07:00
parent 878716f806
commit 388eef4fbb
No known key found for this signature in database
6 changed files with 101 additions and 46 deletions

View file

@ -2,6 +2,8 @@
-- Idempotent: deletes all e2e-* rows then re-inserts deterministic data.
-- 1. Clean up in dependency order
DELETE FROM "LiteLLM_InvitationLink"
WHERE "user_id" LIKE 'e2e-%' OR "created_by" LIKE 'e2e-%' OR "updated_by" LIKE 'e2e-%';
DELETE FROM "LiteLLM_TeamMembership" WHERE "user_id" LIKE 'e2e-%';
DELETE FROM "LiteLLM_VerificationToken" WHERE token LIKE 'e2e-%';
DELETE FROM "LiteLLM_TeamTable" WHERE "team_id" LIKE 'e2e-%';

View file

@ -1,6 +1,7 @@
import { chromium, expect, request } from "@playwright/test";
import { users, Role, STORAGE_PATHS } from "./fixtures/users";
import { ARTIFACT_DIR, UI_BASE_URL } from "./constants";
import { expectUnrestrictedDashboard, setInvitedUserPassword } from "./helpers/userOnboarding";
import * as fs from "fs";
import * as path from "path";
@ -30,32 +31,37 @@ async function globalSetup() {
throw new Error(`Enabling enable_projects_ui failed (${settingsRes.status()}): ${await settingsRes.text()}`);
}
for (const { email, password, seedApiRole } of Object.values(users)) {
if (!seedApiRole) {
continue;
}
const createRes = await api.post(`${UI_BASE_URL}${rootPath}/user/new`, {
headers: { Authorization: `Bearer ${masterKey}` },
data: { user_email: email, user_role: seedApiRole, auto_create_key: false },
});
if (!createRes.ok() && createRes.status() !== 409) {
throw new Error(`Seeding user ${email} failed (${createRes.status()}): ${await createRes.text()}`);
}
const passwordRes = await api.post(`${UI_BASE_URL}${rootPath}/user/update`, {
headers: { Authorization: `Bearer ${masterKey}` },
data: { user_email: email, password },
});
if (!passwordRes.ok()) {
throw new Error(`Setting password for ${email} failed (${passwordRes.status()}): ${await passwordRes.text()}`);
}
}
await api.dispose();
for (const role of Object.values(Role)) {
const { email, password } = users[role];
const roles = [Role.ProxyAdmin, ...Object.values(Role).filter((role) => role !== Role.ProxyAdmin)];
for (const role of roles) {
const { email, password, seedApiRole } = users[role];
const storagePath = STORAGE_PATHS[role];
const page = await browser.newPage();
try {
if (seedApiRole) {
const createRes = await api.post(`${UI_BASE_URL}${rootPath}/user/new`, {
headers: { Authorization: `Bearer ${masterKey}` },
data: { user_email: email, user_role: seedApiRole, auto_create_key: false },
});
if (!createRes.ok() && createRes.status() !== 409) {
throw new Error(`Seeding user ${email} failed (${createRes.status()}): ${await createRes.text()}`);
}
const userId = createRes.ok()
? (await createRes.json()).user_id
: await (async () => {
const existing = await api.get(`${UI_BASE_URL}${rootPath}/user/list`, {
headers: { Authorization: `Bearer ${masterKey}` },
params: { user_email: email },
});
expect(existing.ok(), `Find seeded user ${email}: HTTP ${existing.status()}`).toBe(true);
const matches = (await existing.json()).users.filter(
(user: { user_email: string }) => user.user_email === email,
);
expect(matches, `Exactly one seeded user for ${email}`).toHaveLength(1);
return matches[0].user_id;
})();
expect(typeof userId, `User ID for ${email}`).toBe("string");
await setInvitedUserPassword(api, userId, password);
}
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);
@ -63,7 +69,7 @@ async function globalSetup() {
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 });
await expectUnrestrictedDashboard(page);
// Dismiss feedback popup if present
const dismiss = page.getByText("Don't ask me again");
if (await dismiss.isVisible({ timeout: 1_500 }).catch(() => false)) {
@ -100,6 +106,7 @@ async function globalSetup() {
}
}
await api.dispose();
await browser.close();
}

View file

@ -0,0 +1,59 @@
import { expect, type APIRequestContext, type Page } from "@playwright/test";
import { UI_BASE_URL } from "../constants";
import { masterKey, rootPath } from "./traffic";
const endpoint = (route: string): string => `${UI_BASE_URL}${rootPath()}${route}`;
export async function setInvitedUserPassword(
request: APIRequestContext,
userId: string,
password: string,
): Promise<void> {
const invitation = await request.post(endpoint("/invitation/new"), {
headers: { Authorization: `Bearer ${masterKey()}` },
data: { user_id: userId },
});
expect(invitation.ok(), `Create invitation for ${userId}: HTTP ${invitation.status()}`).toBe(true);
const { id } = await invitation.json();
expect(typeof id, "invitation ID").toBe("string");
const onboarding = await request.get(endpoint("/onboarding/get_token"), {
params: { invite_link: id },
});
expect(onboarding.ok(), `Get onboarding session for ${userId}: HTTP ${onboarding.status()}`).toBe(true);
const { token } = await onboarding.json();
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf-8"));
expect(typeof payload.key, "onboarding credential").toBe("string");
const claimed = await request.post(endpoint("/onboarding/claim_token"), {
headers: { Authorization: `Bearer ${payload.key}` },
data: { invitation_link: id, user_id: userId, password },
});
expect(claimed.ok(), `Claim invitation for ${userId}: HTTP ${claimed.status()}`).toBe(true);
}
export async function readDashboardSession(page: Page): Promise<{
key: string;
user_id: string;
password_reset_required?: boolean;
}> {
await expect.poll(async () => (await page.context().cookies()).some((cookie) => cookie.name === "token")).toBe(true);
const cookie = (await page.context().cookies()).find((candidate) => candidate.name === "token")!;
return JSON.parse(Buffer.from(cookie.value.split(".")[1], "base64url").toString("utf-8"));
}
export async function expectUnrestrictedDashboard(page: Page): Promise<void> {
const virtualKeys = page.getByRole("complementary").getByRole("link", { name: "Virtual Keys", exact: true });
await expect(virtualKeys).toBeVisible({ timeout: 30_000 });
const session = await readDashboardSession(page);
expect(session.password_reset_required === true, "login must not require a password reset").toBe(false);
await virtualKeys.click();
await expect(page.getByRole("main").getByRole("heading", { name: "Virtual Keys", exact: true })).toBeVisible({
timeout: 30_000,
});
const info = await page.request.get(endpoint("/user/info"), {
headers: { Authorization: `Bearer ${session.key}` },
params: { user_id: session.user_id },
});
expect(info.ok(), `Read own user with dashboard session: HTTP ${info.status()}`).toBe(true);
expect((await info.json()).user_id).toBe(session.user_id);
}

View file

@ -1,3 +1,4 @@
import { expectUnrestrictedDashboard, setInvitedUserPassword } from "../../helpers/userOnboarding";
import { test, expect, type APIRequestContext } from "@playwright/test";
import { Page } from "../../fixtures/pages";
import {
@ -76,10 +77,7 @@ test.describe("Internal User - own team key model scope", () => {
user_role: "internal_user",
auto_create_key: false,
});
await postAsMaster(request, "/user/update", {
user_id: userId,
password: MEMBER_PASSWORD,
});
await setInvitedUserPassword(request, userId, MEMBER_PASSWORD);
await postAsMaster(request, "/team/member_add", {
team_id: teamId,
member: { role: "user", user_id: userId },
@ -99,10 +97,7 @@ test.describe("Internal User - own team key model scope", () => {
.getByPlaceholder("Enter your password")
.fill(MEMBER_PASSWORD);
await page.getByRole("button", { name: "Login", exact: true }).click();
await expect(
page.locator("a", { hasText: "Virtual Keys" }),
`${email} never reached the dashboard`,
).toBeVisible({ timeout: 30_000 });
await expectUnrestrictedDashboard(page);
await dismissFeedbackPopup(page);
await navigateToPage(page, Page.ApiKeys);

View file

@ -1,3 +1,4 @@
import { expectUnrestrictedDashboard, setInvitedUserPassword } from "../../helpers/userOnboarding";
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { Page } from "../../fixtures/pages";
@ -46,19 +47,13 @@ test.describe("Second proxy admin", () => {
const userId = await inviteAdminUser();
try {
const passwordRes = await request.post("/user/update", {
headers: auth,
data: { user_email: email, password },
});
expect(passwordRes.ok(), `setting password failed (${passwordRes.status()}): ${await passwordRes.text()}`).toBe(
true,
);
await setInvitedUserPassword(request, userId, password);
await page.goto("/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 expect(page.locator("a", { hasText: "Virtual Keys" })).toBeVisible({ timeout: 30_000 });
await expectUnrestrictedDashboard(page);
await dismissFeedbackPopup(page);
await navigateToPage(page, Page.ApiKeys);

View file

@ -1,3 +1,4 @@
import { expectUnrestrictedDashboard, setInvitedUserPassword } from "../../helpers/userOnboarding";
import { test, expect, type Browser, type BrowserContext, type Page as PlaywrightPage } from "@playwright/test";
import { Page } from "../../fixtures/pages";
import { navigateToPage, dismissFeedbackPopup, clickTeamId } from "../../helpers/navigation";
@ -24,7 +25,7 @@ async function signIn(browser: Browser, email: string): Promise<BrowserContext>
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 expect(page.locator("a", { hasText: "Virtual Keys" })).toBeVisible({ timeout: 30_000 });
await expectUnrestrictedDashboard(page);
await dismissFeedbackPopup(page);
return context;
}
@ -49,11 +50,7 @@ test.describe("Team Admin - Member permissions", () => {
data: { user_id: userId, user_email: email, user_role: "internal_user", auto_create_key: false },
});
expect(created.ok(), `POST /user/new for ${userId} (${created.status()}): ${await created.text()}`).toBe(true);
const password = await request.post("/user/update", {
headers: auth(),
data: { user_id: userId, password: PASSWORD },
});
expect(password.ok(), `POST /user/update for ${userId} (${password.status()})`).toBe(true);
await setInvitedUserPassword(request, userId, PASSWORD);
};
let teamId = "";