mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
feat(ui): migrate access groups to the openapi-react-query client
Rebody the access-group hooks on $api so reads and mutations go through the typed openapi-fetch client instead of hand-rolled fetch helpers. This drops the local fetch wrappers, manual auth headers, manual error parsing, and the createQueryKeys factory; the response/request types now come from schema.d.ts. Public hook signatures are unchanged, so the page, modals, and selector that consume them are untouched. Reads rely on the global QueryCache.onError sink; mutations keep an explicit onError since there is no global mutation sink yet. Tests adopt the vi.hoisted fetch pattern already used by useUsers, asserting the outgoing request and the cache invalidation. Stale raw-fetch suppressions for these files are pruned.
This commit is contained in:
parent
ffa3880b80
commit
62d3b5edc2
8 changed files with 222 additions and 478 deletions
|
|
@ -4,31 +4,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/hooks/accessGroups/useEditAccessGroup.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/hooks/blogPosts/useBlogPosts.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -1,51 +1,25 @@
|
|||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { $api, authHeader } from "@/lib/http/api";
|
||||
import { all_admin_roles } from "@/utils/roles";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { AccessGroupResponse, accessGroupKeys } from "./useAccessGroups";
|
||||
|
||||
// ── Fetch function ───────────────────────────────────────────────────────────
|
||||
|
||||
const fetchAccessGroupDetails = async (accessToken: string, accessGroupId: string): Promise<AccessGroupResponse> => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
const url = `${baseUrl}/v1/access_group/${encodeURIComponent(accessGroupId)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `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 response.json();
|
||||
};
|
||||
|
||||
// ── Hook ─────────────────────────────────────────────────────────────────────
|
||||
import { AccessGroupResponse } from "./useAccessGroups";
|
||||
|
||||
export const useAccessGroupDetails = (accessGroupId?: string) => {
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery<AccessGroupResponse>({
|
||||
queryKey: accessGroupKeys.detail(accessGroupId!),
|
||||
queryFn: async () => fetchAccessGroupDetails(accessToken!, accessGroupId!),
|
||||
enabled: Boolean(accessToken && accessGroupId) && all_admin_roles.includes(userRole || ""),
|
||||
|
||||
// Seed from the list cache when available
|
||||
initialData: () => {
|
||||
if (!accessGroupId) return undefined;
|
||||
|
||||
const groups = queryClient.getQueryData<AccessGroupResponse[]>(accessGroupKeys.list({}));
|
||||
|
||||
return groups?.find((g) => g.access_group_id === accessGroupId);
|
||||
return $api.useQuery(
|
||||
"get",
|
||||
"/v1/access_group/{access_group_id}",
|
||||
{ params: { path: { access_group_id: accessGroupId ?? "" } }, headers: authHeader(accessToken!) },
|
||||
{
|
||||
enabled: Boolean(accessToken && accessGroupId) && all_admin_roles.includes(userRole || ""),
|
||||
initialData: () => {
|
||||
if (!accessGroupId) return undefined;
|
||||
const listKey = $api.queryOptions("get", "/v1/access_group", { headers: authHeader(accessToken!) }).queryKey;
|
||||
const groups = queryClient.getQueryData<AccessGroupResponse[]>(listKey);
|
||||
return groups?.find((g) => g.access_group_id === accessGroupId);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,242 +1,88 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import React from "react";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useAccessGroups, AccessGroupResponse } from "./useAccessGroups";
|
||||
import * as networking from "@/components/networking";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useAccessGroups } from "./useAccessGroups";
|
||||
import type { paths } from "@/lib/http/schema";
|
||||
|
||||
type AccessGroupListResponse = paths["/v1/access_group"]["get"]["responses"][200]["content"]["application/json"];
|
||||
|
||||
const { fetchMock } = vi.hoisted(() => {
|
||||
const fetchMock = vi.fn();
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
return { fetchMock };
|
||||
});
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: vi.fn(() => "http://proxy.example"),
|
||||
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
|
||||
deriveErrorMessage: vi.fn((data: unknown) => (data as { detail?: string })?.detail ?? "Unknown error"),
|
||||
handleError: vi.fn(),
|
||||
getProxyBaseUrl: () => "http://localhost:4000",
|
||||
getGlobalLitellmHeaderName: () => "Authorization",
|
||||
}));
|
||||
|
||||
const mockUseAuthorized = vi.fn();
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(() => ({
|
||||
accessToken: "test-token-123",
|
||||
userRole: "Admin",
|
||||
})),
|
||||
default: () => mockUseAuthorized(),
|
||||
}));
|
||||
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
const DEFAULT_AUTH = { accessToken: "test-access-token", userRole: "Admin" };
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = createQueryClient();
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
const requestOf = (arg: unknown): Request => arg as Request;
|
||||
const jsonResponse = (body: unknown, status = 200): Response =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
||||
|
||||
const mockAccessToken = "test-token-123";
|
||||
const mockAccessGroups: AccessGroupResponse[] = [
|
||||
const groups: AccessGroupListResponse = [
|
||||
{
|
||||
access_group_id: "ag-1",
|
||||
access_group_name: "Group One",
|
||||
description: "First group",
|
||||
description: null,
|
||||
access_model_names: [],
|
||||
access_mcp_server_ids: [],
|
||||
access_agent_ids: [],
|
||||
assigned_team_ids: [],
|
||||
assigned_key_ids: [],
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
created_by: "user-1",
|
||||
created_by: null,
|
||||
updated_at: "2025-01-01T00:00:00Z",
|
||||
updated_by: "user-1",
|
||||
updated_by: null,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
describe("useAccessGroups", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(networking.getProxyBaseUrl).mockReturnValue("http://proxy.example");
|
||||
vi.mocked(networking.getGlobalLitellmHeaderName).mockReturnValue("Authorization");
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: "Admin",
|
||||
} as any);
|
||||
|
||||
global.fetch = fetchMock;
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
fetchMock.mockReset();
|
||||
mockUseAuthorized.mockReset();
|
||||
mockUseAuthorized.mockReturnValue(DEFAULT_AUTH);
|
||||
});
|
||||
|
||||
it("should return hook result without errors", () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
|
||||
it("fetches GET /v1/access_group with the bearer header and returns typed data", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse(groups));
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
const request = requestOf(fetchMock.mock.calls[0][0]);
|
||||
expect(new URL(request.url).pathname).toBe("/v1/access_group");
|
||||
expect(request.method).toBe("GET");
|
||||
expect(request.headers.get("Authorization")).toBe("Bearer test-access-token");
|
||||
expect(result.current.data?.[0].access_group_id).toBe("ag-1");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["null token", { accessToken: null }],
|
||||
["non-admin role", { userRole: "Internal User" }],
|
||||
])("does not fire a request when gated by %s", (_label, override) => {
|
||||
fetchMock.mockResolvedValue(jsonResponse(groups));
|
||||
mockUseAuthorized.mockReturnValue({ ...DEFAULT_AUTH, ...override });
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(result.current).toHaveProperty("data");
|
||||
expect(result.current).toHaveProperty("isSuccess");
|
||||
expect(result.current).toHaveProperty("isError");
|
||||
expect(result.current).toHaveProperty("status");
|
||||
});
|
||||
|
||||
it("should return access groups when access token and admin role are present", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockAccessGroups),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://proxy.example/v1/access_group",
|
||||
expect.objectContaining({
|
||||
method: "GET",
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockAccessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.current.data).toEqual(mockAccessGroups);
|
||||
});
|
||||
|
||||
it("should not fetch when access token is null", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: null,
|
||||
userRole: "Admin",
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(result.current.isFetched).toBe(false);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not fetch when access token is empty string", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: "",
|
||||
userRole: "Admin",
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not fetch when user role is not an admin role", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: "Viewer",
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not fetch when user role is null", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: null,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fetch when user role is proxy_admin", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: "proxy_admin",
|
||||
} as any);
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockAccessGroups),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockAccessGroups);
|
||||
});
|
||||
|
||||
it("should expose error state when fetch fails", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ detail: "Forbidden" }),
|
||||
} as Response);
|
||||
vi.mocked(networking.deriveErrorMessage).mockReturnValue("Forbidden");
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect((result.current.error as Error).message).toBe("Forbidden");
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(networking.handleError).toHaveBeenCalledWith("Forbidden");
|
||||
});
|
||||
|
||||
it("should return empty array when API returns empty list", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it("should propagate network errors", async () => {
|
||||
const networkError = new Error("Network failure");
|
||||
fetchMock.mockRejectedValue(networkError);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(networkError);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,62 +1,17 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createQueryKeys } from "../common/queryKeysFactory";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking";
|
||||
import type { components } from "@/lib/http/schema";
|
||||
import { $api, authHeader } from "@/lib/http/api";
|
||||
import { all_admin_roles } from "@/utils/roles";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AccessGroupResponse {
|
||||
access_group_id: string;
|
||||
access_group_name: string;
|
||||
description: string | null;
|
||||
access_model_names: string[];
|
||||
access_mcp_server_ids: string[];
|
||||
access_agent_ids: string[];
|
||||
assigned_team_ids: string[];
|
||||
assigned_key_ids: string[];
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
updated_at: string;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
// ── Query keys (shared across access-group hooks) ────────────────────────────
|
||||
|
||||
export const accessGroupKeys = createQueryKeys("accessGroups");
|
||||
|
||||
// ── Fetch function ───────────────────────────────────────────────────────────
|
||||
|
||||
const fetchAccessGroups = async (accessToken: string): Promise<AccessGroupResponse[]> => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
const url = `${baseUrl}/v1/access_group`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `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 response.json();
|
||||
};
|
||||
|
||||
// ── Hook ─────────────────────────────────────────────────────────────────────
|
||||
export type AccessGroupResponse = components["schemas"]["AccessGroupResponse"];
|
||||
|
||||
export const useAccessGroups = () => {
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
|
||||
return useQuery<AccessGroupResponse[]>({
|
||||
queryKey: accessGroupKeys.list({}),
|
||||
queryFn: async () => fetchAccessGroups(accessToken!),
|
||||
enabled: Boolean(accessToken) && all_admin_roles.includes(userRole || ""),
|
||||
});
|
||||
return $api.useQuery(
|
||||
"get",
|
||||
"/v1/access_group",
|
||||
{ headers: authHeader(accessToken!) },
|
||||
{ enabled: Boolean(accessToken) && all_admin_roles.includes(userRole || "") },
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useCreateAccessGroup } from "./useCreateAccessGroup";
|
||||
|
||||
const { fetchMock } = vi.hoisted(() => {
|
||||
const fetchMock = vi.fn();
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
return { fetchMock };
|
||||
});
|
||||
|
||||
const handleError = vi.fn();
|
||||
const deriveErrorMessage = vi.fn(() => "derived message");
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: () => "http://localhost:4000",
|
||||
getGlobalLitellmHeaderName: () => "Authorization",
|
||||
handleError: (...args: unknown[]) => handleError(...args),
|
||||
deriveErrorMessage: (...args: unknown[]) => deriveErrorMessage(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: () => ({ accessToken: "test-access-token" }),
|
||||
}));
|
||||
|
||||
const requestOf = (arg: unknown): Request => arg as Request;
|
||||
const jsonResponse = (body: unknown, status = 200): Response =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
||||
|
||||
const created = { access_group_id: "ag-new", access_group_name: "New Group" };
|
||||
|
||||
describe("useCreateAccessGroup", () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
|
||||
fetchMock.mockReset();
|
||||
handleError.mockReset();
|
||||
deriveErrorMessage.mockClear();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
|
||||
it("POSTs the body to /v1/access_group with the bearer header", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse(created, 201));
|
||||
const { result } = renderHook(() => useCreateAccessGroup(), { wrapper });
|
||||
|
||||
result.current.mutate({ access_group_name: "New Group", access_model_names: ["gpt-4o"] });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
const request = requestOf(fetchMock.mock.calls[0][0]);
|
||||
expect(new URL(request.url).pathname).toBe("/v1/access_group");
|
||||
expect(request.method).toBe("POST");
|
||||
expect(request.headers.get("Authorization")).toBe("Bearer test-access-token");
|
||||
expect(await request.json()).toEqual({ access_group_name: "New Group", access_model_names: ["gpt-4o"] });
|
||||
});
|
||||
|
||||
it("invalidates the access-group list on success", async () => {
|
||||
const invalidate = vi.spyOn(queryClient, "invalidateQueries");
|
||||
fetchMock.mockResolvedValue(jsonResponse(created, 201));
|
||||
const { result } = renderHook(() => useCreateAccessGroup(), { wrapper });
|
||||
|
||||
result.current.mutate({ access_group_name: "New Group" });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(invalidate).toHaveBeenCalledWith({ queryKey: ["get", "/v1/access_group"] });
|
||||
});
|
||||
|
||||
it("routes a failed create through the global error handler", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ detail: "boom" }, 400));
|
||||
const { result } = renderHook(() => useCreateAccessGroup(), { wrapper });
|
||||
|
||||
result.current.mutate({ access_group_name: "New Group" });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
expect(deriveErrorMessage).toHaveBeenCalled();
|
||||
expect(handleError).toHaveBeenCalledWith("derived message");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,63 +1,25 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { components } from "@/lib/http/schema";
|
||||
import { $api, authHeader } from "@/lib/http/api";
|
||||
import { handleError, deriveErrorMessage } from "@/components/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { AccessGroupResponse, accessGroupKeys } from "./useAccessGroups";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AccessGroupCreateParams {
|
||||
access_group_name: string;
|
||||
description?: string | null;
|
||||
access_model_names?: string[];
|
||||
access_mcp_server_ids?: string[];
|
||||
access_agent_ids?: string[];
|
||||
assigned_team_ids?: string[];
|
||||
assigned_key_ids?: string[];
|
||||
}
|
||||
|
||||
// ── Fetch function ───────────────────────────────────────────────────────────
|
||||
|
||||
const createAccessGroup = async (
|
||||
accessToken: string,
|
||||
params: AccessGroupCreateParams,
|
||||
): Promise<AccessGroupResponse> => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
const url = `${baseUrl}/v1/access_group`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// ── Hook ─────────────────────────────────────────────────────────────────────
|
||||
export type AccessGroupCreateParams = components["schemas"]["AccessGroupCreateRequest"];
|
||||
|
||||
export const useCreateAccessGroup = () => {
|
||||
const { accessToken } = useAuthorized();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<AccessGroupResponse, Error, AccessGroupCreateParams>({
|
||||
mutationFn: async (params) => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return createAccessGroup(accessToken, params);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: accessGroupKeys.all });
|
||||
},
|
||||
const mutation = $api.useMutation("post", "/v1/access_group", {
|
||||
onError: (error) => handleError(deriveErrorMessage(error)),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group"] }),
|
||||
});
|
||||
|
||||
return {
|
||||
...mutation,
|
||||
mutate: (body: AccessGroupCreateParams, options?: Parameters<typeof mutation.mutate>[1]) =>
|
||||
mutation.mutate({ body, headers: authHeader(accessToken!) }, options),
|
||||
mutateAsync: (body: AccessGroupCreateParams, options?: Parameters<typeof mutation.mutateAsync>[1]) =>
|
||||
mutation.mutateAsync({ body, headers: authHeader(accessToken!) }, options),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,47 +1,28 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { $api, authHeader } from "@/lib/http/api";
|
||||
import { handleError, deriveErrorMessage } from "@/components/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { accessGroupKeys } from "./useAccessGroups";
|
||||
|
||||
// ── Fetch function ───────────────────────────────────────────────────────────
|
||||
|
||||
const deleteAccessGroup = async (accessToken: string, accessGroupId: string): Promise<void> => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
const url = `${baseUrl}/v1/access_group/${encodeURIComponent(accessGroupId)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 204 No Content — nothing to parse
|
||||
};
|
||||
|
||||
// ── Hook ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const useDeleteAccessGroup = () => {
|
||||
const { accessToken } = useAuthorized();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, Error, string>({
|
||||
mutationFn: async (accessGroupId) => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return deleteAccessGroup(accessToken, accessGroupId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: accessGroupKeys.all });
|
||||
},
|
||||
const mutation = $api.useMutation("delete", "/v1/access_group/{access_group_id}", {
|
||||
onError: (error) => handleError(deriveErrorMessage(error)),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group"] }),
|
||||
});
|
||||
|
||||
return {
|
||||
...mutation,
|
||||
mutate: (accessGroupId: string, options?: Parameters<typeof mutation.mutate>[1]) =>
|
||||
mutation.mutate(
|
||||
{ params: { path: { access_group_id: accessGroupId } }, headers: authHeader(accessToken!) },
|
||||
options,
|
||||
),
|
||||
mutateAsync: (accessGroupId: string, options?: Parameters<typeof mutation.mutateAsync>[1]) =>
|
||||
mutation.mutateAsync(
|
||||
{ params: { path: { access_group_id: accessGroupId } }, headers: authHeader(accessToken!) },
|
||||
options,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,72 +1,42 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { components } from "@/lib/http/schema";
|
||||
import { $api, authHeader } from "@/lib/http/api";
|
||||
import { handleError, deriveErrorMessage } from "@/components/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { AccessGroupResponse, accessGroupKeys } from "./useAccessGroups";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AccessGroupUpdateParams {
|
||||
access_group_name?: string;
|
||||
description?: string | null;
|
||||
access_model_names?: string[];
|
||||
access_mcp_server_ids?: string[];
|
||||
access_agent_ids?: string[];
|
||||
assigned_team_ids?: string[];
|
||||
assigned_key_ids?: string[];
|
||||
}
|
||||
export type AccessGroupUpdateParams = components["schemas"]["AccessGroupUpdateRequest"];
|
||||
|
||||
export interface EditAccessGroupVariables {
|
||||
accessGroupId: string;
|
||||
params: AccessGroupUpdateParams;
|
||||
}
|
||||
|
||||
// ── Fetch function ───────────────────────────────────────────────────────────
|
||||
|
||||
const updateAccessGroup = async (
|
||||
accessToken: string,
|
||||
accessGroupId: string,
|
||||
params: AccessGroupUpdateParams,
|
||||
): Promise<AccessGroupResponse> => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
const url = `${baseUrl}/v1/access_group/${encodeURIComponent(accessGroupId)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// ── Hook ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const useEditAccessGroup = () => {
|
||||
const { accessToken } = useAuthorized();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<AccessGroupResponse, Error, EditAccessGroupVariables>({
|
||||
mutationFn: async ({ accessGroupId, params }) => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return updateAccessGroup(accessToken, accessGroupId, params);
|
||||
},
|
||||
onSuccess: (_data, { accessGroupId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: accessGroupKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accessGroupKeys.detail(accessGroupId),
|
||||
});
|
||||
const mutation = $api.useMutation("put", "/v1/access_group/{access_group_id}", {
|
||||
onError: (error) => handleError(deriveErrorMessage(error)),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group/{access_group_id}"] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...mutation,
|
||||
mutate: ({ accessGroupId, params }: EditAccessGroupVariables, options?: Parameters<typeof mutation.mutate>[1]) =>
|
||||
mutation.mutate(
|
||||
{ params: { path: { access_group_id: accessGroupId } }, body: params, headers: authHeader(accessToken!) },
|
||||
options,
|
||||
),
|
||||
mutateAsync: (
|
||||
{ accessGroupId, params }: EditAccessGroupVariables,
|
||||
options?: Parameters<typeof mutation.mutateAsync>[1],
|
||||
) =>
|
||||
mutation.mutateAsync(
|
||||
{ params: { path: { access_group_id: accessGroupId } }, body: params, headers: authHeader(accessToken!) },
|
||||
options,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue