From 37e6e2da1c78e11f4d2edec923f83307334b5e7a Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 30 May 2026 00:29:30 -0700 Subject: [PATCH] test(e2e): assert internal-user navbar identity is scoped to that user (#29077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(e2e): assert internal-user navbar identity is scoped to that user The existing login.spec.ts only checks the admin's navbar identity. This adds the symmetric check for the internal user — verifying the account button + dropdown surface the internal user's email, id, and role, and that no admin-scoped values leak through. * test(e2e): harden navbar identity test per review feedback Locate the user dropdown panel by a data-testid on the popupRender div instead of Ant Design internal + Tailwind class names, so styling refactors no longer risk breaking the identity-scoping assertions. Source the seeded user emails/ids from shared constants (match seed.sql) instead of hardcoding them inline. --- ui/litellm-dashboard/e2e_tests/constants.ts | 6 +++ .../tests/login/internalUserIdentity.spec.ts | 48 +++++++++++++++++++ .../Navbar/UserDropdown/UserDropdown.tsx | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts diff --git a/ui/litellm-dashboard/e2e_tests/constants.ts b/ui/litellm-dashboard/e2e_tests/constants.ts index 8b1b4313869..236909384b0 100644 --- a/ui/litellm-dashboard/e2e_tests/constants.ts +++ b/ui/litellm-dashboard/e2e_tests/constants.ts @@ -5,6 +5,12 @@ 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"; +// Seeded user identities (match seed.sql) +export const E2E_PROXY_ADMIN_USER_ID = "e2e-proxy-admin"; +export const E2E_PROXY_ADMIN_EMAIL = "admin@test.local"; +export const E2E_INTERNAL_USER_ID = "e2e-internal-user"; +export const E2E_INTERNAL_USER_EMAIL = "internal@test.local"; + // Key aliases for seeded test keys (match seed.sql) export const E2E_UPDATE_LIMITS_KEY_ALIAS = "e2eUpdateLimitsKey"; export const E2E_DELETE_KEY_ALIAS = "e2eDeleteKey"; diff --git a/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts new file mode 100644 index 00000000000..cbe95276929 --- /dev/null +++ b/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from "@playwright/test"; +import { + E2E_INTERNAL_USER_EMAIL, + E2E_INTERNAL_USER_ID, + E2E_PROXY_ADMIN_EMAIL, + E2E_PROXY_ADMIN_USER_ID, + INTERNAL_USER_STORAGE_PATH, +} from "../../constants"; + +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +test.describe("Navbar identity scoping", () => { + test.use({ storageState: INTERNAL_USER_STORAGE_PATH }); + + test("Internal user navbar dropdown shows their own role and user id, not the admin's", async ({ page }) => { + await page.goto("/ui"); + await expect(page.getByText("Virtual Keys")).toBeVisible({ timeout: 10_000 }); + + // The account menu button carries the user's role and email/id in its + // aria-label (see UserDropdown.tsx). Match by partial role. + const accountButton = page.locator('button[aria-label^="Account menu"]').first(); + await expect(accountButton).toHaveAttribute("aria-label", /Internal User/, { timeout: 5_000 }); + await expect(accountButton).toHaveAttribute( + "aria-label", + new RegExp( + `signed in as (${escapeRegExp(E2E_INTERNAL_USER_EMAIL)}|${escapeRegExp(E2E_INTERNAL_USER_ID)})`, + ), + { timeout: 5_000 }, + ); + + // Open the dropdown (UserDropdown configures trigger=["click"]). + await accountButton.click(); + + // Locate the panel by its test id (data-testid on the popupRender div in + // UserDropdown.tsx) rather than Ant/Tailwind class names, so styling + // refactors don't silently break the identity-scoping assertions below. + const popup = page.getByTestId("user-dropdown-panel"); + await expect(popup).toBeVisible({ timeout: 5_000 }); + + // The popup must show the internal user's identity — not the seeded + // proxy admin's email/id, which would indicate a session/scope leak. + await expect(popup.getByText(E2E_INTERNAL_USER_EMAIL)).toBeVisible({ timeout: 5_000 }); + await expect(popup.getByText(E2E_INTERNAL_USER_ID)).toBeVisible({ timeout: 5_000 }); + await expect(popup.getByText("Internal User", { exact: true })).toBeVisible({ timeout: 5_000 }); + await expect(popup.getByText(E2E_PROXY_ADMIN_EMAIL)).toHaveCount(0); + await expect(popup.getByText(E2E_PROXY_ADMIN_USER_ID)).toHaveCount(0); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx index 64a2f1260ba..03312faaa39 100644 --- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx @@ -221,7 +221,7 @@ const UserDropdown: React.FC = ({ onLogout }) => { trigger={["click"]} menu={{ items: userItems }} popupRender={(menu) => ( -
+
{renderUserInfoSection()} {React.cloneElement(menu as React.ReactElement, {