mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
Dynamic (#77)
This commit is contained in:
parent
55247b7fcd
commit
991c84a1eb
1464 changed files with 225448 additions and 1985 deletions
154
frontend/src/test/components/dynamicVerdictSection.test.tsx
Normal file
154
frontend/src/test/components/dynamicVerdictSection.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
144
frontend/src/test/components/verdictBadge.test.tsx
Normal file
144
frontend/src/test/components/verdictBadge.test.tsx
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
110
frontend/src/test/graph/surfaceAdapter.test.ts
Normal file
110
frontend/src/test/graph/surfaceAdapter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
83
frontend/src/test/modals/NewScanModal.test.tsx
Normal file
83
frontend/src/test/modals/NewScanModal.test.tsx
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue