Files
kuber/tests/lib/exec-api.test.ts
T
2026-09-03 11:28:30 +07:00

164 lines
4.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { openExecSession, type ExecWebSocket } from "../../lib/exec-api";
class FakeSocket implements ExecWebSocket {
binaryType: "arraybuffer" | "blob" = "blob";
readyState = 0;
sent: string[] = [];
closes: Array<[number | undefined, string | undefined]> = [];
private listeners = new Map<string, Set<(event: any) => void>>();
send(data: string) {
this.sent.push(data);
}
close(code?: number, reason?: string) {
this.closes.push([code, reason]);
this.readyState = 3;
}
addEventListener(type: string, listener: (event: any) => void) {
let values = this.listeners.get(type);
if (!values) {
values = new Set();
this.listeners.set(type, values);
}
values.add(listener);
}
removeEventListener(type: string, listener: (event: any) => void) {
this.listeners.get(type)?.delete(listener);
}
emit(type: string, event: any = {}) {
if (type === "open") this.readyState = 1;
for (const listener of [...(this.listeners.get(type) ?? [])])
listener(event);
}
}
const session = {
token: "secret-token",
expiresAt: "2099-01-01T00:00:00.000Z",
user: { username: "user", roles: [] },
};
describe("exec WebSocket API", () => {
test("authenticates the upgrade and maps protocol byte frames", async () => {
const socket = new FakeSocket();
let connection:
| { url: string; headers: Readonly<Record<string, string>> }
| undefined;
const opening = openExecSession(
"shop/demo",
{
deployment: "api",
command: ["sh", "-c", "echo ok"],
tty: true,
columns: 120,
rows: 40,
},
new AbortController().signal,
{
baseUrl: "https://api.test/api/v2",
session,
socketFactory(url, headers) {
connection = { url, headers };
return socket;
},
},
);
socket.emit("open");
const client = await opening;
expect(connection).toEqual({
url: "wss://api.test/api/v2/workspaces/shop%2Fdemo/exec",
headers: {
authorization: "Bearer secret-token",
},
});
expect(JSON.parse(socket.sent[0]!)).toEqual({
type: "start",
version: 1,
deployment: "api",
command: ["sh", "-c", "echo ok"],
tty: true,
columns: 120,
rows: 40,
});
client.sendStdin(new TextEncoder().encode("hello"), true);
client.resize(80, 24);
const iterator = client[Symbol.asyncIterator]();
socket.emit("message", {
data: JSON.stringify({
type: "stdout",
data: "b3V0",
encoding: "base64",
}),
});
expect(await iterator.next()).toEqual({
done: false,
value: { type: "stdout", data: new TextEncoder().encode("out") },
});
expect(JSON.parse(socket.sent[1]!)).toEqual({
type: "stdin",
data: "aGVsbG8=",
encoding: "base64",
eof: true,
});
expect(JSON.parse(socket.sent[2]!)).toEqual({
type: "resize",
columns: 80,
rows: 24,
});
socket.emit("message", {
data: JSON.stringify({ type: "exit", exitCode: 7 }),
});
expect((await iterator.next()).value).toEqual({
type: "exit",
exitCode: 7,
});
socket.emit("close");
expect((await iterator.next()).done).toBe(true);
});
test("closes an active connection when cancelled", async () => {
const socket = new FakeSocket();
const controller = new AbortController();
const opening = openExecSession(
"shop",
{ deployment: "api", command: ["sh"], tty: false },
controller.signal,
{ session, socketFactory: () => socket },
);
socket.emit("open");
const client = await opening;
const next = client[Symbol.asyncIterator]().next();
controller.abort();
expect((await next).done).toBe(true);
expect(socket.closes).toContainEqual([1000, "aborted"]);
});
test("rejects malformed server frames and closes with protocol error", async () => {
const socket = new FakeSocket();
const opening = openExecSession(
"shop",
{ deployment: "api", command: ["sh"], tty: false },
new AbortController().signal,
{ session, socketFactory: () => socket },
);
socket.emit("open");
const client = await opening;
const next = client[Symbol.asyncIterator]().next();
socket.emit("message", {
data: JSON.stringify({ type: "stdout", data: 1 }),
});
await expect(next).rejects.toThrow("Invalid exec output frame");
expect(socket.closes).toContainEqual([1002, "invalid frame"]);
});
});