mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-08-19 13:43:45 +00:00
Use canonical lowercase UUIDv4 serialization and generate launcher IDs with randomUUID().
165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
import { encodeCbor, encodeClientMessage, encodeFrame } from "@earendil-works/pi-protocol";
|
|
import { afterEach, expect, test } from "vitest";
|
|
import type { ByteConnection, ByteConnectionHandler } from "../src/connection.ts";
|
|
import { PiServer } from "../src/server.ts";
|
|
import { ProtocolTestClient, TestServerHost, type WireChannel } from "../src/testing/index.ts";
|
|
|
|
let server: PiServer | undefined;
|
|
|
|
function connect(): ProtocolTestClient {
|
|
server = new PiServer(new TestServerHost(), { listeners: [], serverId: "00000000-0000-4000-8000-000000000001" });
|
|
let handler: ByteConnectionHandler;
|
|
let client: ProtocolTestClient;
|
|
let closed = false;
|
|
const connection: ByteConnection = {
|
|
get closed() {
|
|
return closed;
|
|
},
|
|
async send(chunk) {
|
|
client.receive(chunk);
|
|
},
|
|
close(finalChunk) {
|
|
if (finalChunk) client.receive(finalChunk);
|
|
closed = true;
|
|
client.markClosed();
|
|
},
|
|
};
|
|
const channel: WireChannel = {
|
|
async send(chunk) {
|
|
handler.onData(chunk);
|
|
},
|
|
async sendFragmented(chunk, splitAt) {
|
|
handler.onData(chunk.subarray(0, splitAt));
|
|
handler.onData(chunk.subarray(splitAt));
|
|
},
|
|
async close() {
|
|
closed = true;
|
|
handler.onClose();
|
|
client.markClosed();
|
|
},
|
|
};
|
|
client = new ProtocolTestClient(channel);
|
|
handler = server.accept(connection);
|
|
return client;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await server?.close();
|
|
server = undefined;
|
|
});
|
|
|
|
test("requires hello as the first message", async () => {
|
|
const client = connect();
|
|
await client.sendMessage({
|
|
type: "request",
|
|
id: "request-1",
|
|
serverId: "00000000-0000-4000-8000-000000000001",
|
|
call: { method: "list", args: [] },
|
|
});
|
|
await expect(client.next((message) => message.type === "hello_error")).resolves.toMatchObject({
|
|
type: "hello_error",
|
|
error: { code: "invalid_request" },
|
|
});
|
|
await client.waitForClose();
|
|
});
|
|
|
|
test("rejects unsupported protocol versions", async () => {
|
|
const client = connect();
|
|
await expect(client.hello(2)).resolves.toMatchObject({ type: "hello_error", error: { code: "version" } });
|
|
await client.waitForClose();
|
|
});
|
|
|
|
test("accepts fragmented hello and request frames", async () => {
|
|
const client = connect();
|
|
const hello = encodeClientMessage({ type: "hello", version: 1 });
|
|
const helloResponse = client.next((message) => message.type === "hello");
|
|
await client.sendFragmentedMessage({ type: "hello", version: 1 }, Math.floor(hello.byteLength / 2));
|
|
await expect(helloResponse).resolves.toMatchObject({
|
|
type: "hello",
|
|
serverId: "00000000-0000-4000-8000-000000000001",
|
|
});
|
|
|
|
const response = client.next((message) => message.type === "response");
|
|
const request = {
|
|
type: "request" as const,
|
|
id: "request-1",
|
|
serverId: "00000000-0000-4000-8000-000000000001",
|
|
call: { method: "list" as const, args: [] as [] },
|
|
};
|
|
const frame = encodeClientMessage(request);
|
|
await client.sendFragmentedMessage(request, Math.floor(frame.byteLength / 2));
|
|
await expect(response).resolves.toMatchObject({ ok: true, result: [] });
|
|
});
|
|
|
|
test.each([
|
|
["malformed CBOR", encodeFrame(Uint8Array.of(0xff))],
|
|
["schema-invalid CBOR", encodeFrame(encodeCbor({ type: "hello", version: 1, extra: true }))],
|
|
["oversized frame", new Uint8Array([1, 0, 0, 1])],
|
|
] as const)("rejects hostile framed input: %s", async (_label, bytes) => {
|
|
const client = connect();
|
|
await client.sendBytes(bytes);
|
|
await expect(client.next((message) => message.type === "hello_error")).resolves.toMatchObject({
|
|
type: "hello_error",
|
|
error: { code: "invalid_request" },
|
|
});
|
|
await client.waitForClose();
|
|
});
|
|
|
|
test("rejects a second hello after completing the handshake", async () => {
|
|
const client = connect();
|
|
await client.hello();
|
|
await client.sendMessage({ type: "hello", version: 1 });
|
|
await expect(client.next((message) => message.type === "hello_error")).resolves.toMatchObject({
|
|
type: "hello_error",
|
|
error: { code: "invalid_request", message: expect.stringMatching(/first message/) },
|
|
});
|
|
await client.waitForClose();
|
|
});
|
|
|
|
test("processes a hello and request coalesced in one byte chunk", async () => {
|
|
const client = connect();
|
|
const hello = encodeClientMessage({ type: "hello", version: 1 });
|
|
const request = encodeClientMessage({
|
|
type: "request",
|
|
id: "request-1",
|
|
serverId: "00000000-0000-4000-8000-000000000001",
|
|
call: { method: "list", args: [] },
|
|
});
|
|
const wire = new Uint8Array(hello.byteLength + request.byteLength);
|
|
wire.set(hello);
|
|
wire.set(request, hello.byteLength);
|
|
|
|
await client.sendBytes(wire);
|
|
await expect(client.next((message) => message.type === "hello")).resolves.toMatchObject({ type: "hello" });
|
|
await expect(client.next((message) => message.type === "response")).resolves.toMatchObject({
|
|
type: "response",
|
|
id: "request-1",
|
|
ok: true,
|
|
result: [],
|
|
});
|
|
});
|
|
|
|
test("reports a truncated final frame when the peer closes", async () => {
|
|
const errors: Error[] = [];
|
|
server = new PiServer(new TestServerHost(), {
|
|
listeners: [],
|
|
serverId: "00000000-0000-4000-8000-000000000001",
|
|
onError: (error) => errors.push(error),
|
|
});
|
|
let closed = false;
|
|
const connection: ByteConnection = {
|
|
get closed() {
|
|
return closed;
|
|
},
|
|
async send() {},
|
|
close() {
|
|
closed = true;
|
|
},
|
|
};
|
|
const handler: ByteConnectionHandler = server.accept(connection);
|
|
handler.onData(new Uint8Array([0, 0, 0, 2, 1]));
|
|
handler.onClose();
|
|
|
|
expect(closed).toBe(false);
|
|
expect(errors).toEqual([expect.objectContaining({ message: expect.stringMatching(/truncated/i) })]);
|
|
});
|