import http from "node:http"; import { ControlApiServer } from "../src/http/ControlApiServer"; import { ControlPortState, ControllerControlStatus } from "../src/domain/ControlTypes"; import { Logger } from "../src/observability/Logger"; import { ControllerControlBackend } from "../src/services/ControllerControlBackend"; import { RuleEngineService } from "../src/services/RuleEngineService"; class FakeBackend implements ControllerControlBackend { private mac: string | null = null; private ports: ControlPortState[] = [ { port: 1, name: "Port 1", fanGroup: "outlet", currentSpeedLevel: 0, online: true, powerState: false }, { port: 2, name: "Port 2", fanGroup: "inside_inlet", currentSpeedLevel: 0, online: true, powerState: false }, { port: 3, name: "Port 3", fanGroup: "outside_inlet", currentSpeedLevel: 0, online: true, powerState: false }, { port: 4, name: "Port 4", fanGroup: "interior", currentSpeedLevel: 0, online: true, powerState: false } ]; public async getStatus(): Promise { return { mode: "ble", backend: "fake", controllerMac: this.mac, connected: this.mac !== null, paired: this.mac !== null, telemetrySource: "ble", lastSnapshotEpochSeconds: null, capabilities: { pairing: true, portControl: true, advancedRules: true }, ports: this.ports, notes: [] }; } public async pair(macAddress: string): Promise { this.mac = macAddress; return this.getStatus(); } public async getPorts(): Promise { return this.ports; } public async setPortSpeed(port: number, speedLevel: number): Promise { this.ports = this.ports.map((item) => { if (item.port !== port) { return item; } return { ...item, currentSpeedLevel: speedLevel, powerState: speedLevel > 0 }; }); } } async function requestJson( port: number, method: string, path: string, body?: unknown ): Promise<{ statusCode: number; payload: unknown }> { return new Promise((resolve, reject) => { const req = http.request( { host: "127.0.0.1", port, method, path, headers: body ? { "content-type": "application/json" } : undefined }, (res) => { const chunks: Buffer[] = []; res.on("data", (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); res.on("end", () => { const text = Buffer.concat(chunks).toString("utf8"); let parsed: unknown = text; try { parsed = JSON.parse(text); } catch { // non-json response in tests should still be visible } resolve({ statusCode: res.statusCode ?? 0, payload: parsed }); }); } ); req.on("error", reject); if (body !== undefined) { req.write(JSON.stringify(body)); } req.end(); }); } describe("ControlApiServer", () => { it("supports status, pair, and per-port speed writes", async () => { const backend = new FakeBackend(); const rules = new RuleEngineService(); const logger = new Logger("error", "test"); const port = 19110; const server = new ControlApiServer(port, backend, rules, logger, "58:8C:81:C6:FC:F6"); await server.start(); const statusBefore = await requestJson(port, "GET", "/api/v2/status"); expect(statusBefore.statusCode).toBe(200); const pair = await requestJson(port, "POST", "/api/v2/pair", {}); expect(pair.statusCode).toBe(200); const setSpeed = await requestJson(port, "POST", "/api/v2/ports/4/speed", { speed_level: 8 }); expect(setSpeed.statusCode).toBe(200); const ports = await requestJson(port, "GET", "/api/v2/ports"); expect(ports.statusCode).toBe(200); const parsed = ports.payload as { ports?: Array<{ port: number; currentSpeedLevel: number }> }; const interior = parsed.ports?.find((item) => item.port === 4); expect(interior?.currentSpeedLevel).toBe(8); await server.stop(); }); });