typhon/tests/ControlApiServer.test.ts

161 lines
4.9 KiB
TypeScript
Raw Normal View History

import http from "node:http";
import { ControlApiServer } from "../src/http/ControlApiServer";
import {
ControlPortState,
ControllerControlStatus,
WifiRecoveryRequest,
WifiRecoveryResult
} 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;
public wifiRecoveryRequests: WifiRecoveryRequest[] = [];
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<ControllerControlStatus> {
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<ControllerControlStatus> {
this.mac = macAddress;
return this.getStatus();
}
public async getPorts(): Promise<ControlPortState[]> {
return this.ports;
}
public async setPortSpeed(port: number, speedLevel: number): Promise<void> {
this.ports = this.ports.map((item) => {
if (item.port !== port) {
return item;
}
return {
...item,
currentSpeedLevel: speedLevel,
powerState: speedLevel > 0
};
});
}
public async recoverWifi(request: WifiRecoveryRequest): Promise<WifiRecoveryResult> {
this.wifiRecoveryRequests.push(request);
return {
queued: true,
backend: "fake",
controllerMac: this.mac,
notes: ["queued"]
};
}
}
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 wifiRecover = await requestJson(port, "POST", "/api/v2/wifi/recover", {
ssid: "atlas-net",
password: "secret-password",
hidden: false
});
expect(wifiRecover.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);
expect(backend.wifiRecoveryRequests).toHaveLength(1);
expect(backend.wifiRecoveryRequests[0]).toEqual({
ssid: "atlas-net",
password: "secret-password",
hidden: false
});
await server.stop();
});
});