138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { DISPLACED_CLOSE_CODE, GatewayRegistry } from "../src/modules/trapos-cloud-gateway/registry.js";
|
|
import { GatewayError, type ComputerResponse } from "../src/modules/trapos-cloud-gateway/protocol.js";
|
|
import { FakeSocket, silentLog } from "./helpers.js";
|
|
|
|
function response(messageId: string, ok: boolean, payload: Record<string, unknown> = {}, error?: { code: string; message?: string }): ComputerResponse {
|
|
return { event: "computer_response", traposSenderId: "7:", messageId, type: "ping", ok, payload, error };
|
|
}
|
|
|
|
function sentMessageId(socket: FakeSocket): string {
|
|
return socket.lastFrame().messageId as string;
|
|
}
|
|
|
|
test("register / has / count / unregister", () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
assert.equal(reg.has("local", "7:"), true);
|
|
assert.equal(reg.count(), 1);
|
|
reg.unregister("local", "7:", a);
|
|
assert.equal(reg.has("local", "7:"), false);
|
|
assert.equal(reg.count(), 0);
|
|
});
|
|
|
|
test("newest-wins evicts previous socket for same key", () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
const b = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
reg.register("local", "7:", b);
|
|
assert.equal(a.closed, true);
|
|
// Distinct close code lets the displaced Lua client yield instead of reconnect-looping.
|
|
assert.equal(a.closeCode, DISPLACED_CLOSE_CODE);
|
|
assert.equal(b.closed, false);
|
|
assert.equal(reg.count(), 1);
|
|
});
|
|
|
|
test("same traposId under different accounts coexist", () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
reg.register("local", "7:", new FakeSocket());
|
|
reg.register("other", "7:", new FakeSocket());
|
|
assert.equal(reg.count(), 2);
|
|
});
|
|
|
|
test("request sends a server_request frame and resolves on matching ok response", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
|
|
const promise = reg.request("local", "7:", "ping", {});
|
|
const frame = a.lastFrame();
|
|
assert.equal(frame.event, "server_request");
|
|
assert.equal(frame.type, "ping");
|
|
assert.equal(frame.traposReceiverId, "7:");
|
|
|
|
reg.resolveResponse(a, "local", "7:", response(frame.messageId as string, true, { traposId: "7:" }));
|
|
assert.deepEqual(await promise, { traposId: "7:" });
|
|
});
|
|
|
|
test("request rejects with the response error code on ok:false", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
|
|
const promise = reg.request("local", "7:", "ping", {});
|
|
reg.resolveResponse(a, "local", "7:", response(sentMessageId(a), false, {}, { code: "unknown_type", message: "nope" }));
|
|
|
|
await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "unknown_type");
|
|
});
|
|
|
|
test("request rejects not_connected when no socket registered", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
await assert.rejects(
|
|
reg.request("local", "nobody", "ping", {}),
|
|
(err: unknown) => err instanceof GatewayError && err.code === "not_connected",
|
|
);
|
|
});
|
|
|
|
test("request rejects timeout after the deadline", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
reg.register("local", "7:", new FakeSocket());
|
|
await assert.rejects(
|
|
reg.request("local", "7:", "ping", {}, 10),
|
|
(err: unknown) => err instanceof GatewayError && err.code === "timeout",
|
|
);
|
|
});
|
|
|
|
test("messageId correlation: concurrent requests resolve independently", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
|
|
const p1 = reg.request("local", "7:", "ping", { n: 1 });
|
|
const id1 = a.lastFrame().messageId as string;
|
|
const p2 = reg.request("local", "7:", "ping", { n: 2 });
|
|
const id2 = a.lastFrame().messageId as string;
|
|
assert.notEqual(id1, id2);
|
|
|
|
// resolve out of order
|
|
reg.resolveResponse(a, "local", "7:", response(id2, true, { n: 2 }));
|
|
reg.resolveResponse(a, "local", "7:", response(id1, true, { n: 1 }));
|
|
assert.deepEqual(await p1, { n: 1 });
|
|
assert.deepEqual(await p2, { n: 2 });
|
|
});
|
|
|
|
test("pending rejected on unregister (close)", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
const promise = reg.request("local", "7:", "ping", {});
|
|
reg.unregister("local", "7:", a);
|
|
await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected");
|
|
});
|
|
|
|
test("eviction: old socket's pending rejected and late unregister(A) does not evict B", async () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
const b = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
const promise = reg.request("local", "7:", "ping", {});
|
|
|
|
reg.register("local", "7:", b); // evict A
|
|
await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected");
|
|
|
|
reg.unregister("local", "7:", a); // late close from evicted A — must be a no-op
|
|
assert.equal(reg.has("local", "7:"), true);
|
|
assert.equal(reg.count(), 1);
|
|
});
|
|
|
|
test("resolveResponse with no matching pending is a silent no-op", () => {
|
|
const reg = new GatewayRegistry(silentLog);
|
|
const a = new FakeSocket();
|
|
reg.register("local", "7:", a);
|
|
// should not throw
|
|
reg.resolveResponse(a, "local", "7:", response("unknown-id", true, {}));
|
|
});
|