mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Merge pull request #41319 from BerriAI/litellm_fix_ui_onboarding
fix(e2e): onboard dashboard users through invitations
This commit is contained in:
commit
113a416619
6 changed files with 101 additions and 46 deletions
|
|
@ -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-%';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
59
tests/e2e/ui/helpers/userOnboarding.ts
Normal file
59
tests/e2e/ui/helpers/userOnboarding.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue