fix(web): coerce fingerprint_type to string; sync frontend types and tests

This commit is contained in:
2026-05-10 22:27:38 -04:00
parent a009746dd1
commit 80fff1efa4
8 changed files with 23 additions and 16 deletions

View File

@@ -224,7 +224,7 @@ const AttackerDetail: React.FC = () => {
const groups: Record<string, any[]> = {}; const groups: Record<string, any[]> = {};
filteredFps.forEach((fp) => { filteredFps.forEach((fp) => {
const p = getPayload(fp); const p = getPayload(fp);
let fpType: string = p.fingerprint_type || 'unknown'; let fpType: string = String(p.fingerprint_type || 'unknown');
if (fpType === 'tls_certificate') { if (fpType === 'tls_certificate') {
fpType = p.target_ip ? 'tls_certificate_active' : 'tls_certificate_passive'; fpType = p.target_ip ? 'tls_certificate_active' : 'tls_certificate_passive';
} }

View File

@@ -34,7 +34,7 @@ describe('nextSortState', () => {
}); });
const cred = (over: Partial<CredentialEntry> = {}): CredentialEntry => ({ const cred = (over: Partial<CredentialEntry> = {}): CredentialEntry => ({
id: '1', last_seen: '2026-05-01T00:00:00Z', decky_name: 'd1', service: 'ssh', id: 1, last_seen: '2026-05-01T00:00:00Z', decky_name: 'd1', service: 'ssh',
attacker_ip: '1.2.3.4', principal: 'root', secret_sha256: 'a', attacker_ip: '1.2.3.4', principal: 'root', secret_sha256: 'a',
secret_kind: 'plaintext', secret_printable: 'p', attempt_count: 5, secret_kind: 'plaintext', secret_printable: 'p', attempt_count: 5,
...over, ...over,
@@ -42,7 +42,7 @@ const cred = (over: Partial<CredentialEntry> = {}): CredentialEntry => ({
describe('sortCreds', () => { describe('sortCreds', () => {
it('returns the input untouched for empty col', () => { it('returns the input untouched for empty col', () => {
const rows = [cred({ id: 'A' }), cred({ id: 'B' })]; const rows = [cred({ id: 1 }), cred({ id: 2 })];
expect(sortCreds(rows, '', 'asc')).toBe(rows); expect(sortCreds(rows, '', 'asc')).toBe(rows);
}); });
it('sorts numbers numerically (asc/desc)', () => { it('sorts numbers numerically (asc/desc)', () => {

View File

@@ -26,7 +26,9 @@
} }
.sidebar-header { .sidebar-header {
padding: 20px; height: 64px;
flex-shrink: 0;
padding: 0 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;

View File

@@ -66,6 +66,11 @@
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
html[data-theme="light"] .login-form input {
background-color: rgba(255, 255, 255, 0.7);
color: var(--ink, #1a1a1a);
}
.error-msg { .error-msg {
color: #ff4141; color: #ff4141;
font-size: 0.8rem; font-size: 0.8rem;
@@ -80,6 +85,7 @@
margin-top: 8px; margin-top: 8px;
font-weight: bold; font-weight: bold;
letter-spacing: 2px; letter-spacing: 2px;
justify-content: center;
} }
.login-footer { .login-footer {

View File

@@ -7,9 +7,6 @@ import { useMazeContextMenu } from './useMazeContextMenu';
import type { Net, MazeNode } from './types'; import type { Net, MazeNode } from './types';
import type { UseTopologyEditor } from './useTopologyEditor'; import type { UseTopologyEditor } from './useTopologyEditor';
const noop = () => {};
const noopAsync = async () => {};
const stubEditor = (): UseTopologyEditor => ({ const stubEditor = (): UseTopologyEditor => ({
inFlight: 0, inFlight: 0,
addLan: vi.fn(), addLan: vi.fn(),
@@ -43,7 +40,8 @@ const decky: MazeNode = {
archetype: 'workstation', services: ['ssh'], status: 'idle', x: 0, y: 0, archetype: 'workstation', services: ['ssh'], status: 'idle', x: 0, y: 0,
}; };
const observed: MazeNode = { const observed: MazeNode = {
kind: 'observed', id: 'obs-1', label: '1.2.3.4', netId: 'lan-www', kind: 'observed', id: 'obs-1', netId: 'lan-www', name: '1.2.3.4',
archetype: 'attacker-pool', services: ['*'], status: 'idle',
x: 0, y: 0, x: 0, y: 0,
}; };
@@ -51,7 +49,10 @@ const baseArgs = () => ({
nets: [subnet, internet], nets: [subnet, internet],
nodes: [decky, observed], nodes: [decky, observed],
services: [ services: [
{ slug: 'http', name: 'HTTP', proto: 'tcp', port: 80, icon: 'globe', risk: 'medium' as const }, {
slug: 'http', name: 'HTTP', proto: 'tcp' as const, port: 80,
icon: 'globe', risk: 'med' as const, group: 'Web' as const,
},
], ],
archetypes: [ archetypes: [
{ slug: 'workstation', name: 'Workstation', services: ['ssh'], icon: 'monitor' }, { slug: 'workstation', name: 'Workstation', services: ['ssh'], icon: 'monitor' },

View File

@@ -47,7 +47,7 @@ describe('coercePersona', () => {
const r = coercePersona({ const r = coercePersona({
name: 'a', email: 'a@b.c', role: 'r', name: 'a', email: 'a@b.c', role: 'r',
tone: 'custom', tone_custom: long, tone: 'custom', tone_custom: long,
mannerisms: Array.from({ length: 20 }, (_, i) => `m${i}`).concat([42, null]), mannerisms: Array.from({ length: 20 }, (_, i) => `m${i}`).concat(['42', ''] as string[]),
}); });
expect('ok' in r).toBe(true); expect('ok' in r).toBe(true);
if ('ok' in r) { if ('ok' in r) {

View File

@@ -100,7 +100,7 @@ describe('useSwarmHosts', () => {
}); });
}); });
expect(r?.ok).toBe(true); expect(r?.ok).toBe(true);
expect(r?.data?.host_uuid).toBe('h-9'); expect(r?.ok && r.data?.host_uuid).toBe('h-9');
}); });
it('polls /swarm/hosts every 10s', async () => { it('polls /swarm/hosts every 10s', async () => {

View File

@@ -3,11 +3,9 @@ import api from '../../utils/api';
import { extractErrorDetail } from './helpers'; import { extractErrorDetail } from './helpers';
import type { BundleRequest, BundleResult, SwarmHost } from './types'; import type { BundleRequest, BundleResult, SwarmHost } from './types';
export interface MutationResult<T = void> { export type MutationResult<T = void> = T extends void
ok: boolean; ? { ok: true } | { ok: false; reason: string }
reason?: string; : { ok: true; data: T } | { ok: false; reason: string };
data?: T;
}
export interface UseSwarmHosts { export interface UseSwarmHosts {
hosts: SwarmHost[]; hosts: SwarmHost[];