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:
Cursor Agent 2026-03-12 07:48:10 +00:00
parent 679b8fd52a
commit d03404d21e
3 changed files with 101 additions and 66 deletions

View file

@ -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 });

View file

@ -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),
});
};

View file

@ -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,