164 lines
4.5 KiB
TypeScript
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"]);
|
|
});
|
|
});
|