// @vitest-environment jsdom import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, waitFor, within, fireEvent, cleanup } from "@testing-library/react"; import React from "react"; import { MemoryRouter } from "react-router-dom"; // ── Shared mock state (vi.hoisted avoids TDZ with vi.mock hoisting) ──────────── const H = vi.hoisted(() => ({ mockNavigate: vi.fn(), mockListNetworks: vi.fn(), mockListAvailableZtNetworks: vi.fn(), mockToast: vi.fn(), state: { orgId: "org-test-123" as string | null, }, navigateCalls: [] as string[], })); vi.mock("react-router-dom", async () => { const actual = await vi.importActual("react-router-dom"); return { ...actual, useNavigate: () => { const fn = (path: string) => { H.navigateCalls.push(path); H.mockNavigate(path); }; return fn; }, useParams: () => ({ orgId: H.state.orgId }), }; }); vi.mock("@/hooks/useCurrentOrganization", () => ({ useCurrentOrganizationId: () => ({ orgId: H.state.orgId, isLoading: false, }), useCurrentOrganization: () => ({ org: { id: H.state.orgId, name: "Test Org", slug: "test-org", description: null, logo_url: null, is_active: true, role: "admin", created_at: "2024-01-01", updated_at: "2024-01-01", }, isLoading: false, }), })); vi.mock("@/hooks/use-toast", () => ({ useToast: () => ({ toast: H.mockToast, dismiss: () => {}, toasts: [], }), })); vi.mock("@/lib/api", () => ({ api: { zerotier: { listNetworks: H.mockListNetworks, listAvailableZtNetworks: H.mockListAvailableZtNetworks, createNetwork: vi.fn(), updateNetwork: vi.fn(), deleteNetwork: vi.fn(), }, }, ApiError: class ApiError extends Error { code: number; type: string; details: Record; constructor(message: string, code: number, type: string, details: Record = {}) { super(message); this.name = "ApiError"; this.code = code; this.type = type; this.details = details; } }, })); import NetworksPage from "../src/pages/org/NetworksPage"; // ── Test data ────────────────────────────────────────────────────────────────── const MOCK_NETWORKS = [ { id: "net-001", organization_id: "org-test-123", name: "Production VPN", description: "Main production network", owner_user_id: "user-1", zerotier_network_id: "d6578dd03c894448", environment: "production" as const, request_mode: "approval_required" as const, default_activation_lifetime_minutes: 480, max_activation_lifetime_minutes: null, is_active: true, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", deleted_at: null, approved_user_count: 25, active_membership_count: 12, }, { id: "net-002", organization_id: "org-test-123", name: "Dev Network", description: "Development and staging", owner_user_id: "user-1", zerotier_network_id: "abcdef1234567890", environment: "development" as const, request_mode: "open" as const, default_activation_lifetime_minutes: 240, max_activation_lifetime_minutes: 1440, is_active: false, created_at: "2024-01-02T00:00:00Z", updated_at: "2024-01-02T00:00:00Z", deleted_at: null, approved_user_count: 5, active_membership_count: 0, }, ]; const MOCK_ZT_NETWORKS = [ { id: "zt-net-001", name: "External ZeroTier", description: "An external ZT network", owner_id: null, online_member_count: 3, authorized_member_count: 10, total_member_count: 10, already_managed: false, portal_network_id: null, portal_network_name: null, }, ]; // ── Helpers ──────────────────────────────────────────────────────────────────── function renderPage() { return render( , ); } // Default: all API calls return never-resolving promise (loading state) // Individual tests override BEFORE calling renderPage(). beforeEach(() => { vi.clearAllMocks(); H.navigateCalls.length = 0; H.mockListNetworks.mockImplementation(() => new Promise(() => {})); H.mockListAvailableZtNetworks.mockImplementation(() => new Promise(() => {})); }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); // ═══════════════════════════════════════════════════════════════════════════════ // HAPPY PATH: Data Loading // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — Data Loading", () => { test("renders loading state while fetching networks", () => { renderPage(); expect(screen.getByText("Loading networks…")).toBeDefined(); }); test("renders network data when API resolves", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); expect(screen.getByText("Dev Network")).toBeDefined(); expect(screen.getByText("d6578dd03c894448")).toBeDefined(); expect(screen.getByText("abcdef1234567890")).toBeDefined(); }); test("renders error state when API fails", async () => { H.mockListNetworks.mockRejectedValue(new Error("Network error")); renderPage(); await waitFor(() => { expect( screen.getByText("Failed to load networks. Please try again."), ).toBeDefined(); }); }); test("renders empty state when no networks exist", async () => { H.mockListNetworks.mockResolvedValue({ networks: [], count: 0 }); renderPage(); await waitFor(() => { expect( screen.getByText("No networks configured yet. Add one to get started."), ).toBeDefined(); }); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // NAVIGATION: Row Click // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — Row Click Navigation", () => { test("clicking a network row navigates to /org/zerotier/networks/{networkId}", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); const productionRow = screen.getByText("Production VPN").closest("button"); expect(productionRow).not.toBeNull(); fireEvent.click(productionRow!); expect(H.mockNavigate).toHaveBeenCalledTimes(1); expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-001"); expect(H.navigateCalls).toEqual(["/org/zerotier/networks/net-001"]); }); test("clicking second network row navigates to its URL", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Dev Network")).toBeDefined(); }); const devRow = screen.getByText("Dev Network").closest("button"); expect(devRow).not.toBeNull(); fireEvent.click(devRow!); expect(H.mockNavigate).toHaveBeenCalledTimes(1); expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-002"); }); test("navigate NOT called before any click", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); expect(H.mockNavigate).not.toHaveBeenCalled(); expect(H.navigateCalls).toEqual([]); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // NAVIGATION: Dropdown "View details" // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — Dropdown View details Navigation", () => { test('"View details" dropdown item navigates to /org/zerotier/networks/{networkId}', async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); // The MoreHorizontal button is a CHILD of the row button (nested inside it). // Find it by looking for the button with the MoreHorizontal icon inside the row. const productionRow = screen.getByText("Production VPN").closest("button")!; // Find ALL nested buttons within the row const nestedButtons = productionRow.querySelectorAll("button"); // The first nested button should be the MoreHorizontal dropdown trigger expect(nestedButtons.length).toBeGreaterThan(0); // Radix DropdownMenu opens on pointerdown fireEvent.pointerDown(nestedButtons[0]); // DropdownMenuContent renders in a portal, screen.getByText searches the whole document await waitFor(() => { expect(screen.getByText("View details")).toBeDefined(); }); fireEvent.click(screen.getByText("View details")); expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-001"); }); test('"View details" for second network navigates to its URL', async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Dev Network")).toBeDefined(); }); const devRow = screen.getByText("Dev Network").closest("button")!; const nestedButtons = devRow.querySelectorAll("button"); expect(nestedButtons.length).toBeGreaterThan(0); fireEvent.pointerDown(nestedButtons[0]); await waitFor(() => { expect(screen.getByText("View details")).toBeDefined(); }); fireEvent.click(screen.getByText("View details")); expect(H.mockNavigate).toHaveBeenCalledWith("/org/zerotier/networks/net-002"); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // CARD DESCRIPTION TEXT // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — Card Description", () => { test("CardDescription reflects page navigation (not old drawer text)", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); const description = screen.getByText( "Click a network to manage members, devices, and access requests", ); expect(description).toBeDefined(); }); test("old drawer-related text is absent", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); // The old Sheet content (Network Details, member list) should NOT be present expect(screen.queryByText("Network Details")).toBeNull(); expect(screen.queryByText("Members")).toBeNull(); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // ZERO TIER NETWORK PICKER SHEET // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — ZeroTier Picker Sheet", () => { test('"Import from ZeroTier" button is present', async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); const importButton = screen.getByRole("button", { name: /import from zerotier/i, }); expect(importButton).toBeDefined(); }); test('clicking "Import from ZeroTier" opens the ZT Picker Sheet', async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); H.mockListAvailableZtNetworks.mockResolvedValue({ networks: MOCK_ZT_NETWORKS, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); const importButton = screen.getByRole("button", { name: /import from zerotier/i, }); fireEvent.click(importButton); // Wait for the Sheet to render with its content await waitFor(() => { expect(screen.getByText("External ZeroTier")).toBeDefined(); }); expect(screen.getByText("zt-net-001")).toBeDefined(); expect(screen.getByText("Import")).toBeDefined(); }); test("ZT Picker calls API with correct orgId", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); H.mockListAvailableZtNetworks.mockResolvedValue({ networks: MOCK_ZT_NETWORKS, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); const importButton = screen.getByRole("button", { name: /import from zerotier/i, }); fireEvent.click(importButton); await waitFor(() => { expect(screen.getByText("External ZeroTier")).toBeDefined(); }); expect(H.mockListAvailableZtNetworks).toHaveBeenCalledTimes(1); expect(H.mockListAvailableZtNetworks).toHaveBeenCalledWith("org-test-123"); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // DATA DISPLAY // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — Data Display", () => { test("displays network count badge", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); // The CardTitle contains "Portal Networks" and the count badge expect(screen.getByText("Portal Networks")).toBeDefined(); // Badge with count "2" should be present expect(screen.getByText("2")).toBeDefined(); }); test("displays approved user counts", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); expect(screen.getByText("25")).toBeDefined(); expect(screen.getByText("5")).toBeDefined(); }); test("displays active device counts", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); expect(screen.getByText("12")).toBeDefined(); expect(screen.getByText("0")).toBeDefined(); }); test("displays environment badges", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); expect(screen.getByText("Production")).toBeDefined(); expect(screen.getByText("Development")).toBeDefined(); }); test("displays request mode badges", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); expect(screen.getByText("Approval Required")).toBeDefined(); expect(screen.getByText("Open")).toBeDefined(); }); test('displays "Inactive" badge for inactive networks', async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Dev Network")).toBeDefined(); }); expect(screen.getByText("Inactive")).toBeDefined(); }); test("renders search-empty state when filter matches nothing", async () => { H.mockListNetworks.mockResolvedValue({ networks: MOCK_NETWORKS, count: MOCK_NETWORKS.length, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); const searchInput = screen.getByPlaceholderText("Search networks…"); fireEvent.change(searchInput, { target: { value: "zzzz_nonexistent" } }); await waitFor(() => { expect( screen.getByText("No networks match your search."), ).toBeDefined(); }); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // EDGE CASES / ADVERSARIAL INPUTS // ═══════════════════════════════════════════════════════════════════════════════ describe("NetworksPage — Adversarial Inputs", () => { test("handles XSS-like network name as text", async () => { H.mockListNetworks.mockResolvedValue({ networks: [ { ...MOCK_NETWORKS[0], name: 'VPN ', description: "desc ${injection}", zerotier_network_id: "../../etc/passwd", }, ], count: 1, }); renderPage(); await waitFor(() => { expect( screen.getByText('VPN '), ).toBeDefined(); }); }); test("handles very long network name", async () => { const longName = "A".repeat(500); H.mockListNetworks.mockResolvedValue({ networks: [{ ...MOCK_NETWORKS[0], name: longName, id: "net-long" }], count: 1, }); renderPage(); await waitFor(() => { expect(screen.getByText(longName)).toBeDefined(); }); }); test("handles Unicode network name", async () => { const unicodeName = "ネットワーク \u{1F525} 测试"; H.mockListNetworks.mockResolvedValue({ networks: [ { ...MOCK_NETWORKS[0], name: unicodeName, id: "net-unicode" }, ], count: 1, }); renderPage(); await waitFor(() => { expect(screen.getByText(unicodeName)).toBeDefined(); }); }); test("handles missing optional counts (undefined)", async () => { H.mockListNetworks.mockResolvedValue({ networks: [ { ...MOCK_NETWORKS[0], approved_user_count: undefined, active_membership_count: undefined, }, ], count: 1, }); renderPage(); await waitFor(() => { expect(screen.getByText("Production VPN")).toBeDefined(); }); // Should show "0" for undefined counts (nullish coalescing: ?? 0) const zeros = screen.getAllByText("0"); expect(zeros.length).toBeGreaterThanOrEqual(2); }); });