import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createLocalUiServer } from '../index.js'; import { fakeNat } from './fakes.js'; import type { HostProcessOrchestrator } from '../../orchestrator/index.js'; import type { HostedNodeService } from '../../hosted/index.js'; function fakeOrchestrator(): HostProcessOrchestrator { return { listNodes: () => [], getNode: () => undefined, resolveDockerId: () => undefined, onStateChange: () => () => undefined, } as unknown as HostProcessOrchestrator; } /** A hosted-node service with nothing in it; its listener is what the server wires to the bus. */ function fakeHostedNodes(): HostedNodeService { return { list: () => [], get: () => undefined, onChange: () => () => undefined, } as unknown as HostedNodeService; } function writeConfig(dir: string): void { const cfg = { version: 3, installId: 'inst-x', uiPort: 8765, dataDir: dir, upnpEnabled: true, installedAt: '2025-01-01T00:00:00Z', installerVersion: '0.6.0', updates: { autoApply: false }, }; writeFileSync(join(dir, 'host.config.json'), JSON.stringify(cfg, null, 2) + '\n', 'utf8'); } describe('createLocalUiServer smoke', () => { let dataDir: string; let server: ReturnType; let baseUrl: string; beforeEach(async () => { dataDir = mkdtempSync(join(tmpdir(), 'cadre-host-smoke-')); writeConfig(dataDir); server = createLocalUiServer({ uiPort: 8765, dataDir, orchestrator: fakeOrchestrator(), nat: fakeNat(), hostedNodes: fakeHostedNodes(), forcePort: 0, }); const { url } = await server.start(); baseUrl = url; }); afterEach(async () => { await server.stop(); rmSync(dataDir, { recursive: true, force: true }); }); it('serves /api/status, with connectivity and no nodes', async () => { // Without host header: undici's fetch sets it; this is the happy path. const res = await fetch(`${baseUrl}/api/status`); expect(res.status).toBe(200); const body = await res.json() as { service: { name: string }; nodes: unknown[]; connectivity: { directReachability: string }; }; expect(body.service.name).toBe('cadre-host'); expect(body.nodes).toEqual([]); expect(body.connectivity.directReachability).toBe('reachable'); }); it('rejects requests with a foreign Host header', async () => { // fetch() forbids overriding the Host header — use node:http directly. const { request: httpRequest } = await import('node:http'); const u = new URL(`${baseUrl}/api/status`); const status = await new Promise((resolve, reject) => { const req = httpRequest( { hostname: u.hostname, port: u.port, path: u.pathname, method: 'GET', headers: { host: 'evil.example.com' }, }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode ?? 0)); }, ); req.on('error', reject); req.end(); }); expect(status).toBe(403); }); it('serves a placeholder index.html when dist/ui is missing', async () => { const res = await fetch(`${baseUrl}/`); expect(res.status).toBe(200); expect(res.headers.get('content-type') ?? '').toContain('text/html'); const text = await res.text(); expect(text).toContain('cadre-host is running'); }); it('an unknown /api path returns 404 (not the static SPA)', async () => { const res = await fetch(`${baseUrl}/api/this-does-not-exist`); expect(res.status).toBe(404); const body = await res.json() as { error: { code: string } }; expect(body.error.code).toBe('not_found'); }); it('serves the hosted-node surface (GET /api/hosted-nodes)', async () => { const res = await fetch(`${baseUrl}/api/hosted-nodes`); expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true, data: { nodes: [] } }); }); it('serves the NAT surface (GET /nat/status)', async () => { const res = await fetch(`${baseUrl}/nat/status`); expect(res.status).toBe(200); const body = await res.json() as { directReachability: string }; expect(body.directReachability).toBe('reachable'); }); });