Files
js-sdk/tests/integration/foundation.integration.test.ts
Tasnim Kabir Sadik 5b3f2741a3 feat: add SDK foundation layer
Implement config resolution, typed errors, HTTP and WebSocket transport
helpers, and timeout handling for the SDK foundation. Add unit and local
integration tests covering the SDK foundation behaviour and align
package exports with the tsup output.
2026-05-09 16:32:41 +06:00

184 lines
5.3 KiB
TypeScript

import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import type { AddressInfo } from "node:net";
import { afterEach, describe, expect, it } from "vitest";
import { WebSocketServer } from "ws";
import { HttpClient } from "../../src/_shared/http.js";
import { WsConnection } from "../../src/_shared/websocket.js";
import { ConflictError, TimeoutError } from "../../src/exceptions.js";
interface CapturedRequest {
method?: string;
url?: string;
headers: IncomingMessage["headers"];
body: string;
}
function readRequestBody(request: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function listen(server: ReturnType<typeof createServer>): Promise<number> {
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
const address = server.address() as AddressInfo;
resolve(address.port);
});
});
}
function closeHttpServer(
server: ReturnType<typeof createServer>,
): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
}
function closeWebSocketServer(server: WebSocketServer): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
}
describe("foundation integration", () => {
const cleanup: Array<() => Promise<void>> = [];
afterEach(async () => {
await Promise.all(cleanup.splice(0).map((close) => close()));
});
it("sends JSON requests through a real HTTP server", async () => {
let captured: CapturedRequest | undefined;
const server = createServer(async (request, response: ServerResponse) => {
captured = {
method: request.method,
url: request.url,
headers: request.headers,
body: await readRequestBody(request),
};
response.writeHead(200, { "Content-Type": "application/json" });
response.end(JSON.stringify({ ok: true }));
});
const port = await listen(server);
cleanup.push(() => closeHttpServer(server));
const client = new HttpClient({
baseUrl: `http://127.0.0.1:${port}`,
apiKey: "api-key",
token: "jwt-token",
hostToken: "host-token",
});
await expect(
client.post(
"/v1/foundation",
{ hello: "world" },
{ params: { page: 1 } },
),
).resolves.toEqual({ ok: true });
expect(captured).toMatchObject({
method: "POST",
url: "/v1/foundation?page=1",
body: JSON.stringify({ hello: "world" }),
});
expect(captured?.headers["content-type"]).toBe("application/json");
expect(captured?.headers.accept).toBe("application/json");
expect(captured?.headers.authorization).toBe("Bearer jwt-token");
expect(captured?.headers["x-api-key"]).toBe("api-key");
expect(captured?.headers["x-host-token"]).toBe("host-token");
});
it("maps real HTTP error responses to SDK errors", async () => {
const server = createServer((_request, response) => {
response.writeHead(409, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
error: { code: "capsule_busy", message: "Capsule is busy" },
}),
);
});
const port = await listen(server);
cleanup.push(() => closeHttpServer(server));
const client = new HttpClient({ baseUrl: `http://127.0.0.1:${port}` });
await expect(client.get("/v1/error")).rejects.toMatchObject({
code: "capsule_busy",
message: "Capsule is busy",
statusCode: 409,
});
await expect(client.get("/v1/error")).rejects.toBeInstanceOf(ConflictError);
});
it("aborts real HTTP requests using timeoutMs", async () => {
const server = createServer((_request, response) => {
setTimeout(() => {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(JSON.stringify({ ok: true }));
}, 200);
});
const port = await listen(server);
cleanup.push(() => closeHttpServer(server));
const client = new HttpClient({ baseUrl: `http://127.0.0.1:${port}` });
await expect(
client.get("/v1/slow", { timeoutMs: 10 }),
).rejects.toBeInstanceOf(TimeoutError);
});
it("exchanges JSON messages through a real WebSocket server", async () => {
const server = new WebSocketServer({ port: 0, host: "127.0.0.1" });
cleanup.push(() => closeWebSocketServer(server));
const receivedByServer = new Promise<unknown>((resolve) => {
server.on("connection", (socket, request) => {
expect(request.headers["x-api-key"]).toBe("api-key");
expect(request.headers["x-host-token"]).toBe("host-token");
socket.on("message", (raw) => resolve(JSON.parse(raw.toString())));
socket.send(JSON.stringify({ type: "ready" }));
});
});
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address() as AddressInfo;
const messages: unknown[] = [];
const connection = await WsConnection.connect({
baseUrl: `http://127.0.0.1:${address.port}`,
path: "/v1/ws",
apiKey: "api-key",
hostToken: "host-token",
onMessage: (message) => messages.push(message),
});
connection.send({ type: "start" });
await expect(receivedByServer).resolves.toEqual({ type: "start" });
expect(messages).toEqual([{ type: "ready" }]);
expect(connection.isClosed).toBe(false);
connection.close();
});
});