162 lines
6.6 KiB
TypeScript
162 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createConnection } from "../src/modules/trapos-cloud-gateway/connection.js";
|
|
import { GatewayRegistry } from "../src/modules/trapos-cloud-gateway/registry.js";
|
|
import type { OpencodeProbe, ProbeResult } from "../src/opencode.js";
|
|
import { FakeSocket, silentLog } from "./helpers.js";
|
|
|
|
function fakeProbe(result: ProbeResult): OpencodeProbe {
|
|
return { probe: () => Promise.resolve(result) };
|
|
}
|
|
|
|
function setup(opts?: { secret?: string; probe?: ProbeResult }) {
|
|
const socket = new FakeSocket();
|
|
const registry = new GatewayRegistry(silentLog);
|
|
const expectedSecret = opts?.secret;
|
|
const connection = createConnection({
|
|
socket,
|
|
registry,
|
|
probe: fakeProbe(opts?.probe ?? { ok: true }),
|
|
resolveAccount: (secret) => (expectedSecret === undefined || secret === expectedSecret ? "local" : null),
|
|
log: silentLog,
|
|
});
|
|
return { socket, registry, connection };
|
|
}
|
|
|
|
function helloFrame(traposId: string | undefined, secret?: string): string {
|
|
const payload: Record<string, unknown> = {};
|
|
if (traposId !== undefined) payload.traposId = traposId;
|
|
if (secret !== undefined) payload.secret = secret;
|
|
return JSON.stringify({ event: "computer_request", traposSenderId: traposId ?? "?", messageId: "h1", type: "hello", payload });
|
|
}
|
|
|
|
function requestFrame(type: string, messageId = "r1"): string {
|
|
return JSON.stringify({ event: "computer_request", traposSenderId: "7:", messageId, type, payload: {} });
|
|
}
|
|
|
|
test("pre-hello non-hello request -> not_authenticated, no close, not registered", async () => {
|
|
const { socket, registry, connection } = setup();
|
|
await connection.onMessage(requestFrame("probe_server"));
|
|
const frame = socket.lastFrame();
|
|
assert.equal(frame.event, "server_response");
|
|
assert.equal(frame.ok, false);
|
|
assert.deepEqual(frame.error, { code: "not_authenticated", message: "hello required before any other request" });
|
|
assert.equal(socket.closed, false);
|
|
assert.equal(registry.count(), 0);
|
|
});
|
|
|
|
test("pre-hello wrong-direction frame is dropped silently", async () => {
|
|
const { socket, connection } = setup();
|
|
await connection.onMessage(
|
|
JSON.stringify({ event: "computer_response", traposSenderId: "7:", messageId: "x", type: "ping", ok: true, payload: {} }),
|
|
);
|
|
assert.equal(socket.sent.length, 0);
|
|
});
|
|
|
|
test("hello missing traposId -> not_authenticated, no close, not registered", async () => {
|
|
const { socket, registry, connection } = setup();
|
|
await connection.onMessage(helloFrame(undefined));
|
|
const frame = socket.lastFrame();
|
|
assert.equal(frame.ok, false);
|
|
assert.deepEqual(frame.error, { code: "not_authenticated", message: "missing traposId" });
|
|
assert.equal(socket.closed, false);
|
|
assert.equal(registry.count(), 0);
|
|
});
|
|
|
|
test("hello bad secret -> unauthorized and close", async () => {
|
|
const { socket, registry, connection } = setup({ secret: "right" });
|
|
await connection.onMessage(helloFrame("7:", "wrong"));
|
|
const frame = socket.lastFrame();
|
|
assert.equal(frame.ok, false);
|
|
assert.deepEqual(frame.error, { code: "unauthorized", message: "invalid secret" });
|
|
assert.equal(socket.closed, true);
|
|
assert.equal(registry.count(), 0);
|
|
});
|
|
|
|
test("hello ok -> registers and replies with accountId", async () => {
|
|
const { socket, registry, connection } = setup();
|
|
await connection.onMessage(helloFrame("7:", "anything"));
|
|
const frame = socket.lastFrame();
|
|
assert.equal(frame.event, "server_response");
|
|
assert.equal(frame.type, "hello");
|
|
assert.equal(frame.ok, true);
|
|
assert.deepEqual(frame.payload, { accountId: "local" });
|
|
assert.equal(registry.has("local", "7:"), true);
|
|
assert.equal(registry.count(), 1);
|
|
});
|
|
|
|
test("post-hello probe_server (ok) -> serverOk + opencodeOk, no detail", async () => {
|
|
const { socket, connection } = setup({ probe: { ok: true } });
|
|
await connection.onMessage(helloFrame("7:"));
|
|
await connection.onMessage(requestFrame("probe_server"));
|
|
const frame = socket.lastFrame();
|
|
assert.equal(frame.ok, true);
|
|
assert.deepEqual(frame.payload, { serverOk: true, opencodeOk: true });
|
|
});
|
|
|
|
test("post-hello probe_server (fail) -> opencodeOk:false + opencodeDetail", async () => {
|
|
const { socket, connection } = setup({ probe: { ok: false, detail: "http://x -> 401" } });
|
|
await connection.onMessage(helloFrame("7:"));
|
|
await connection.onMessage(requestFrame("probe_server"));
|
|
const frame = socket.lastFrame();
|
|
assert.deepEqual(frame.payload, { serverOk: true, opencodeOk: false, opencodeDetail: "http://x -> 401" });
|
|
});
|
|
|
|
test("post-hello unknown type -> unknown_type error", async () => {
|
|
const { socket, connection } = setup();
|
|
await connection.onMessage(helloFrame("7:"));
|
|
await connection.onMessage(requestFrame("does_not_exist"));
|
|
const frame = socket.lastFrame();
|
|
assert.equal(frame.ok, false);
|
|
assert.equal((frame.error as { code: string }).code, "unknown_type");
|
|
});
|
|
|
|
test("post-hello duplicate hello is ignored", async () => {
|
|
const { socket, registry, connection } = setup();
|
|
await connection.onMessage(helloFrame("7:"));
|
|
const afterFirst = socket.sent.length;
|
|
await connection.onMessage(helloFrame("9:"));
|
|
assert.equal(socket.sent.length, afterFirst); // no reply
|
|
assert.equal(registry.has("local", "7:"), true);
|
|
assert.equal(registry.has("local", "9:"), false);
|
|
});
|
|
|
|
test("post-hello wrong-direction frame is dropped", async () => {
|
|
const { socket, connection } = setup();
|
|
await connection.onMessage(helloFrame("7:"));
|
|
const afterHello = socket.sent.length;
|
|
await connection.onMessage(
|
|
JSON.stringify({ event: "server_request", traposReceiverId: "7:", messageId: "g1", type: "ping", payload: {} }),
|
|
);
|
|
assert.equal(socket.sent.length, afterHello);
|
|
});
|
|
|
|
test("reverse direction: gateway request resolved by computer_response", async () => {
|
|
const { socket, registry, connection } = setup();
|
|
await connection.onMessage(helloFrame("7:"));
|
|
|
|
const promise = registry.request("local", "7:", "ping", {});
|
|
const sent = socket.lastFrame();
|
|
assert.equal(sent.event, "server_request");
|
|
assert.equal(sent.type, "ping");
|
|
|
|
await connection.onMessage(
|
|
JSON.stringify({ event: "computer_response", traposSenderId: "7:", messageId: sent.messageId, type: "ping", ok: true, payload: { traposId: "7:" } }),
|
|
);
|
|
assert.deepEqual(await promise, { traposId: "7:" });
|
|
});
|
|
|
|
test("onClose unregisters the connection", async () => {
|
|
const { registry, connection } = setup();
|
|
await connection.onMessage(helloFrame("7:"));
|
|
assert.equal(registry.count(), 1);
|
|
connection.onClose();
|
|
assert.equal(registry.count(), 0);
|
|
});
|
|
|
|
test("malformed frame is dropped silently", async () => {
|
|
const { socket, connection } = setup();
|
|
await connection.onMessage("not json at all");
|
|
assert.equal(socket.sent.length, 0);
|
|
});
|