This commit is contained in:
Eli Peter 2026-06-05 10:16:30 -05:00 committed by GitHub
parent 55247b7fcd
commit 991c84a1eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1464 changed files with 225448 additions and 1985 deletions

View file

@ -0,0 +1,154 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { DynamicVerdictSection } from '@/pages/FindingDetailPage';
import type { VerifyResult } from '@/api/types';
function makeVerdict(
status: VerifyResult['status'],
extras: Partial<VerifyResult> = {},
): VerifyResult {
return {
finding_id: 'test-finding-id-abc',
status,
attempts: [],
...extras,
};
}
// Mock navigator.clipboard before each test.
beforeEach(() => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: vi.fn().mockResolvedValue(undefined) },
configurable: true,
writable: true,
});
});
describe('DynamicVerdictSection', () => {
it('renders Confirmed badge', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('Confirmed', {
triggered_payload: 'sqli-tautology',
})}
/>,
);
expect(screen.getByTestId('verdict-badge-confirmed')).toBeInTheDocument();
});
it('renders NotConfirmed badge', () => {
render(<DynamicVerdictSection verdict={makeVerdict('NotConfirmed')} />);
expect(
screen.getByTestId('verdict-badge-notconfirmed'),
).toBeInTheDocument();
});
it('renders PartiallyConfirmed badge', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('PartiallyConfirmed', {
detail: 'sink reached but exploit chain did not complete',
})}
/>,
);
expect(
screen.getByTestId('verdict-badge-partiallyconfirmed'),
).toBeInTheDocument();
});
it('does not crash when the API omits an empty attempts array', () => {
render(
<DynamicVerdictSection
verdict={{ finding_id: 'no-attempts', status: 'Confirmed' }}
/>,
);
expect(screen.getByTestId('verdict-badge-confirmed')).toBeInTheDocument();
});
it('renders Unsupported badge', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('Unsupported', { reason: 'NoPayloadsForCap' })}
/>,
);
expect(screen.getByTestId('verdict-badge-unsupported')).toBeInTheDocument();
});
it('renders Inconclusive badge', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('Inconclusive', {
inconclusive_reason: 'BuildFailed',
})}
/>,
);
expect(
screen.getByTestId('verdict-badge-inconclusive'),
).toBeInTheDocument();
});
it('shows repro panel only for Confirmed status', () => {
const { unmount } = render(
<DynamicVerdictSection verdict={makeVerdict('Confirmed')} />,
);
expect(screen.getByTestId('repro-panel')).toBeInTheDocument();
unmount();
for (const status of [
'PartiallyConfirmed',
'NotConfirmed',
'Unsupported',
'Inconclusive',
] as const) {
const { unmount: u } = render(
<DynamicVerdictSection verdict={makeVerdict(status)} />,
);
expect(screen.queryByTestId('repro-panel')).toBeNull();
u();
}
});
it('repro-panel contains the finding_id in the CLI command', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('Confirmed', { finding_id: 'cafecafe12345678' })}
/>,
);
const panel = screen.getByTestId('repro-panel');
expect(panel.textContent).toContain('cafecafe12345678');
expect(panel.textContent).toContain('nyx repro');
});
it('Copy button triggers clipboard writeText with the repro command', async () => {
const findingId = 'test-finding-id-abc';
render(<DynamicVerdictSection verdict={makeVerdict('Confirmed')} />);
const copyBtn = screen.getByRole('button', { name: /copy/i });
fireEvent.click(copyBtn);
expect(navigator.clipboard.writeText).toHaveBeenCalledOnce();
const calledWith = (
navigator.clipboard.writeText as ReturnType<typeof vi.fn>
).mock.calls[0][0] as string;
expect(calledWith).toContain(findingId);
expect(calledWith).toContain('nyx repro');
});
it('shows exact toolchain match label when toolchain_match is exact', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('Confirmed', { toolchain_match: 'exact' })}
/>,
);
expect(screen.getByText('exact toolchain')).toBeInTheDocument();
});
it('shows approximate toolchain match label when toolchain_match is drift', () => {
render(
<DynamicVerdictSection
verdict={makeVerdict('Confirmed', { toolchain_match: 'drift' })}
/>,
);
expect(screen.getByText('approximate toolchain')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,144 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { VerdictBadge } from '@/components/VerdictBadge';
import type { VerifyResult } from '@/api/types';
function makeVerdict(
status: VerifyResult['status'],
extras: Partial<VerifyResult> = {},
): VerifyResult {
return {
finding_id: 'test-finding-id',
status,
attempts: [],
...extras,
};
}
describe('VerdictBadge', () => {
it('renders dash when verdict is undefined', () => {
render(<VerdictBadge verdict={undefined} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('renders Confirmed badge with flame and correct class', () => {
render(
<VerdictBadge
verdict={makeVerdict('Confirmed', {
triggered_payload: 'sqli-tautology',
})}
/>,
);
const badge = screen.getByTestId('verdict-badge-confirmed');
expect(badge).toBeInTheDocument();
expect(badge.className).toContain('badge-dyn-confirmed');
expect(badge.textContent).toContain('🔥');
});
it('renders PartiallyConfirmed badge with amber class and no flame', () => {
render(
<VerdictBadge
verdict={makeVerdict('PartiallyConfirmed', {
detail:
'sink-reachability probe fired but the oracle marker was not observed',
})}
/>,
);
const badge = screen.getByTestId('verdict-badge-partiallyconfirmed');
expect(badge).toBeInTheDocument();
expect(badge.className).toContain('badge-dyn-partiallyconfirmed');
expect(badge.textContent).not.toContain('🔥');
expect(badge.getAttribute('title')).toContain('sink reached');
});
it('renders NotConfirmed badge with correct class', () => {
render(<VerdictBadge verdict={makeVerdict('NotConfirmed')} />);
const badge = screen.getByTestId('verdict-badge-notconfirmed');
expect(badge).toBeInTheDocument();
expect(badge.className).toContain('badge-dyn-notconfirmed');
expect(badge.textContent).not.toContain('🔥');
});
it('renders when attempts are omitted by the API', () => {
render(
<VerdictBadge
verdict={{ finding_id: 'test-finding-id', status: 'NotConfirmed' }}
/>,
);
expect(
screen.getByTestId('verdict-badge-notconfirmed'),
).toBeInTheDocument();
});
it('renders Unsupported badge with correct class', () => {
render(
<VerdictBadge
verdict={makeVerdict('Unsupported', { reason: 'NoPayloadsForCap' })}
/>,
);
const badge = screen.getByTestId('verdict-badge-unsupported');
expect(badge).toBeInTheDocument();
expect(badge.className).toContain('badge-dyn-unsupported');
});
it('renders Inconclusive badge with amber class', () => {
render(
<VerdictBadge
verdict={makeVerdict('Inconclusive', {
inconclusive_reason: 'BuildFailed',
})}
/>,
);
const badge = screen.getByTestId('verdict-badge-inconclusive');
expect(badge).toBeInTheDocument();
expect(badge.className).toContain('badge-dyn-inconclusive');
});
it('tooltip contains payload for Confirmed', () => {
render(
<VerdictBadge
verdict={makeVerdict('Confirmed', {
triggered_payload: 'sqli-payload',
})}
/>,
);
const badge = screen.getByTestId('verdict-badge-confirmed');
expect(badge.getAttribute('title')).toContain('sqli-payload');
});
it('tooltip contains reason for Unsupported', () => {
render(
<VerdictBadge
verdict={makeVerdict('Unsupported', { reason: 'ConfidenceTooLow' })}
/>,
);
const badge = screen.getByTestId('verdict-badge-unsupported');
expect(badge.getAttribute('title')).toContain('ConfidenceTooLow');
});
it('compact mode renders single character', () => {
render(<VerdictBadge verdict={makeVerdict('Confirmed')} compact />);
const badge = screen.getByTestId('verdict-badge-confirmed');
// Compact: first char of status + flame emoji
expect(badge.textContent?.replace('🔥 ', '')).toBe('C');
});
it('renders all five VerifyStatus variants without crashing', () => {
const statuses: VerifyResult['status'][] = [
'Confirmed',
'PartiallyConfirmed',
'NotConfirmed',
'Unsupported',
'Inconclusive',
];
for (const status of statuses) {
const { unmount } = render(
<VerdictBadge verdict={makeVerdict(status)} />,
);
expect(
screen.getByTestId(`verdict-badge-${status.toLowerCase()}`),
).toBeInTheDocument();
unmount();
}
});
});

View file

@ -49,6 +49,29 @@ describe('getNodeStyle', () => {
const s = getNodeStyle('Call', 'callgraph', { isRecursive: true });
expect(s.fill).toBe('#5a5042');
});
it('returns a double shape for surface entry-point nodes', () => {
const s = getNodeStyle('EntryPoint', 'surface');
expect(s.shape).toBe('double');
expect(s.fill).toBe('#1c5c38');
});
it('returns a terminal shape for surface dangerous-local nodes', () => {
const s = getNodeStyle('DangerousLocal', 'surface');
expect(s.shape).toBe('terminal');
expect(s.fill).toBe('#9d2f25');
});
it('returns a warning fill for surface data-store nodes', () => {
const s = getNodeStyle('DataStore', 'surface');
expect(s.fill).toBe('#8c6310');
expect(s.shape).toBe('rect');
});
it('returns an accent fill for surface external-service nodes', () => {
const s = getNodeStyle('ExternalService', 'surface');
expect(s.fill).toBe('#0b3d2a');
});
});
describe('getEdgeStyle', () => {
@ -90,4 +113,26 @@ describe('getEdgeStyle', () => {
const s = getEdgeStyle('Call', 'callgraph');
expect(s.dash).toEqual([]);
});
it('returns a dashed style for surface auth_required_on edges', () => {
const s = getEdgeStyle('auth_required_on', 'surface');
expect(s.dash).toEqual([2, 4]);
});
it('returns a solid danger color for surface reaches edges', () => {
const s = getEdgeStyle('reaches', 'surface');
expect(s.color).toBe('#9d2f25');
expect(s.dash).toEqual([]);
});
it('returns a dashed success style for surface triggers edges', () => {
const s = getEdgeStyle('triggers', 'surface');
expect(s.dash).toEqual([4, 3]);
});
it('returns a fallback style for unknown surface edge kinds', () => {
const s = getEdgeStyle('mystery', 'surface');
expect(s.color).toContain('rgba');
expect(s.dash).toEqual([]);
});
});

View file

@ -0,0 +1,110 @@
import { describe, expect, it } from 'vitest';
import { adaptSurfaceMap, SURFACE_NODE_KIND } from '@/graph/adapters/surface';
import type { SurfaceMap } from '@/api/types';
const SAMPLE: SurfaceMap = {
nodes: [
{
node: 'entry_point',
location: { file: 'app.py', line: 10, col: 0 },
framework: 'flask',
method: 'POST',
route: '/api/run',
handler_name: 'run',
handler_location: { file: 'app.py', line: 12, col: 2 },
auth_required: false,
},
{
node: 'data_store',
location: { file: 'db.py', line: 40, col: 0 },
kind: 'sql',
label: 'orders',
},
{
node: 'external_service',
location: { file: 'client.py', line: 5, col: 0 },
kind: 'http_api',
label: 'github.com',
},
{
node: 'dangerous_local',
location: { file: 'app.py', line: 24, col: 4 },
function_name: 'run',
cap_bits: 0x400,
},
],
edges: [
{ from: 0, to: 3, kind: 'calls' },
{ from: 3, to: 1, kind: 'writes_to' },
{ from: 0, to: 2, kind: 'talks_to' },
],
};
describe('adaptSurfaceMap', () => {
it('produces a surface-kind GraphModel', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.kind).toBe('surface');
expect(model.nodes).toHaveLength(4);
expect(model.edges).toHaveLength(3);
});
it('keys nodes by index so SurfaceEdge.from/to map directly', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes.map((n) => n.key)).toEqual(['0', '1', '2', '3']);
expect(model.edges[0]?.source).toBe('0');
expect(model.edges[0]?.target).toBe('3');
});
it('maps each SurfaceNode kind to a distinct style discriminator', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[0]?.kind).toBe(SURFACE_NODE_KIND.entry_point);
expect(model.nodes[1]?.kind).toBe(SURFACE_NODE_KIND.data_store);
expect(model.nodes[2]?.kind).toBe(SURFACE_NODE_KIND.external_service);
expect(model.nodes[3]?.kind).toBe(SURFACE_NODE_KIND.dangerous_local);
});
it('builds entry-point labels from method and route', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[0]?.label).toBe('POST /api/run');
expect(model.nodes[0]?.detail).toBe('flask · run');
});
it('renders dangerous_local cap_bits as hex in detail', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[3]?.detail).toBe('cap=0x400');
});
it('uses handler_location for entry_point line, location for others', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[0]?.line).toBe(12);
expect(model.nodes[1]?.line).toBe(40);
});
it('emits an auth badge only for entry_points marked auth_required', () => {
const protectedEntry = adaptSurfaceMap({
nodes: [
{
...SAMPLE.nodes[0],
node: 'entry_point',
auth_required: true,
} as SurfaceMap['nodes'][0],
],
edges: [],
});
expect(protectedEntry.nodes[0]?.badges).toEqual(['auth']);
const openEntry = adaptSurfaceMap(SAMPLE);
expect(openEntry.nodes[0]?.badges).toBeUndefined();
});
it('produces unique edge keys even for parallel edges of the same kind', () => {
const parallel: SurfaceMap = {
nodes: SAMPLE.nodes,
edges: [
{ from: 0, to: 1, kind: 'calls' },
{ from: 0, to: 1, kind: 'calls' },
],
};
const model = adaptSurfaceMap(parallel);
expect(model.edges[0]?.key).not.toBe(model.edges[1]?.key);
});
});

View file

@ -0,0 +1,83 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { NewScanModal } from '@/modals/NewScanModal';
const mockMutateAsync = vi.hoisted(() => vi.fn());
const mockNavigate = vi.hoisted(() => vi.fn());
const mockToastSuccess = vi.hoisted(() => vi.fn());
const mockToastError = vi.hoisted(() => vi.fn());
vi.mock('@/api/queries/health', () => ({
useHealth: () => ({ data: { scan_root: '/test/project' } }),
}));
vi.mock('@/api/mutations/scans', () => ({
useStartScan: () => ({
mutateAsync: mockMutateAsync,
isPending: false,
}),
}));
vi.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));
vi.mock('@/contexts/ToastContext', () => ({
useToast: () => ({ success: mockToastSuccess, error: mockToastError }),
}));
vi.mock('@/components/ui/Modal', () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Modal: ({ open, children }: { open: boolean; children?: any }) =>
open ? <>{children}</> : null,
}));
describe('NewScanModal', () => {
beforeEach(() => {
mockMutateAsync.mockReset();
mockMutateAsync.mockResolvedValue(undefined);
mockNavigate.mockReset();
mockToastSuccess.mockReset();
mockToastError.mockReset();
});
it('renders when open is true', () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
expect(screen.getByText('Start new scan')).toBeInTheDocument();
});
it('calls mutateAsync without verify key when checkbox is untouched', async () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
const payload = mockMutateAsync.mock.calls[0][0];
expect(payload).not.toHaveProperty('verify');
expect(payload).toEqual({
engine_profile: 'balanced',
verify_backend: 'auto',
harden_profile: 'standard',
});
});
it('calls mutateAsync with verify: false when checkbox is checked', async () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
fireEvent.click(screen.getByRole('checkbox'));
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
const payload = mockMutateAsync.mock.calls[0][0];
expect(payload).toEqual({ engine_profile: 'balanced', verify: false });
});
it('allows selecting the unsafe process verification backend', async () => {
render(<NewScanModal open={true} onClose={vi.fn()} />);
const selects = screen.getAllByRole('combobox');
fireEvent.change(selects[2], { target: { value: 'process' } });
fireEvent.click(screen.getByRole('button', { name: 'Start scan' }));
await waitFor(() => expect(mockMutateAsync).toHaveBeenCalledOnce());
const payload = mockMutateAsync.mock.calls[0][0];
expect(payload).toMatchObject({
verify_backend: 'process',
harden_profile: 'standard',
});
});
});