mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
feat(ui): add userGetInfoV2 networking function and migrate useCurrentUser hook
- Add UserInfoV2Response type and userGetInfoV2() function in networking.tsx - Migrate useCurrentUser hook from userInfoCall to userGetInfoV2 - Update useCurrentUser.test.ts to test new v2 API integration - The hook no longer needs userRole since the endpoint handles auth itself Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
This commit is contained in:
parent
679b8fd52a
commit
d03404d21e
3 changed files with 101 additions and 66 deletions
|
|
@ -3,12 +3,12 @@ import { renderHook, waitFor } from "@testing-library/react";
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useCurrentUser } from "./useCurrentUser";
|
||||
import { userInfoCall } from "@/components/networking";
|
||||
import type { UserInfo } from "@/components/view_users/types";
|
||||
import { userGetInfoV2 } from "@/components/networking";
|
||||
import type { UserInfoV2Response } from "@/components/networking";
|
||||
|
||||
// Mock the networking function
|
||||
vi.mock("@/components/networking", () => ({
|
||||
userInfoCall: vi.fn(),
|
||||
userGetInfoV2: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the queryKeysFactory - we'll mock the specific return value
|
||||
|
|
@ -28,21 +28,22 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
|||
default: () => mockUseAuthorized(),
|
||||
}));
|
||||
|
||||
// Mock data - response from userInfoCall should have user_info property
|
||||
const mockUserInfoResponse = {
|
||||
user_info: {
|
||||
user_id: "test-user-id",
|
||||
user_email: "test@example.com",
|
||||
user_alias: "Test User",
|
||||
user_role: "Admin",
|
||||
spend: 150.75,
|
||||
max_budget: 1000.0,
|
||||
key_count: 5,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
sso_user_id: null,
|
||||
budget_duration: "monthly",
|
||||
} as UserInfo,
|
||||
// Mock data - response from userGetInfoV2 is the user object directly
|
||||
const mockUserInfoV2Response: UserInfoV2Response = {
|
||||
user_id: "test-user-id",
|
||||
user_email: "test@example.com",
|
||||
user_alias: "Test User",
|
||||
user_role: "internal_user",
|
||||
spend: 150.75,
|
||||
max_budget: 1000.0,
|
||||
models: ["gpt-4"],
|
||||
budget_duration: "monthly",
|
||||
budget_reset_at: null,
|
||||
metadata: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
sso_user_id: null,
|
||||
teams: ["team-1"],
|
||||
};
|
||||
|
||||
describe("useCurrentUser", () => {
|
||||
|
|
@ -77,8 +78,8 @@ describe("useCurrentUser", () => {
|
|||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
|
||||
it("should return user info data when query is successful", async () => {
|
||||
// Mock successful API call
|
||||
(userInfoCall as any).mockResolvedValue(mockUserInfoResponse);
|
||||
// Mock successful API call - v2 returns user object directly
|
||||
(userGetInfoV2 as any).mockResolvedValue(mockUserInfoV2Response);
|
||||
|
||||
const { result } = renderHook(() => useCurrentUser(), { wrapper });
|
||||
|
||||
|
|
@ -92,18 +93,19 @@ describe("useCurrentUser", () => {
|
|||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockUserInfoResponse.user_info);
|
||||
expect(result.current.data).toEqual(mockUserInfoV2Response);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(userInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", false, null, null);
|
||||
expect(userInfoCall).toHaveBeenCalledTimes(1);
|
||||
// v2 call only needs accessToken (no userId for self-lookup)
|
||||
expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token");
|
||||
expect(userGetInfoV2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle error when userInfoCall fails", async () => {
|
||||
it("should handle error when userGetInfoV2 fails", async () => {
|
||||
const errorMessage = "Failed to fetch user info";
|
||||
const testError = new Error(errorMessage);
|
||||
|
||||
// Mock failed API call
|
||||
(userInfoCall as any).mockRejectedValue(testError);
|
||||
(userGetInfoV2 as any).mockRejectedValue(testError);
|
||||
|
||||
const { result } = renderHook(() => useCurrentUser(), { wrapper });
|
||||
|
||||
|
|
@ -118,8 +120,8 @@ describe("useCurrentUser", () => {
|
|||
|
||||
expect(result.current.error).toEqual(testError);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(userInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", false, null, null);
|
||||
expect(userInfoCall).toHaveBeenCalledTimes(1);
|
||||
expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token");
|
||||
expect(userGetInfoV2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not execute query when accessToken is missing", async () => {
|
||||
|
|
@ -143,7 +145,7 @@ describe("useCurrentUser", () => {
|
|||
expect(result.current.isFetched).toBe(false);
|
||||
|
||||
// API should not be called
|
||||
expect(userInfoCall).not.toHaveBeenCalled();
|
||||
expect(userGetInfoV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not execute query when userId is missing", async () => {
|
||||
|
|
@ -167,31 +169,7 @@ describe("useCurrentUser", () => {
|
|||
expect(result.current.isFetched).toBe(false);
|
||||
|
||||
// API should not be called
|
||||
expect(userInfoCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not execute query when userRole is missing", async () => {
|
||||
// Mock missing userRole
|
||||
mockUseAuthorized.mockReturnValue({
|
||||
accessToken: "test-access-token",
|
||||
userId: "test-user-id",
|
||||
userRole: null,
|
||||
token: "test-token",
|
||||
userEmail: "test@example.com",
|
||||
premiumUser: false,
|
||||
disabledPersonalKeyCreation: null,
|
||||
showSSOBanner: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useCurrentUser(), { wrapper });
|
||||
|
||||
// Query should not execute
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(result.current.isFetched).toBe(false);
|
||||
|
||||
// API should not be called
|
||||
expect(userInfoCall).not.toHaveBeenCalled();
|
||||
expect(userGetInfoV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not execute query when all auth values are missing", async () => {
|
||||
|
|
@ -215,12 +193,12 @@ describe("useCurrentUser", () => {
|
|||
expect(result.current.isFetched).toBe(false);
|
||||
|
||||
// API should not be called
|
||||
expect(userInfoCall).not.toHaveBeenCalled();
|
||||
expect(userGetInfoV2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should execute query when all auth values are present", async () => {
|
||||
// Mock successful API call
|
||||
(userInfoCall as any).mockResolvedValue(mockUserInfoResponse);
|
||||
(userGetInfoV2 as any).mockResolvedValue(mockUserInfoV2Response);
|
||||
|
||||
// Ensure all auth values are present (already set in beforeEach)
|
||||
const { result } = renderHook(() => useCurrentUser(), { wrapper });
|
||||
|
|
@ -230,15 +208,15 @@ describe("useCurrentUser", () => {
|
|||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(userInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", false, null, null);
|
||||
expect(userInfoCall).toHaveBeenCalledTimes(1);
|
||||
expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token");
|
||||
expect(userGetInfoV2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle network timeout error", async () => {
|
||||
const timeoutError = new Error("Network timeout");
|
||||
|
||||
// Mock network timeout
|
||||
(userInfoCall as any).mockRejectedValue(timeoutError);
|
||||
(userGetInfoV2 as any).mockRejectedValue(timeoutError);
|
||||
|
||||
const { result } = renderHook(() => useCurrentUser(), { wrapper });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { UserInfo, userInfoCall } from "@/components/networking";
|
||||
import { UserInfoV2Response, userGetInfoV2 } from "@/components/networking";
|
||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { createQueryKeys } from "../common/queryKeysFactory";
|
||||
|
||||
const userKeys = createQueryKeys("users");
|
||||
|
||||
export const useCurrentUser = (): UseQueryResult<UserInfo> => {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
return useQuery<UserInfo>({
|
||||
export const useCurrentUser = (): UseQueryResult<UserInfoV2Response> => {
|
||||
const { accessToken, userId } = useAuthorized();
|
||||
return useQuery<UserInfoV2Response>({
|
||||
queryKey: userKeys.detail(userId!),
|
||||
queryFn: async () => {
|
||||
const data = await userInfoCall(accessToken!, userId!, userRole!, false, null, null);
|
||||
console.log(`userInfo: ${JSON.stringify(data)}`);
|
||||
return data.user_info;
|
||||
return await userGetInfoV2(accessToken!);
|
||||
},
|
||||
enabled: Boolean(accessToken && userId && userRole),
|
||||
enabled: Boolean(accessToken && userId),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1199,6 +1199,65 @@ export const userListCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Response type for /v2/user/info — lightweight endpoint that returns only the user object.
|
||||
*/
|
||||
export interface UserInfoV2Response {
|
||||
user_id: string;
|
||||
user_email: string | null;
|
||||
user_alias: string | null;
|
||||
user_role: string | null;
|
||||
spend: number;
|
||||
max_budget: number | null;
|
||||
models: string[];
|
||||
budget_duration: string | null;
|
||||
budget_reset_at: string | null;
|
||||
metadata: Record<string, any> | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
sso_user_id: string | null;
|
||||
teams: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight user info fetch from /v2/user/info.
|
||||
* Returns only the user object — no keys, no teams objects.
|
||||
*
|
||||
* @param accessToken - Bearer token for auth
|
||||
* @param userId - Optional user ID to look up. If omitted, returns the caller's own info.
|
||||
*/
|
||||
export const userGetInfoV2 = async (
|
||||
accessToken: string,
|
||||
userId?: string,
|
||||
): Promise<UserInfoV2Response> => {
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/user/info` : `/v2/user/info`;
|
||||
if (userId) {
|
||||
url += `?user_id=${encodeURIComponent(userId)}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user info v2:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const userInfoCall = async (
|
||||
accessToken: string,
|
||||
userID: string | null,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue