diff --git a/src/components/layout/use-nav-groups.ts b/src/components/layout/use-nav-groups.ts index ca9e3661..9dd1e1b9 100644 --- a/src/components/layout/use-nav-groups.ts +++ b/src/components/layout/use-nav-groups.ts @@ -6,12 +6,15 @@ import { getNavGroups, type NavGroup } from "@/config/menu"; * when the backend's can-approve answer (UpliftService tier-role rule, the * same authority behind uplift approve/reject) is true for either tier. * Loading and error states stay closed so the entry never flashes for a - * user the backend would refuse. + * user the backend would refuse, including when a refetch fails and the + * query still holds an earlier `true`. */ export function useNavGroups(): NavGroup[] { const tier1 = useUpliftsCanApprove(1); const tier2 = useUpliftsCanApprove(2); - const canReviewUplifts = tier1.data === true || tier2.data === true; + const allows = (query: { data?: boolean; isError?: boolean }) => + query.data === true && !query.isError; + const canReviewUplifts = allows(tier1) || allows(tier2); return getNavGroups({ canReviewUplifts }); } diff --git a/src/test/components/layout/app-sidebar-can-approve.test.tsx b/src/test/components/layout/app-sidebar-can-approve.test.tsx new file mode 100644 index 00000000..9db93295 --- /dev/null +++ b/src/test/components/layout/app-sidebar-can-approve.test.tsx @@ -0,0 +1,58 @@ +import { screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { AppSidebar } from "@/components/layout/app-sidebar"; +import { queryKeys } from "@/infra/query-key/query-key"; +import { createTestQueryClient, renderWithProviders } from "@/test/test-utils"; + +const canApproveApiMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/domain/uplifts/api/uplifts-api", () => ({ + upliftsApi: { canApprove: canApproveApiMock }, +})); + +describe("AppSidebar Uplift Approvals gating through the can-approve query", () => { + beforeEach(() => { + canApproveApiMock.mockReset(); + }); + + it("shows the entry when the API allows tier 2 and fails for tier 1", async () => { + canApproveApiMock.mockImplementation(async (tier: number) => { + if (tier === 1) throw new Error("503"); + return true; + }); + + renderWithProviders(, { route: "/", withAuth: false }); + + expect(await screen.findByRole("link", { name: "Uplift Approvals" })).toHaveAttribute( + "href", + "/uplifts", + ); + expect(canApproveApiMock).toHaveBeenCalledWith(1); + expect(canApproveApiMock).toHaveBeenCalledWith(2); + }); + + it("keeps the entry hidden once the API has denied both tiers", async () => { + canApproveApiMock.mockResolvedValue(false); + const queryClient = createTestQueryClient(); + + renderWithProviders(, { + route: "/", + withAuth: false, + queryClient, + }); + + await waitFor(() => { + for (const tier of [1, 2]) { + expect(queryClient.getQueryState(queryKeys.uplifts.canApprove(tier))?.status).toBe( + "success", + ); + } + }); + expect(screen.queryByRole("link", { name: "Uplift Approvals" })).toBeNull(); + expect(screen.getByRole("link", { name: "Services" })).toHaveAttribute( + "href", + "/settings/services", + ); + }); +}); diff --git a/src/test/components/layout/app-sidebar-nav.test.tsx b/src/test/components/layout/app-sidebar-nav.test.tsx index 6a1dc738..07e58155 100644 --- a/src/test/components/layout/app-sidebar-nav.test.tsx +++ b/src/test/components/layout/app-sidebar-nav.test.tsx @@ -76,6 +76,15 @@ describe("AppSidebar navigation entries", () => { expect(links["Uplift Approvals"]).toBeUndefined(); }); + it("hides Uplift Approvals when a refetch fails after an earlier approval", () => { + canApproveMock.mockReturnValue({ data: true, isError: true, error: new Error("403") }); + + renderSidebar("/"); + const links = sidebarLinks(); + + expect(links["Uplift Approvals"]).toBeUndefined(); + }); + it("offers Services to every user", () => { canApproveMock.mockReturnValue({ data: false });