cc-libs/tools/trapos-server/test/registry.test.ts

138 lines
5.4 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("global", "7:", a);
assert.equal(reg.has("global", "7:"), true);
assert.equal(reg.count(), 1);
reg.unregister("global", "7:", a);
assert.equal(reg.has("global", "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("global", "7:", a);
reg.register("global", "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 networks coexist", () => {
const reg = new GatewayRegistry(silentLog);
reg.register("global", "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("global", "7:", a);
const promise = reg.request("global", "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, "global", "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("global", "7:", a);
const promise = reg.request("global", "7:", "ping", {});
reg.resolveResponse(a, "global", "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("global", "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("global", "7:", new FakeSocket());
await assert.rejects(
reg.request("global", "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("global", "7:", a);
const p1 = reg.request("global", "7:", "ping", { n: 1 });
const id1 = a.lastFrame().messageId as string;
const p2 = reg.request("global", "7:", "ping", { n: 2 });
const id2 = a.lastFrame().messageId as string;
assert.notEqual(id1, id2);
// resolve out of order
reg.resolveResponse(a, "global", "7:", response(id2, true, { n: 2 }));
reg.resolveResponse(a, "global", "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("global", "7:", a);
const promise = reg.request("global", "7:", "ping", {});
reg.unregister("global", "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("global", "7:", a);
const promise = reg.request("global", "7:", "ping", {});
reg.register("global", "7:", b); // evict A
await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected");
reg.unregister("global", "7:", a); // late close from evicted A — must be a no-op
assert.equal(reg.has("global", "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("global", "7:", a);
// should not throw
reg.resolveResponse(a, "global", "7:", response("unknown-id", true, {}));
});