test(decnet_web): MSW-based test foundation for UI refactor
Phase 0 of the decnet_web refactor: stand up an MSW server, fixtures,
and a router-aware render helper so the upcoming god-component splits
(AttackerDetail first) can land with same-commit test coverage.
- msw devDep + setupServer wired into src/test/setup.ts
- src/test/server.ts re-exports server, http, HttpResponse, apiUrl()
- src/test/fixtures/{attacker,decky,canary,topology}.ts factories
- src/test/renderWithRouter.tsx wraps MemoryRouter + ToastProvider
- baseline coverage thresholds (0%) in vite.config.ts; raise per PR
- coverage/ added to decnet_web/.gitignore
Existing Orchestrator/AttackerDetail/ThemeLab tests stay on vi.mock
and continue to pass; new tests use MSW.
This commit is contained in:
58
decnet_web/src/test/fixtures/attacker.ts
vendored
Normal file
58
decnet_web/src/test/fixtures/attacker.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
export interface AttackerFixture {
|
||||
uuid: string;
|
||||
ip: string;
|
||||
identity_id: string | null;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
event_count: number;
|
||||
service_count: number;
|
||||
decky_count: number;
|
||||
services: string[];
|
||||
deckies: string[];
|
||||
traversal_path: string | null;
|
||||
is_traversal: boolean;
|
||||
bounty_count: number;
|
||||
credential_count: number;
|
||||
fingerprints: unknown[];
|
||||
commands: { service: string; decky: string; command: string; timestamp: string }[];
|
||||
country_code: string | null;
|
||||
country_source: string | null;
|
||||
asn: number | null;
|
||||
as_name: string | null;
|
||||
asn_source: string | null;
|
||||
ptr_record: string | null;
|
||||
updated_at: string;
|
||||
behavior: null;
|
||||
service_activity: { interacted: string[]; scanned: string[] };
|
||||
observations: never[];
|
||||
}
|
||||
|
||||
export const makeAttacker = (overrides: Partial<AttackerFixture> = {}): AttackerFixture => ({
|
||||
uuid: '11111111-1111-1111-1111-111111111111',
|
||||
ip: '198.51.100.10',
|
||||
identity_id: null,
|
||||
first_seen: '2026-05-01T10:00:00Z',
|
||||
last_seen: '2026-05-09T11:00:00Z',
|
||||
event_count: 12,
|
||||
service_count: 2,
|
||||
decky_count: 1,
|
||||
services: ['ssh', 'http'],
|
||||
deckies: ['decoy-01'],
|
||||
traversal_path: null,
|
||||
is_traversal: false,
|
||||
bounty_count: 0,
|
||||
credential_count: 0,
|
||||
fingerprints: [],
|
||||
commands: [],
|
||||
country_code: 'US',
|
||||
country_source: 'maxmind',
|
||||
asn: 64500,
|
||||
as_name: 'EXAMPLE-AS',
|
||||
asn_source: 'maxmind',
|
||||
ptr_record: null,
|
||||
updated_at: '2026-05-09T11:00:00Z',
|
||||
behavior: null,
|
||||
service_activity: { interacted: ['ssh'], scanned: ['http'] },
|
||||
observations: [],
|
||||
...overrides,
|
||||
});
|
||||
63
decnet_web/src/test/fixtures/canary.ts
vendored
Normal file
63
decnet_web/src/test/fixtures/canary.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
export interface CanaryTokenFixture {
|
||||
uuid: string;
|
||||
kind: 'http' | 'dns' | 'aws_passive';
|
||||
decky_name: string;
|
||||
topology_id: string | null;
|
||||
blob_uuid: string | null;
|
||||
instrumenter: string | null;
|
||||
generator: string | null;
|
||||
placement_path: string;
|
||||
callback_token: string;
|
||||
placed_at: string;
|
||||
last_triggered_at: string | null;
|
||||
trigger_count: number;
|
||||
created_by: string;
|
||||
state: 'planted' | 'revoked' | 'failed';
|
||||
last_error: string | null;
|
||||
}
|
||||
|
||||
export interface CanaryBlobFixture {
|
||||
uuid: string;
|
||||
sha256: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size_bytes: number;
|
||||
uploaded_by: string;
|
||||
uploaded_at: string;
|
||||
token_count: number;
|
||||
}
|
||||
|
||||
export const makeCanaryToken = (
|
||||
overrides: Partial<CanaryTokenFixture> = {},
|
||||
): CanaryTokenFixture => ({
|
||||
uuid: '22222222-2222-2222-2222-222222222222',
|
||||
kind: 'http',
|
||||
decky_name: 'decoy-01',
|
||||
topology_id: null,
|
||||
blob_uuid: null,
|
||||
instrumenter: 'git_config',
|
||||
generator: 'git_config',
|
||||
placement_path: '/etc/.git/config',
|
||||
callback_token: 'abc123token',
|
||||
placed_at: '2026-05-01T10:00:00Z',
|
||||
last_triggered_at: null,
|
||||
trigger_count: 0,
|
||||
created_by: 'admin',
|
||||
state: 'planted',
|
||||
last_error: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeCanaryBlob = (
|
||||
overrides: Partial<CanaryBlobFixture> = {},
|
||||
): CanaryBlobFixture => ({
|
||||
uuid: '33333333-3333-3333-3333-333333333333',
|
||||
sha256: 'a'.repeat(64),
|
||||
filename: 'creds.json',
|
||||
content_type: 'application/json',
|
||||
size_bytes: 1024,
|
||||
uploaded_by: 'admin',
|
||||
uploaded_at: '2026-05-01T10:00:00Z',
|
||||
token_count: 0,
|
||||
...overrides,
|
||||
});
|
||||
31
decnet_web/src/test/fixtures/decky.ts
vendored
Normal file
31
decnet_web/src/test/fixtures/decky.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface DeckyFixture {
|
||||
name: string;
|
||||
ip: string;
|
||||
services: string[];
|
||||
distro: string;
|
||||
hostname: string;
|
||||
archetype: string | null;
|
||||
service_config: Record<string, Record<string, unknown>>;
|
||||
mutate_interval: number | null;
|
||||
last_mutated: number;
|
||||
swarm?: {
|
||||
host_uuid: string;
|
||||
host_name: string;
|
||||
state: string;
|
||||
last_error: string | null;
|
||||
last_seen: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export const makeDecky = (overrides: Partial<DeckyFixture> = {}): DeckyFixture => ({
|
||||
name: 'decoy-01',
|
||||
ip: '10.10.10.10',
|
||||
services: ['ssh', 'http'],
|
||||
distro: 'debian-12',
|
||||
hostname: 'fileserver-01',
|
||||
archetype: 'workstation',
|
||||
service_config: {},
|
||||
mutate_interval: null,
|
||||
last_mutated: 0,
|
||||
...overrides,
|
||||
});
|
||||
9
decnet_web/src/test/fixtures/index.ts
vendored
Normal file
9
decnet_web/src/test/fixtures/index.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export { makeAttacker, type AttackerFixture } from './attacker';
|
||||
export { makeDecky, type DeckyFixture } from './decky';
|
||||
export {
|
||||
makeCanaryToken,
|
||||
makeCanaryBlob,
|
||||
type CanaryTokenFixture,
|
||||
type CanaryBlobFixture,
|
||||
} from './canary';
|
||||
export { makeTopology, type TopologyFixture } from './topology';
|
||||
18
decnet_web/src/test/fixtures/topology.ts
vendored
Normal file
18
decnet_web/src/test/fixtures/topology.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface TopologyFixture {
|
||||
id: string;
|
||||
name: string;
|
||||
mode: string;
|
||||
target_host_uuid: string | null;
|
||||
status: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export const makeTopology = (overrides: Partial<TopologyFixture> = {}): TopologyFixture => ({
|
||||
id: '44444444-4444-4444-4444-444444444444',
|
||||
name: 'corp-net-01',
|
||||
mode: 'flat',
|
||||
target_host_uuid: null,
|
||||
status: 'active',
|
||||
version: 1,
|
||||
...overrides,
|
||||
});
|
||||
34
decnet_web/src/test/renderWithRouter.tsx
Normal file
34
decnet_web/src/test/renderWithRouter.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { render, type RenderOptions, type RenderResult } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { ToastProvider } from '../components/Toasts/ToastProvider';
|
||||
|
||||
export interface RenderWithRouterOptions extends Omit<RenderOptions, 'wrapper'> {
|
||||
/** Initial URL the MemoryRouter starts at. */
|
||||
initialEntries?: string[];
|
||||
/** When set, the rendered UI is mounted at this route path so `useParams` resolves. */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
const Wrap = ({ children }: { children: ReactNode }) => (
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
);
|
||||
|
||||
export const renderWithRouter = (
|
||||
ui: ReactElement,
|
||||
{ initialEntries = ['/'], path, ...rest }: RenderWithRouterOptions = {},
|
||||
): RenderResult => {
|
||||
const tree = path ? (
|
||||
<Routes>
|
||||
<Route path={path} element={ui} />
|
||||
</Routes>
|
||||
) : (
|
||||
ui
|
||||
);
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<Wrap>{tree}</Wrap>
|
||||
</MemoryRouter>,
|
||||
rest,
|
||||
);
|
||||
};
|
||||
13
decnet_web/src/test/server.ts
Normal file
13
decnet_web/src/test/server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const API_BASE = 'http://localhost:8000/api/v1';
|
||||
|
||||
export const server = setupServer();
|
||||
|
||||
export { http, HttpResponse };
|
||||
|
||||
export const apiUrl = (path: string): string => {
|
||||
const trimmed = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${API_BASE}${trimmed}`;
|
||||
};
|
||||
@@ -1,7 +1,13 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { afterEach } from 'vitest';
|
||||
import { afterAll, afterEach, beforeAll } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import { server } from './server';
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => server.close());
|
||||
|
||||
Reference in New Issue
Block a user