diff --git a/ui/litellm-dashboard/e2e_tests/playwright.config.ts b/ui/litellm-dashboard/e2e_tests/playwright.config.ts index 9df2281016..329bb7f7af 100644 --- a/ui/litellm-dashboard/e2e_tests/playwright.config.ts +++ b/ui/litellm-dashboard/e2e_tests/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://localhost:3000", + baseURL: "http://localhost:4000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", diff --git a/ui/litellm-dashboard/e2e_tests/tests/auth/unauthenticatedRedirect.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/auth/unauthenticatedRedirect.spec.ts new file mode 100644 index 0000000000..d8cc26f864 --- /dev/null +++ b/ui/litellm-dashboard/e2e_tests/tests/auth/unauthenticatedRedirect.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Authentication Checks", () => { + test("should redirect unauthenticated user from a protected page", async ({ page }) => { + const protectedPageUrl = "http://localhost:4000/ui?page=llm-playground"; + const expectedRedirectUrl = "http://localhost:4000/ui/login/"; + await page.goto(protectedPageUrl, { waitUntil: "domcontentloaded" }); + await expect(page).toHaveURL(expectedRedirectUrl); + await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); + }); +}); diff --git a/ui/litellm-dashboard/e2e_tests/tests/users/searchUsers.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/users/searchUsers.spec.ts new file mode 100644 index 0000000000..5873bb3125 --- /dev/null +++ b/ui/litellm-dashboard/e2e_tests/tests/users/searchUsers.spec.ts @@ -0,0 +1,90 @@ +import { test, expect, Page } from "@playwright/test"; +test.describe("Internal Users Search", () => { + test.use({ storageState: "admin.storageState.json" }); + + async function goToInternalUsers(page: Page) { + await page.goto("http://localhost:4000/ui"); + + const tab = page.getByRole("menuitem", { name: "Internal User" }); + await expect(tab).toBeVisible(); + await tab.click(); + + await expect(page.locator("tbody tr").first()).toBeVisible(); + await expect(page.locator(".ant-skeleton")).toHaveCount(0); + } + + test("can search users by email", async ({ page }) => { + await goToInternalUsers(page); + + const rows = page.locator("tbody tr"); + const searchInput = page.getByPlaceholder("Search by email..."); + + await expect(searchInput).toBeVisible(); + + // Ensure initial data is loaded + const initialCount = await rows.count(); + expect(initialCount).toBeGreaterThan(0); + + // 🔹 Apply filter + wait for backend response + await Promise.all([ + page.waitForResponse( + (res) => + res.url().includes("/user/list") && + res.url().includes("user_email=test%40") && // encoded "test@" + res.status() === 200, + ), + searchInput.fill("test@"), + ]); + await page.waitForTimeout(5000); + const filteredCount = await rows.count(); + await expect(filteredCount).toBeLessThan(initialCount); + + // 🔹 Clear filter + wait for unfiltered request + await Promise.all([ + page.waitForResponse( + (res) => res.url().includes("/user/list") && !res.url().includes("user_email=") && res.status() === 200, + ), + searchInput.clear(), + ]); + + const resetCount = await rows.count(); + await expect(resetCount).toBe(initialCount); + }); + + test("can filter users by user ID and SSO ID", async ({ page }) => { + await goToInternalUsers(page); + const rows = page.locator("tbody tr"); + + // Ensure initial data is loaded + const initialCount = await rows.count(); + expect(initialCount).toBeGreaterThan(0); + + const filtersButton = page.getByRole("button", { + name: "Filters", + exact: true, + }); + await filtersButton.click(); + + const userIdInput = page.getByPlaceholder("Filter by User ID"); + const ssoIdInput = page.getByPlaceholder("Filter by SSO ID"); + await Promise.all([ + page.waitForResponse( + (res) => res.url().includes("/user/list") && res.url().includes("user_ids=user") && res.status() === 200, + ), + userIdInput.fill("user"), + ]); + + await Promise.all([ + page.waitForResponse( + (res) => + res.url().includes("/user/list") && + res.url().includes("user_ids=user") && + res.url().includes("sso_user_ids=sso") && + res.status() === 200, + ), + ssoIdInput.fill("sso"), + ]); + const combinedFilteredCount = await rows.count(); + await expect(combinedFilteredCount).toBeLessThan(initialCount); + }); +}); diff --git a/ui/litellm-dashboard/e2e_tests/tests/users/viewInternalUsers.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/users/viewInternalUsers.spec.ts new file mode 100644 index 0000000000..980c7233e4 --- /dev/null +++ b/ui/litellm-dashboard/e2e_tests/tests/users/viewInternalUsers.spec.ts @@ -0,0 +1,52 @@ +import { test, expect, Page } from "@playwright/test"; + +test.describe("Internal Users Page", () => { + test.use({ storageState: "admin.storageState.json" }); + + async function goToInternalUsers(page: Page) { + await page.goto("http://localhost:4000/ui"); + + const internalUserTab = page.getByRole("menuitem", { name: "Internal User" }); + await expect(internalUserTab).toBeVisible(); + await internalUserTab.click(); + + const firstRow = page.locator("tbody tr").first(); + await expect(firstRow).toBeVisible(); + await expect(page.locator(".ant-skeleton")).toHaveCount(0); + } + + test("renders internal users table correctly", async ({ page }) => { + await goToInternalUsers(page); + + const rows = page.locator("tbody tr"); + const rowCount = await rows.count(); + expect(rowCount).toBeGreaterThan(0); + + const userIdHeader = page.getByRole("columnheader", { name: "User ID" }); + await expect(userIdHeader).toBeVisible(); + + const virtualKeysHeader = page.getByRole("columnheader", { name: "Virtual Keys" }); + await expect(virtualKeysHeader).toBeVisible(); + }); + + test("pagination controls work correctly", async ({ page }) => { + await goToInternalUsers(page); + + const paginationInfo = page.locator(".text-sm.text-gray-700"); + const prevButton = page.getByRole("button", { name: "Previous" }); + const nextButton = page.getByRole("button", { name: "Next" }); + + const infoText = (await paginationInfo.textContent()) || ""; + + // On first page, Previous should be disabled + if (infoText.includes("1 -")) { + await expect(prevButton).toBeDisabled(); + } + + // Check if there are more pages + const hasMorePages = infoText.includes("of") && !infoText.endsWith("25 of 25"); + if (hasMorePages) { + await expect(nextButton).toBeEnabled(); + } + }); +});