diff --git a/web/src/pages/AgentFleetPage.test.tsx b/web/src/pages/AgentFleetPage.test.tsx new file mode 100644 index 0000000..48bfcb8 --- /dev/null +++ b/web/src/pages/AgentFleetPage.test.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, cleanup } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter } from 'react-router-dom'; +import type { ReactNode } from 'react'; + +// ----------------------------------------------------------------------------- +// M-029 Pass 3 (Audit M-026): AgentFleetPage XSS-hardening + render coverage. +// Agent name / hostname / OS / arch / IP are agent-self-reported (M-003 in +// the MCP fence path); the GUI rendering must also be XSS-safe. +// ----------------------------------------------------------------------------- + +vi.mock('../api/client', () => ({ + getAgents: vi.fn(), +})); + +import AgentFleetPage from './AgentFleetPage'; +import * as client from '../api/client'; + +function renderWithQuery(ui: ReactNode) { + const qc = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: 0, staleTime: 0 } }, + }); + return render( + + {ui} + , + ); +} + +const xssPayload = ''; + +const xssAgent = { + id: 'a-xss-001', + name: xssPayload, + hostname: xssPayload, + os: xssPayload, + architecture: xssPayload, + ip_address: xssPayload, + version: xssPayload, + status: 'online', + last_heartbeat_at: new Date().toISOString(), + agent_group_id: 'ag-xss', +}; + +describe('AgentFleetPage — render + XSS hardening (M-026 / M-029 Pass 3)', () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + delete (window as unknown as { __xss_pwned__?: number }).__xss_pwned__; + }); + + it('renders the page header when getAgents resolves', async () => { + vi.mocked(client.getAgents).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + renderWithQuery(); + await waitFor(() => { + expect(screen.getByText(/Agent/i)).toBeInTheDocument(); + }); + }); + + it('does NOT execute '; + +const xssCheck = { + id: 'hc-xss-001', + endpoint: xssPayload, + status: 'failing', + last_error: xssPayload, + last_checked_at: new Date().toISOString(), + acknowledged: false, +}; + +describe('HealthMonitorPage — render + XSS hardening (M-026 / M-029 Pass 3)', () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + delete (window as unknown as { __xss_pwned__?: number }).__xss_pwned__; + vi.mocked(client.getHealthCheckSummary).mockResolvedValue({ total: 0, failing: 0, ok: 0 } as never); + }); + + it('renders the page header when listHealthChecks resolves', async () => { + vi.mocked(client.listHealthChecks).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 100 } as never); + renderWithQuery(); + await waitFor(() => { + expect(screen.getByText(/Health/i)).toBeInTheDocument(); + }); + }); + + it('does NOT execute '; + +const xssJob = { + id: 'j-xss-001', + type: xssPayload, + status: 'Failed', + certificate_id: xssPayload, + agent_id: xssPayload, + error_message: xssPayload, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), +}; + +describe('JobsPage — render + XSS hardening (M-026 / M-029 Pass 3)', () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + delete (window as unknown as { __xss_pwned__?: number }).__xss_pwned__; + }); + + it('renders the page header when getJobs resolves', async () => { + vi.mocked(client.getJobs).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + renderWithQuery(); + await waitFor(() => { + expect(screen.getByText(/Jobs/i)).toBeInTheDocument(); + }); + }); + + it('does NOT execute '; + +const xssScanTarget = { + id: 'ns-xss-001', + name: xssPayload, + network_range: xssPayload, + ports: '443,8443', + agent_id: xssPayload, + enabled: true, + last_scan_at: new Date().toISOString(), + last_scan_status: 'failed', + last_scan_message: xssPayload, +}; + +describe('NetworkScanPage — render + XSS hardening (M-026 / M-029 Pass 3)', () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + delete (window as unknown as { __xss_pwned__?: number }).__xss_pwned__; + }); + + it('renders the page header when getNetworkScanTargets resolves', async () => { + vi.mocked(client.getNetworkScanTargets).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + renderWithQuery(); + await waitFor(() => { + expect(screen.getByText(/Network/i)).toBeInTheDocument(); + }); + }); + + it('does NOT execute '; + +const xssProfile = { + id: 'cp-xss-001', + name: xssPayload, + description: xssPayload, + max_ttl_seconds: 3600, + allow_short_lived: false, + ekus: [xssPayload], + key_usages: [xssPayload], + san_types: [xssPayload], + created_at: new Date().toISOString(), +}; + +describe('ProfilesPage — render + XSS hardening (M-026 / M-029 Pass 3)', () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + delete (window as unknown as { __xss_pwned__?: number }).__xss_pwned__; + }); + + it('renders the page header when getProfiles resolves', async () => { + vi.mocked(client.getProfiles).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + renderWithQuery(); + await waitFor(() => { + expect(screen.getByText(/Profile/i)).toBeInTheDocument(); + }); + }); + + it('does NOT execute