fix(ui): stop useCan from firing /organization/list on every page

useCan wraps useIsOrgAdmin which unconditionally called useOrganizations, so
every gated page hit /organization/list even for capabilities that only look
at the session role. That broke three page integration tests that assert
fetch is not called for a non-admin, and it broke three cost-optimization
tests where the extra useQuery threw because they rendered without a
QueryClientProvider.

Skip the fetch when the capability isn't in ORG_ADMIN_CAPABILITIES, and wrap
the three cost-optimization test files with the shared providers helper so
the still-necessary useQuery has a client.

Co-authored-by: Krrish Dholakia <krrish-berri-2@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-11 04:38:22 +00:00
parent 6f36bee6ba
commit f8f40c4284
No known key found for this signature in database
7 changed files with 47 additions and 13 deletions

View file

@ -1,5 +1,13 @@
import { fireEvent, render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { fireEvent, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils";
const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: useAuthorizedMock,
}));
const mockUserDailyActivityCall = vi.fn();
@ -36,10 +44,15 @@ const singlePage = {
};
describe("CostOptimizationView daily activity", () => {
beforeEach(() => {
testQueryClient.clear();
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "proxy_admin" });
});
it("fetches daily activity once for the page and shares it with every tab that needs it", async () => {
mockUserDailyActivityCall.mockResolvedValue(singlePage);
const { getByRole, getByTestId } = render(
const { getByRole, getByTestId } = renderWithProviders(
<CostOptimizationView accessToken="test-token" userId="u1" userRole="proxy_admin" />,
);

View file

@ -1,6 +1,8 @@
import { fireEvent, render } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils";
const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
@ -19,11 +21,12 @@ import CostOptimizationView from "./CostOptimizationView";
const renderView = (userRole = "Admin") => {
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole });
return render(<CostOptimizationView accessToken="test-token" userId="u1" userRole={userRole} />);
return renderWithProviders(<CostOptimizationView accessToken="test-token" userId="u1" userRole={userRole} />);
};
describe("CostOptimizationView", () => {
beforeEach(() => {
testQueryClient.clear();
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "Admin" });
});

View file

@ -1,10 +1,11 @@
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ToolSpendResponse } from "@/components/networking";
import type { DailyData, SpendMetrics } from "@/components/UsagePage/types";
import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils";
const mockGetToolSpend = vi.fn();
const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() }));
@ -106,7 +107,7 @@ const renderWith = (results: DailyData[], options: RenderOptions = {}) => {
} = options;
mockGetToolSpend.mockResolvedValue(toolSpend);
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole });
return render(
return renderWithProviders(
<UsageTab
accessToken="test-token"
activity={{
@ -124,6 +125,7 @@ const readSeries = (element: HTMLElement) => JSON.parse(element.getAttribute("da
describe("UsageTab", () => {
beforeEach(() => {
testQueryClient.clear();
mockGetToolSpend.mockReset();
});

View file

@ -10,7 +10,14 @@ export interface OrganizationListFilters {
org_alias?: string | null;
}
export const useOrganizations = (filters?: OrganizationListFilters): UseQueryResult<Organization[]> => {
export interface UseOrganizationsOptions {
enabled?: boolean;
}
export const useOrganizations = (
filters?: OrganizationListFilters,
options?: UseOrganizationsOptions,
): UseQueryResult<Organization[]> => {
const { accessToken, userId, userRole } = useAuthorized();
const orgId = filters?.org_id || null;
const orgAlias = filters?.org_alias || null;
@ -21,7 +28,7 @@ export const useOrganizations = (filters?: OrganizationListFilters): UseQueryRes
: {},
),
queryFn: async () => await organizationListCall(accessToken!, orgId, orgAlias),
enabled: Boolean(accessToken && userId && userRole),
enabled: (options?.enabled ?? true) && Boolean(accessToken && userId && userRole),
});
};

View file

@ -1,13 +1,13 @@
"use client";
import { hasCapability, type Capability } from "@/utils/capabilities";
import { capabilityRequiresOrgAdmin, hasCapability, type Capability } from "@/utils/capabilities";
import useAuthorized from "./useAuthorized";
import useIsOrgAdmin from "./useIsOrgAdmin";
const useCan = (capability: Capability): boolean => {
const { userRole } = useAuthorized();
const isOrgAdmin = useIsOrgAdmin();
const isOrgAdmin = useIsOrgAdmin({ enabled: capabilityRequiresOrgAdmin(capability) });
return hasCapability(userRole, capability, isOrgAdmin);
};

View file

@ -5,9 +5,16 @@ import { isOrgAdminForAnyOrg, isOrgAdminSessionRole } from "@/utils/roles";
import { useOrganizations } from "./organizations/useOrganizations";
import useAuthorized from "./useAuthorized";
const useIsOrgAdmin = (): boolean => {
export interface UseIsOrgAdminOptions {
enabled?: boolean;
}
const useIsOrgAdmin = (options?: UseIsOrgAdminOptions): boolean => {
const { userId, userRole } = useAuthorized();
const { data: organizations } = useOrganizations();
const { data: organizations } = useOrganizations(undefined, options);
if (options?.enabled === false) {
return isOrgAdminSessionRole(userRole);
}
return isOrgAdminSessionRole(userRole) || isOrgAdminForAnyOrg(organizations, userId);
};

View file

@ -21,6 +21,8 @@ export type Capability = keyof typeof CAPABILITY_ROLES;
const ORG_ADMIN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>(["viewDeletedTeams"]);
export const capabilityRequiresOrgAdmin = (capability: Capability): boolean => ORG_ADMIN_CAPABILITIES.has(capability);
export const hasCapability = (
userRole: string | null | undefined,
capability: Capability,