fix(nav): hide Uplift Approvals when the can-approve refetch fails

React Query keeps the last successful data after a failed refetch, so an
earlier true kept the entry visible. Require a non-error query per tier,
and cover the real query path with a mocked API.
This commit is contained in:
Alexandre Brandizzi 2026-09-24 20:53:05 -03:00
parent aee2ef5164
commit 15e2565aca
3 changed files with 72 additions and 2 deletions

View file

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

View file

@ -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(<AppSidebar collapsed={false} />, { 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(<AppSidebar collapsed={false} />, {
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",
);
});
});

View file

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