import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; const useLicenseMock = vi.fn(); const useAuthMock = vi.fn(); const useNodesMock = vi.fn(); vi.mock('@/context/LicenseContext', () => ({ useLicense: () => useLicenseMock() })); vi.mock('@/context/AuthContext', () => ({ useAuth: () => useAuthMock() })); vi.mock('@/context/NodeContext', () => ({ useNodes: () => useNodesMock() })); vi.mock('@/lib/api', () => ({ apiFetch: vi.fn() })); vi.mock('@/lib/nodesApi', () => ({ cordonNode: vi.fn(), uncordonNode: vi.fn() })); vi.mock('@/components/ui/toast-store', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); import { NodeCard } from '../NodeCard'; import type { FleetNode } from '../types'; function onlineNode(): FleetNode { return { id: 2, name: 'Edge', type: 'remote', status: 'online', stats: { active: 3, managed: 3, unmanaged: 0, exited: 1, total: 4 }, systemStats: { cpu: { usage: '20.0', cores: 4 }, memory: { total: 100, used: 40, free: 60, usagePercent: '40.0' }, disk: { total: 100, used: 30, free: 70, usagePercent: '30.0' } }, stacks: ['web'], cordoned: false, cordoned_at: null, cordoned_reason: null, }; } function offlineNode(): FleetNode { return { ...onlineNode(), status: 'offline', stats: null, systemStats: null, stacks: null }; } function baseProps(node: FleetNode) { return { node, onNavigate: vi.fn() }; } beforeEach(() => { useNodesMock.mockReturnValue({ nodes: [], hasCapability: vi.fn(() => false) }); useAuthMock.mockReturnValue({ isAdmin: true, can: vi.fn(() => true) }); useLicenseMock.mockReturnValue({ isPaid: false }); }); afterEach(() => vi.clearAllMocks()); describe('NodeCard', () => { it('renders stats and the online badge for an online node', () => { render(); expect(screen.getByText('Online')).toBeInTheDocument(); expect(screen.getByText('Running')).toBeInTheDocument(); expect(screen.queryByText('Node unreachable')).not.toBeInTheDocument(); }); it('shows the unreachable placeholder and hides stats for an offline node', () => { render(); expect(screen.getByText('Offline')).toBeInTheDocument(); expect(screen.getByText('Node unreachable')).toBeInTheDocument(); expect(screen.queryByText('Running')).not.toBeInTheDocument(); }); it('hides the actions menu for a free-tier user', () => { useLicenseMock.mockReturnValue({ isPaid: false }); render(); expect(screen.queryByRole('button', { name: 'Node actions' })).not.toBeInTheDocument(); }); it('hides the actions menu for a Community admin with node:manage when cordon requires Admiral', () => { useLicenseMock.mockReturnValue({ isPaid: false }); render(); // Cordon is Admiral-only; without edit/delete props, no menu items are available to Community. expect(screen.queryByRole('button', { name: 'Node actions' })).not.toBeInTheDocument(); }); it('exposes the actions menu (cordon entry point) for a paid admin', () => { useLicenseMock.mockReturnValue({ isPaid: true }); render(); // With no edit/delete affordances wired, the menu renders iff cordon is // allowed: isPaid && can('node:manage'). The admin's can() returns true. expect(screen.getByRole('button', { name: 'Node actions' })).toBeInTheDocument(); }); it('exposes the cordon control for an Admiral node-admin via the node:manage permission', async () => { const can = vi.fn((action: string) => action === 'node:manage'); useAuthMock.mockReturnValue({ isAdmin: false, can }); useLicenseMock.mockReturnValue({ isPaid: true }); render(); await userEvent.click(screen.getByRole('button', { name: 'Node actions' })); expect(await screen.findByText('Cordon node')).toBeInTheDocument(); expect(can).toHaveBeenCalledWith('node:manage', 'node', '2'); }); it('hides the cordon control from a paid user lacking node:manage', () => { useAuthMock.mockReturnValue({ isAdmin: false, can: vi.fn(() => false) }); useLicenseMock.mockReturnValue({ isPaid: true }); render(); // The paid tier alone must not surface cordon to a deployer/viewer/auditor. expect(screen.queryByRole('button', { name: 'Node actions' })).not.toBeInTheDocument(); }); it('shows Uncordon when the node is already cordoned', async () => { const can = vi.fn((action: string) => action === 'node:manage'); useAuthMock.mockReturnValue({ isAdmin: false, can }); useLicenseMock.mockReturnValue({ isPaid: true }); render(); await userEvent.click(screen.getByRole('button', { name: 'Node actions' })); expect(await screen.findByText('Uncordon node')).toBeInTheDocument(); }); const updateAvailableStatus = { nodeId: 2, name: 'Edge', type: 'remote' as const, version: '1.0.0', latestVersion: '1.1.0', updateAvailable: true, updateStatus: null, }; it('renders the update button for an admin when an update is available', () => { useAuthMock.mockReturnValue({ isAdmin: true }); render(); expect(screen.getByRole('button', { name: /Update/ })).toBeInTheDocument(); }); it('hides the update button and shows Pinned when updateBlocked', () => { useAuthMock.mockReturnValue({ isAdmin: true }); render( , ); expect(screen.getByText('Pinned')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /Update/ })).not.toBeInTheDocument(); }); it('shows the networking signal badge and switches to the node on click', async () => { const onOpenNetworking = vi.fn(); const user = userEvent.setup(); render( , ); const badge = screen.getByText(/Networking/); expect(badge).toBeInTheDocument(); await user.click(badge); expect(onOpenNetworking).toHaveBeenCalledWith(2); }); it('hides the networking signal badge when there is nothing to flag', () => { render( , ); expect(screen.queryByText(/Networking/)).not.toBeInTheDocument(); }); });