runa/vscode/test/client.test.ts

181 lines
7.0 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { once } from "node:events";
import test from "node:test";
import { ImageClient, parseEndpoint } from "../src/client";
import { MAX_LINE_BYTES, ProtocolError } from "../src/protocol";
import { actor, fakeImage, json, REF, response } from "./fixtures";
test("only numeric loopback endpoints and valid ports are accepted", () => {
assert.deepEqual(parseEndpoint("127.0.0.1:3737"), { host: "127.0.0.1", port: 3737 });
assert.deepEqual(parseEndpoint("127.2.3.4:1"), { host: "127.2.3.4", port: 1 });
assert.deepEqual(parseEndpoint("[::1]:65535"), { host: "::1", port: 65535 });
for (const input of [
"localhost:3737",
"0.0.0.0:3737",
"192.168.1.1:3737",
"[::]:3737",
"[::ffff:127.0.0.1]:3737",
"example.org:3737",
"127.0.0.1:0",
"127.0.0.1:65536",
"127.0.0.1:-1",
"127.0.0.1:1/path",
]) {
assert.throws(() => parseEndpoint(input));
}
});
test("fragmented UTF-8 replies and simultaneous reads preserve correlation and values", async (t) => {
const operations: string[] = [];
const address = await fakeImage(t, (request, socket) => {
operations.push(request.command.operation);
assert.equal(request.version, 3);
const reply =
request.command.operation === "list_actors"
? { kind: "actors", data: [actor()] }
: { kind: "actor", data: actor() };
const bytes = Buffer.from(response(request, reply));
const split = bytes.indexOf(Buffer.from("雪")) + 1;
socket.write(bytes.subarray(0, split));
setImmediate(() => socket.write(bytes.subarray(split)));
});
const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined));
t.after(() => client.close());
const [actors, inspected] = await Promise.all([client.listActors(), client.inspectActor(REF)]);
assert.deepEqual(actors, [actor()]);
assert.deepEqual(inspected, actor());
assert.deepEqual(operations, ["list_actors", "inspect_actor"]);
});
test("protocol errors preserve the connection and are never retried", async (t) => {
let requests = 0;
const address = await fakeImage(t, (request, socket) => {
requests++;
socket.write(
requests === 1
? json({
version: 3,
request_id: request.request_id,
result: { Err: { code: "not_found", message: "Missing actor" } },
}) + "\n"
: response(request, { kind: "actors", data: [] }),
);
});
const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined));
t.after(() => client.close());
await assert.rejects(client.inspectActor(REF), ProtocolError);
assert.deepEqual(await client.listActors(), []);
assert.equal(requests, 2);
});
test("bad correlation, malformed JSON/UTF-8, oversize, truncation, and wrong versions close the stream", async (t) => {
for (const mode of ["correlation", "json", "utf8", "size", "truncated", "version", "kind"]) {
await t.test(mode, async (t) => {
let requests = 0;
const failures: Error[] = [];
const address = await fakeImage(t, (request, socket) => {
requests++;
if (mode === "json") socket.write("{broken}\n");
else if (mode === "utf8") socket.write(Buffer.from([0xff, 10]));
else if (mode === "size") socket.write(Buffer.alloc(MAX_LINE_BYTES + 1, 32));
else if (mode === "truncated") socket.end('{"version":3');
else
socket.write(
json({
version: mode === "version" ? 2 : 3,
request_id: mode === "correlation" ? "wrong" : request.request_id,
result: { Ok: { kind: mode === "kind" ? "acknowledged" : "actors", data: [] } },
}) + "\n",
);
});
const client = await ImageClient.connect(address, (error) => {
if (error) failures.push(error);
});
t.after(() => client.close());
await assert.rejects(client.listActors());
await assert.rejects(client.listActors(), /Not connected/);
assert.equal(requests, 1);
assert.equal(failures.length, 1);
});
}
});
test("request timeout rejects pending and queued reads without retry", async (t) => {
let requests = 0;
const address = await fakeImage(t, () => {
requests++;
});
const client = await ImageClient.connect(address, () => {}, { connect: 1000, request: 30 });
t.after(() => client.close());
await Promise.all([
assert.rejects(client.listActors(), /timed out/),
assert.rejects(client.inspectActor(REF), /Not connected/),
]);
assert.equal(requests, 1);
});
test("the 1 MiB response limit is inclusive and framing survives the next request", async (t) => {
const address = await fakeImage(t, (request, socket) => {
const line = response(request, { kind: "actors", data: [] }).slice(0, -1);
socket.write(line + " ".repeat(MAX_LINE_BYTES - Buffer.byteLength(line)) + "\n");
});
const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined));
t.after(() => client.close());
assert.deepEqual(await client.listActors(), []);
assert.deepEqual(await client.listActors(), []);
});
test("unsolicited partial frames invalidate the connection", async (t) => {
const failures: Error[] = [];
const address = await fakeImage(t, (request, socket) => {
socket.write(response(request, { kind: "actors", data: [] }) + "{");
});
const client = await ImageClient.connect(address, (error) => {
if (error) failures.push(error);
});
t.after(() => client.close());
assert.deepEqual(await client.listActors(), []);
await assert.rejects(client.listActors(), /Not connected/);
assert.match(failures[0]?.message ?? "", /unsolicited/);
});
test("disconnect rejects pending reads and idle peer close is reported", async (t) => {
const address = await fakeImage(t, (_request, socket) => socket.end());
let disconnected = false;
const client = await ImageClient.connect(address, (error) => {
assert.match(error?.message ?? "", /closed/);
disconnected = true;
});
t.after(() => client.close());
await assert.rejects(client.listActors(), /closed/);
assert.equal(disconnected, true);
const idleAddress = await fakeImage(t, (request, socket) => {
socket.write(response(request, { kind: "actors", data: [] }));
setImmediate(() => socket.end());
});
let onClosed = () => {};
const closed = new Promise<void>((resolve) => {
onClosed = resolve;
});
const idle = await ImageClient.connect(idleAddress, () => onClosed());
t.after(() => idle.close());
await idle.listActors();
await closed;
await assert.rejects(idle.listActors(), /Not connected/);
});
test("failed connection reports the error", async () => {
const { createServer } = await import("node:net");
const server = createServer();
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") throw new Error("Missing address");
await new Promise<void>((resolve) => server.close(() => resolve()));
await assert.rejects(
ImageClient.connect(`127.0.0.1:${address.port}`, () => {}),
/ECONNREFUSED/,
);
});