From aa65dea9f1f6ea48f553e407fed556190822ea9a Mon Sep 17 00:00:00 2001 From: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:22:57 +0000 Subject: [PATCH] =?UTF-8?q?hotfix(ui):=20reset=20networking.test.ts=20to?= =?UTF-8?q?=20upstream=20v1.87.0=20=E2=80=94=20companion=20to=20#62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same root cause as #62: Wave 7's `git checkout --theirs` on `networking.test.ts` preserved our UI 401 test additions but referenced `handleErrorResponse` — a function that only existed in our cherry-pick version of `networking.tsx`. After #62 reset `networking.tsx` to upstream, the tests pointed at a now-nonexistent symbol. Before: 5 of 21 tests fail with `TypeError: handleErrorResponse is not a function`. After: 18 of 18 upstream tests pass. Our UI 401 test logic is deferred to the same follow-up PR that re-applies the 401-redirect logic onto upstream-clean networking.tsx. Tier: B (internal infra hotfix; final piece of Wave 7's misresolved --theirs). --- .../src/components/networking.test.ts | 118 ++++++++---------- 1 file changed, 50 insertions(+), 68 deletions(-) diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts index c358859ea3f..05aa2d51624 100644 --- a/ui/litellm-dashboard/src/components/networking.test.ts +++ b/ui/litellm-dashboard/src/components/networking.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; +import { clearTokenCookies } from "@/utils/cookieUtils"; import * as Networking from "./networking"; vi.mock("@/utils/cookieUtils", () => ({ @@ -80,73 +80,6 @@ describe("networking - expired session handling", () => { }); }); -describe("handleErrorResponse - status-aware auth handling", () => { - // Stub window.location so the redirect path doesn't crash jsdom. - let originalLocation: Location; - - beforeEach(() => { - vi.clearAllMocks(); - originalLocation = window.location; - delete (window as any).location; - (window as any).location = { ...originalLocation, href: "/admin", pathname: "/admin" }; - }); - - afterEach(() => { - (window as any).location = originalLocation; - }); - - it("redirects on 401 when the auth cookie is gone (session expired)", async () => { - vi.mocked(getCookie).mockReturnValue(undefined as any); - - await Networking.handleErrorResponse({ status: 401 }, { error: "no cookie" }); - - expect(clearTokenCookies).toHaveBeenCalledOnce(); - }); - - it("redirects on 401 when the body carries a session-expired marker", async () => { - // Cookie still set, but the body explicitly says the credential is dead. - vi.mocked(getCookie).mockReturnValue("any-token" as any); - - await Networking.handleErrorResponse( - { status: 401 }, - { error: { message: "Authentication Error - Expired Key" } }, - ); - - expect(clearTokenCookies).toHaveBeenCalledOnce(); - }); - - it("does NOT redirect on 401 when the cookie is still valid and body says no session-expired marker", async () => { - // This is the "logged in but called an admin-only endpoint" case. - // LiteLLM uses 401 for permission too — we must not bounce the user - // out of an otherwise healthy session. - vi.mocked(getCookie).mockReturnValue("valid-token" as any); - - await Networking.handleErrorResponse( - { status: 401 }, - { error: { message: "Master Key required" } }, - ); - - expect(clearTokenCookies).not.toHaveBeenCalled(); - }); - - it("does NOT redirect on 403 (permission denied)", async () => { - vi.mocked(getCookie).mockReturnValue("valid-token" as any); - - await Networking.handleErrorResponse({ status: 403 }, { error: "forbidden" }); - - expect(clearTokenCookies).not.toHaveBeenCalled(); - }); - - it("falls through to handleError for non-401/403 errors", async () => { - vi.mocked(getCookie).mockReturnValue("valid-token" as any); - - // 500 should not trigger the auth redirect. - await Networking.handleErrorResponse({ status: 500 }, { error: "internal" }); - - expect(clearTokenCookies).not.toHaveBeenCalled(); - }); -}); - describe("loginCall - storeLoginToken integration", () => { const originalFetch = global.fetch; @@ -471,3 +404,52 @@ describe("individualModelHealthCheckCall", () => { expect(parsed.searchParams.get("model_id")).toBe("id/with/slashes"); }); }); + +describe("teamInfoCall", () => { + const originalFetch = global.fetch; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("should URL-encode team_id query param to handle special characters safely", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ team_id: "team with spaces & special?chars" }), + } as any); + global.fetch = mockFetch as any; + + const teamID = "team with spaces & special?chars"; + await Networking.teamInfoCall("token", teamID); + + expect(mockFetch).toHaveBeenCalledOnce(); + const [url] = mockFetch.mock.calls[0]; + const urlStr = typeof url === "string" ? url : (url as Request).url; + const parsed = typeof url === "string" ? new URL(url, "http://example.com") : new URL((url as Request).url); + + expect(urlStr).toContain("/team/info"); + // Encoded value is present in the raw URL string (verifies encodeURIComponent was used) + expect(urlStr).toContain(`team_id=${encodeURIComponent(teamID)}`); + // Round-trip parse returns the original team_id + expect(parsed.searchParams.get("team_id")).toBe(teamID); + }); + + it("should not append team_id when teamID is null", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({}), + } as any); + global.fetch = mockFetch as any; + + await Networking.teamInfoCall("token", null); + + expect(mockFetch).toHaveBeenCalledOnce(); + const [url] = mockFetch.mock.calls[0]; + const parsed = typeof url === "string" ? new URL(url, "http://example.com") : new URL((url as Request).url); + expect(parsed.searchParams.has("team_id")).toBe(false); + }); +});