// @vitest-environment jsdom import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ApiError } from "@/api/client"; import { CompanyProvider, useCompany } from "@/context/CompanyContext"; import { queryKeys } from "@/lib/queryKeys"; import { InstanceAccess } from "./InstanceAccess"; const mocks = vi.hoisted(() => ({ list: vi.fn(), directory: vi.fn(), detachInflightList: vi.fn(), detachInflightDirectory: vi.fn(), getSession: vi.fn(), searchAdminUsers: vi.fn(), getUserCompanyAccess: vi.fn(), setUserCompanyAccess: vi.fn(), setBreadcrumbs: vi.fn(), pushToast: vi.fn(), })); vi.mock("@/api/companies", () => ({ companiesApi: mocks })); vi.mock("@/api/auth", () => ({ authApi: mocks })); vi.mock("@/api/access", () => ({ accessApi: mocks })); vi.mock("@/context/BreadcrumbContext", () => ({ useBreadcrumbs: () => mocks })); vi.mock("@/context/ToastContext", () => ({ useToast: () => mocks })); const companyA = { id: "company-a", name: "Company A", issuePrefix: "CPA", status: "active" }; const companyB = { id: "company-b", name: "Company B", issuePrefix: "CPB", status: "active" }; const user = { id: "admin", name: "Admin", email: "admin@example.com", isInstanceAdmin: true }; const membershipA = { id: "membership-a", companyId: companyA.id, companyName: companyA.name, status: "active", membershipRole: "owner", updatedAt: "2020-01-01T00:00:00Z", }; let container: HTMLDivElement; let root: Root; let client: QueryClient; function NavigationProbe() { const { companies } = useCompany(); return ; } async function renderPage() { await act(async () => { root.render( , ); }); } async function eventually(assertion: () => void) { await vi.waitFor(async () => { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); assertion(); }); } function button(text: string) { return [...container.querySelectorAll("button")].find((element) => element.textContent === text); } beforeEach(() => { vi.resetAllMocks(); Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); localStorage.clear(); mocks.getSession.mockResolvedValue({ session: { id: "session-admin", userId: user.id }, user }); mocks.list.mockResolvedValue([companyA]); mocks.directory.mockResolvedValue([companyA, companyB]); mocks.searchAdminUsers.mockResolvedValue([user]); mocks.getUserCompanyAccess.mockResolvedValue({ user, companyAccess: [membershipA] }); mocks.setUserCompanyAccess.mockResolvedValue({}); client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); }); describe("InstanceAccess company directory", () => { it("lets admins grant access outside their navigation list and refreshes their own navigation", async () => { await renderPage(); await eventually(() => { expect(button("Save organization access")).toBeDefined(); expect(container.querySelector("nav")?.textContent).toBe("Company A"); }); const otherCompany = [...container.querySelectorAll("label")].find((label) => label.textContent?.includes("Company B")); const checkbox = otherCompany?.querySelector('[role="checkbox"]'); expect(checkbox?.getAttribute("aria-checked")).toBe("false"); mocks.setUserCompanyAccess.mockImplementation(async () => { mocks.list.mockResolvedValue([companyA, companyB]); mocks.getUserCompanyAccess.mockResolvedValue({ user, companyAccess: [membershipA, { ...membershipA, id: "membership-b", companyId: companyB.id, companyName: companyB.name, }] }); return {}; }); await act(async () => checkbox!.click()); await act(async () => button("Save organization access")!.click()); await eventually(() => { expect(mocks.setUserCompanyAccess).toHaveBeenCalledWith(user.id, [companyA.id, companyB.id]); expect(container.querySelector("nav")?.textContent).toBe("Company A, Company B"); }); }); it("prevents editing an incomplete directory and lets the admin retry", async () => { mocks.directory.mockRejectedValue(new Error("Unavailable")); await renderPage(); await eventually(() => { expect(container.textContent).toContain("Couldn't load organizations"); expect(container.querySelector("nav")?.textContent).toBe("Company A"); }); expect(button("Save organization access")).toBeUndefined(); expect(mocks.setUserCompanyAccess).not.toHaveBeenCalled(); mocks.directory.mockResolvedValue([companyA, companyB]); await act(async () => button("Retry")!.click()); await eventually(() => expect(button("Save organization access")).toBeDefined()); }); it("does not request the directory when instance administration is forbidden", async () => { mocks.searchAdminUsers.mockRejectedValue(new ApiError("Forbidden", 403, {})); await renderPage(); await eventually(() => expect(container.textContent).toContain("Instance admin access is required")); expect(container.querySelector('[data-query-view="error"]')).toBeNull(); expect(mocks.directory).not.toHaveBeenCalled(); }); }); describe("InstanceAccess read resilience", () => { it("keeps the loaded user list when a refetch fails transiently", async () => { await renderPage(); await eventually(() => expect(button("Save organization access")).toBeDefined()); expect(container.textContent).toContain("admin@example.com"); mocks.searchAdminUsers.mockRejectedValue( new ApiError("Paperclip is restarting.", 503, { error: "tenant_app_unavailable" }), ); await act(async () => { await client.invalidateQueries({ queryKey: queryKeys.access.adminUsers("") }); }); await eventually(() => expect(mocks.searchAdminUsers).toHaveBeenCalledTimes(2)); expect(container.textContent).toContain("admin@example.com"); expect(button("Save organization access")).toBeDefined(); expect(container.querySelector('[data-query-view="error"]')).toBeNull(); expect(container.textContent).not.toContain("Paperclip is restarting."); expect(container.textContent).not.toContain("tenant_app_unavailable"); }); it("shows readable copy and a Retry button when users fail to load", async () => { mocks.searchAdminUsers.mockRejectedValueOnce(new ApiError("Boom", 500, { error: "Boom" })); await renderPage(); await eventually(() => expect(container.textContent).toContain("Couldn't load users")); expect(container.querySelector('[data-query-view="error"]')).not.toBeNull(); expect(container.textContent).not.toContain("Instance admin access is required"); await act(async () => button("Retry")!.click()); await eventually(() => expect(button("Save organization access")).toBeDefined()); expect(container.querySelector('[data-query-view="error"]')).toBeNull(); }); });