diff --git a/ui/litellm-dashboard/src/components/model_add/ChatGPTLoginButton.test.tsx b/ui/litellm-dashboard/src/components/model_add/ChatGPTLoginButton.test.tsx new file mode 100644 index 00000000000..2950f7f491a --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/ChatGPTLoginButton.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import ChatGPTLoginButton from "./ChatGPTLoginButton"; + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => ({ accessToken: "test-token" }), +})); + +vi.mock("@/components/networking", async () => { + const actual = await vi.importActual("@/components/networking"); + return { + ...actual, + chatgptOauthStartCall: vi.fn(), + chatgptOauthStatusCall: vi.fn(), + chatgptOauthCancelCall: vi.fn(), + }; +}); + +describe("ChatGPTLoginButton", () => { + it("renders with the ChatGPT provider label", () => { + render(); + expect( + screen.getByRole("button", { name: /Sign in with ChatGPT/i }), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/model_add/CopilotLoginButton.test.tsx b/ui/litellm-dashboard/src/components/model_add/CopilotLoginButton.test.tsx new file mode 100644 index 00000000000..8a701b1c17b --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/CopilotLoginButton.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CopilotLoginButton from "./CopilotLoginButton"; + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => ({ accessToken: "test-token" }), +})); + +vi.mock("@/components/networking", async () => { + const actual = await vi.importActual("@/components/networking"); + return { + ...actual, + copilotOauthStartCall: vi.fn(), + copilotOauthStatusCall: vi.fn(), + copilotOauthCancelCall: vi.fn(), + }; +}); + +describe("CopilotLoginButton", () => { + it("renders with the GitHub Copilot provider label", () => { + render(); + expect( + screen.getByRole("button", { name: /Sign in with GitHub Copilot/i }), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/model_add/OAuthDeviceLoginButton.test.tsx b/ui/litellm-dashboard/src/components/model_add/OAuthDeviceLoginButton.test.tsx new file mode 100644 index 00000000000..cb874098f2e --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/OAuthDeviceLoginButton.test.tsx @@ -0,0 +1,170 @@ +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import OAuthDeviceLoginButton from "./OAuthDeviceLoginButton"; + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => ({ accessToken: "test-token" }), +})); + +describe("OAuthDeviceLoginButton", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + + const baseProps = { + providerLabel: "ChatGPT", + startCall: vi.fn(), + statusCall: vi.fn(), + cancelCall: vi.fn(), + onSuccess: vi.fn(), + }; + + it("disables Sign-in when no credential name is provided", () => { + render(); + const button = screen.getByRole("button", { name: /Sign in with ChatGPT/i }); + expect(button).toBeDisabled(); + }); + + it("enables Sign-in once a credential name is provided", () => { + render( + , + ); + expect( + screen.getByRole("button", { name: /Sign in with ChatGPT/i }), + ).toBeEnabled(); + }); + + it("on Sign-in click, shows the user code and verification URL returned by startCall", async () => { + const startCall = vi.fn().mockResolvedValue({ + session_id: "sess-1", + user_code: "ABCD-1234", + verification_url: "https://auth.example/code", + interval: 5, + }); + const statusCall = vi.fn().mockResolvedValue({ status: "pending" }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i })); + + await waitFor(() => { + expect(screen.getByText("ABCD-1234")).toBeInTheDocument(); + }); + expect( + screen.getByRole("link", { name: "https://auth.example/code" }), + ).toHaveAttribute("href", "https://auth.example/code"); + expect(startCall).toHaveBeenCalledWith("test-token", "my-creds"); + }); + + it("calls onSuccess when polling reports success", async () => { + const onSuccess = vi.fn(); + const startCall = vi.fn().mockResolvedValue({ + session_id: "sess-1", + user_code: "CODE", + verification_url: "https://auth.example/code", + interval: 5, + }); + const statusCall = vi.fn().mockResolvedValue({ status: "success" }); + + render( + , + ); + fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i })); + + // Poll interval is 3s; allow up to 10s of real time for the first poll + // to fire and resolve with status=success. + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalledTimes(1); + }, + { timeout: 10000 }, + ); + }, 15000); + + it("surfaces error status from polling", async () => { + const startCall = vi.fn().mockResolvedValue({ + session_id: "sess-1", + user_code: "CODE", + verification_url: "https://auth.example/code", + interval: 5, + }); + const statusCall = vi + .fn() + .mockResolvedValue({ status: "error", message: "upstream failure" }); + + render( + , + ); + fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i })); + + await waitFor( + () => { + expect(screen.getByText("upstream failure")).toBeInTheDocument(); + }, + { timeout: 10000 }, + ); + }, 15000); + + it("on Cancel click, calls cancelCall and returns to idle state", async () => { + const cancelCall = vi.fn().mockResolvedValue(undefined); + const startCall = vi.fn().mockResolvedValue({ + session_id: "sess-1", + user_code: "CODE", + verification_url: "https://auth.example/code", + interval: 5, + }); + const statusCall = vi.fn().mockResolvedValue({ status: "pending" }); + + render( + , + ); + fireEvent.click(screen.getByRole("button", { name: /Sign in with ChatGPT/i })); + await waitFor(() => { + expect(screen.getByText("CODE")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole("button", { name: /Cancel/i })); + + await waitFor(() => { + expect(cancelCall).toHaveBeenCalledWith("test-token", "sess-1"); + }); + // Back to idle — Sign-in button visible again + expect( + screen.getByRole("button", { name: /Sign in with ChatGPT/i }), + ).toBeInTheDocument(); + }); +});