Compare commits
2 Commits
5b3f2741a3
...
52282618bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 52282618bd | |||
| f1522eaa0b |
@ -29,6 +29,8 @@ export interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
/** Return response text instead of parsing JSON. */
|
||||
asText?: boolean;
|
||||
/** Fetch redirect behavior. Defaults to the runtime fetch default. */
|
||||
redirect?: RequestRedirect;
|
||||
}
|
||||
|
||||
/** Thin `fetch` wrapper with Wrenn authentication and error mapping. */
|
||||
@ -137,6 +139,29 @@ export class HttpClient {
|
||||
return res.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and returns the raw `Response` object.
|
||||
*
|
||||
* This is intended for endpoints that intentionally return non-JSON responses,
|
||||
* such as OAuth redirects. Unlike {@link request}, this method does not map
|
||||
* non-OK statuses to SDK errors.
|
||||
*
|
||||
* @param method - HTTP method to use.
|
||||
* @param path - API path relative to the configured base URL.
|
||||
* @param body - Optional JSON request body.
|
||||
* @param opts - Optional query, header, auth, timeout, and redirect settings.
|
||||
* @returns Raw fetch response.
|
||||
* @throws TimeoutError When the configured timeout aborts the request.
|
||||
*/
|
||||
response(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
opts?: RequestOptions,
|
||||
): Promise<Response> {
|
||||
return this.rawRequest(method, path, body, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and parses the response as JSON unless `asText` is set.
|
||||
*
|
||||
@ -188,6 +213,7 @@ export class HttpClient {
|
||||
method,
|
||||
headers: { ...headers, ...opts?.headers },
|
||||
};
|
||||
if (opts?.redirect) init.redirect = opts.redirect;
|
||||
|
||||
if (body !== undefined) {
|
||||
init.body = JSON.stringify(body);
|
||||
|
||||
1412
src/client.ts
Normal file
1412
src/client.ts
Normal file
File diff suppressed because it is too large
Load Diff
20
src/index.ts
20
src/index.ts
@ -2,6 +2,26 @@ export type { HttpClientConfig, RequestOptions } from "./_shared/http.js";
|
||||
export { HttpClient } from "./_shared/http.js";
|
||||
export type { WsConnectionOpts } from "./_shared/websocket.js";
|
||||
export { WsConnection } from "./_shared/websocket.js";
|
||||
export type {
|
||||
FileUploadInput,
|
||||
OperationJsonBody,
|
||||
OperationJsonResponse,
|
||||
OperationQueryParams,
|
||||
OperationRequestOptions,
|
||||
} from "./client.js";
|
||||
export {
|
||||
AccountResource,
|
||||
APIKeysResource,
|
||||
AuthResource,
|
||||
CapsulesResource,
|
||||
ChannelsResource,
|
||||
FilesResource,
|
||||
HostsResource,
|
||||
SnapshotsResource,
|
||||
TeamsResource,
|
||||
UsersResource,
|
||||
WrennClient,
|
||||
} from "./client.js";
|
||||
export type { ClientConfig, ResolvedClientConfig } from "./config.js";
|
||||
export {
|
||||
DEFAULT_BASE_URL,
|
||||
|
||||
492
tests/client.test.ts
Normal file
492
tests/client.test.ts
Normal file
@ -0,0 +1,492 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { WrennClient } from "../src/client.js";
|
||||
|
||||
interface CapturedRequest {
|
||||
url: string;
|
||||
init: RequestInit;
|
||||
}
|
||||
|
||||
function setupFetch(status = 200, body: unknown = { ok: true }) {
|
||||
const calls: CapturedRequest[] = [];
|
||||
const fetchMock = vi.fn(
|
||||
async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({ url: String(url), init: init ?? {} });
|
||||
if (status === 204) return new Response(null, { status });
|
||||
return Response.json(body, { status });
|
||||
},
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return { calls, fetchMock };
|
||||
}
|
||||
|
||||
function expectLastCall(
|
||||
calls: CapturedRequest[],
|
||||
expected: { method: string; url: string; body?: unknown },
|
||||
) {
|
||||
const call = calls.at(-1);
|
||||
expect(call?.url).toBe(expected.url);
|
||||
expect(call?.init.method).toBe(expected.method);
|
||||
if (expected.body !== undefined) {
|
||||
expect(call?.init.body).toBe(JSON.stringify(expected.body));
|
||||
}
|
||||
}
|
||||
|
||||
describe("WrennClient", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("initializes every resource with resolved auth headers", async () => {
|
||||
const { calls } = setupFetch();
|
||||
const client = new WrennClient({
|
||||
apiKey: "api-key",
|
||||
baseUrl: "https://api.example.com/",
|
||||
hostToken: "host-token",
|
||||
token: "jwt-token",
|
||||
});
|
||||
|
||||
await client.auth.login({ email: "a@example.com", password: "password" });
|
||||
|
||||
expect(client.account).toBeDefined();
|
||||
expect(client.apiKeys).toBeDefined();
|
||||
expect(client.users).toBeDefined();
|
||||
expect(client.teams).toBeDefined();
|
||||
expect(client.capsules).toBeDefined();
|
||||
expect(client.files).toBeDefined();
|
||||
expect(client.snapshots).toBeDefined();
|
||||
expect(client.hosts).toBeDefined();
|
||||
expect(client.channels).toBeDefined();
|
||||
expect(calls.at(-1)?.init.headers).toMatchObject({
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer jwt-token",
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "api-key",
|
||||
"X-Host-Token": "host-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps auth endpoints", async () => {
|
||||
const { calls } = setupFetch();
|
||||
const client = new WrennClient({ baseUrl: "https://api.example.com" });
|
||||
|
||||
await client.auth.signup({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/auth/signup",
|
||||
});
|
||||
|
||||
await client.auth.activate({ token: "activation-token" });
|
||||
expectLastCall(calls, {
|
||||
body: { token: "activation-token" },
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/auth/activate",
|
||||
});
|
||||
|
||||
await client.auth.login({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/auth/login",
|
||||
});
|
||||
|
||||
await client.auth.oauthRedirect("github", { redirect: "manual" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/auth/oauth/github",
|
||||
});
|
||||
expect(calls.at(-1)?.init.redirect).toBe("manual");
|
||||
|
||||
await client.auth.oauthCallback("github", { code: "code", state: "state" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/auth/oauth/github/callback?code=code&state=state",
|
||||
});
|
||||
|
||||
await client.auth.switchTeam({ team_id: "team_1" });
|
||||
expectLastCall(calls, {
|
||||
body: { team_id: "team_1" },
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/auth/switch-team",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps account, API key, user, and team endpoints", async () => {
|
||||
const { calls } = setupFetch();
|
||||
const client = new WrennClient({ baseUrl: "https://api.example.com" });
|
||||
|
||||
await client.account.getMe();
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/me",
|
||||
});
|
||||
await client.account.updateName({ name: "New Name" });
|
||||
expectLastCall(calls, {
|
||||
body: { name: "New Name" },
|
||||
method: "PATCH",
|
||||
url: "https://api.example.com/v1/me",
|
||||
});
|
||||
await client.account.deleteAccount({ confirmation: "a@example.com" });
|
||||
expectLastCall(calls, {
|
||||
body: { confirmation: "a@example.com" },
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/me",
|
||||
});
|
||||
await client.account.changePassword({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/me/password",
|
||||
});
|
||||
await client.account.requestPasswordReset({ email: "a@example.com" });
|
||||
expectLastCall(calls, {
|
||||
body: { email: "a@example.com" },
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/me/password/reset",
|
||||
});
|
||||
await client.account.confirmPasswordReset({
|
||||
new_password: "password",
|
||||
token: "token",
|
||||
});
|
||||
expectLastCall(calls, {
|
||||
body: { new_password: "password", token: "token" },
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/me/password/reset/confirm",
|
||||
});
|
||||
await client.account.connectProvider("github");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/me/providers/github/connect",
|
||||
});
|
||||
await client.account.disconnectProvider("github");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/me/providers/github",
|
||||
});
|
||||
|
||||
await client.apiKeys.list();
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/api-keys",
|
||||
});
|
||||
await client.apiKeys.create({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/api-keys",
|
||||
});
|
||||
await client.apiKeys.delete("key/1");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/api-keys/key%2F1",
|
||||
});
|
||||
|
||||
await client.users.search({ email: "alice@" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/users/search?email=alice%40",
|
||||
});
|
||||
|
||||
await client.teams.list();
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/teams",
|
||||
});
|
||||
await client.teams.create({ name: "Team" });
|
||||
expectLastCall(calls, {
|
||||
body: { name: "Team" },
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/teams",
|
||||
});
|
||||
await client.teams.get("team/1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/teams/team%2F1",
|
||||
});
|
||||
await client.teams.rename("team_1", { name: "New" });
|
||||
expectLastCall(calls, {
|
||||
body: { name: "New" },
|
||||
method: "PATCH",
|
||||
url: "https://api.example.com/v1/teams/team_1",
|
||||
});
|
||||
await client.teams.delete("team_1");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/teams/team_1",
|
||||
});
|
||||
await client.teams.listMembers("team_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/teams/team_1/members",
|
||||
});
|
||||
await client.teams.addMember("team_1", { email: "a@example.com" });
|
||||
expectLastCall(calls, {
|
||||
body: { email: "a@example.com" },
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/teams/team_1/members",
|
||||
});
|
||||
await client.teams.updateMemberRole("team_1", "user_1", { role: "admin" });
|
||||
expectLastCall(calls, {
|
||||
body: { role: "admin" },
|
||||
method: "PATCH",
|
||||
url: "https://api.example.com/v1/teams/team_1/members/user_1",
|
||||
});
|
||||
await client.teams.removeMember("team_1", "user_1");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/teams/team_1/members/user_1",
|
||||
});
|
||||
await client.teams.leave("team_1");
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/teams/team_1/leave",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps capsule, file, and snapshot endpoints", async () => {
|
||||
const { calls } = setupFetch();
|
||||
const client = new WrennClient({ baseUrl: "https://api.example.com" });
|
||||
|
||||
await client.capsules.create({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules",
|
||||
});
|
||||
await client.capsules.list();
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/capsules",
|
||||
});
|
||||
await client.capsules.get("cap_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/capsules/cap_1",
|
||||
});
|
||||
await client.capsules.destroy("cap_1");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/capsules/cap_1",
|
||||
});
|
||||
await client.capsules.exec("cap_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/exec",
|
||||
});
|
||||
await client.capsules.listProcesses("cap_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/processes",
|
||||
});
|
||||
await client.capsules.killProcess("cap_1", "pid/1", { signal: "SIGTERM" });
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/processes/pid%2F1?signal=SIGTERM",
|
||||
});
|
||||
await client.capsules.ping("cap_1");
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/ping",
|
||||
});
|
||||
await client.capsules.metrics("cap_1", { range: "10m" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/metrics?range=10m",
|
||||
});
|
||||
await client.capsules.pause("cap_1");
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/pause",
|
||||
});
|
||||
await client.capsules.resume("cap_1");
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/resume",
|
||||
});
|
||||
await client.capsules.stats({ range: "1h" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/capsules/stats?range=1h",
|
||||
});
|
||||
await client.capsules.usage({ from: "2026-01-01", to: "2026-01-02" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/capsules/usage?from=2026-01-01&to=2026-01-02",
|
||||
});
|
||||
|
||||
await client.files.upload("cap_1", { file: "hello", path: "/tmp/a.txt" });
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/write",
|
||||
});
|
||||
expect(calls.at(-1)?.init.body).toBeInstanceOf(FormData);
|
||||
await client.files.download("cap_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/read",
|
||||
});
|
||||
await client.files.list("cap_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/list",
|
||||
});
|
||||
await client.files.mkdir("cap_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/mkdir",
|
||||
});
|
||||
await client.files.remove("cap_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/remove",
|
||||
});
|
||||
await client.files.streamUpload("cap_1", {
|
||||
file: "hello",
|
||||
path: "/tmp/a.txt",
|
||||
});
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/stream/write",
|
||||
});
|
||||
await client.files.streamDownload("cap_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/capsules/cap_1/files/stream/read",
|
||||
});
|
||||
|
||||
await client.snapshots.create({} as never, { overwrite: "true" });
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/snapshots?overwrite=true",
|
||||
});
|
||||
await client.snapshots.list({ type: "base" });
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/snapshots?type=base",
|
||||
});
|
||||
await client.snapshots.delete("snap/1");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/snapshots/snap%2F1",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps host and channel endpoints", async () => {
|
||||
const { calls } = setupFetch();
|
||||
const client = new WrennClient({ baseUrl: "https://api.example.com" });
|
||||
|
||||
await client.hosts.create({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/hosts",
|
||||
});
|
||||
await client.hosts.list();
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/hosts",
|
||||
});
|
||||
await client.hosts.get("host_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/hosts/host_1",
|
||||
});
|
||||
await client.hosts.delete("host_1", { force: true });
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/hosts/host_1?force=true",
|
||||
});
|
||||
await client.hosts.regenerateToken("host_1");
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/hosts/host_1/token",
|
||||
});
|
||||
await client.hosts.register({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/hosts/register",
|
||||
});
|
||||
await client.hosts.heartbeat("host_1");
|
||||
expectLastCall(calls, {
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/hosts/host_1/heartbeat",
|
||||
});
|
||||
await client.hosts.refreshToken({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/hosts/auth/refresh",
|
||||
});
|
||||
await client.hosts.deletePreview("host_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/hosts/host_1/delete-preview",
|
||||
});
|
||||
await client.hosts.listTags("host_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/hosts/host_1/tags",
|
||||
});
|
||||
await client.hosts.addTag("host_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/hosts/host_1/tags",
|
||||
});
|
||||
await client.hosts.removeTag("host_1", "gpu/a");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/hosts/host_1/tags/gpu%2Fa",
|
||||
});
|
||||
|
||||
await client.channels.create({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/channels",
|
||||
});
|
||||
await client.channels.list();
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/channels",
|
||||
});
|
||||
await client.channels.test({} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "POST",
|
||||
url: "https://api.example.com/v1/channels/test",
|
||||
});
|
||||
await client.channels.get("channel_1");
|
||||
expectLastCall(calls, {
|
||||
method: "GET",
|
||||
url: "https://api.example.com/v1/channels/channel_1",
|
||||
});
|
||||
await client.channels.update("channel_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "PATCH",
|
||||
url: "https://api.example.com/v1/channels/channel_1",
|
||||
});
|
||||
await client.channels.delete("channel_1");
|
||||
expectLastCall(calls, {
|
||||
method: "DELETE",
|
||||
url: "https://api.example.com/v1/channels/channel_1",
|
||||
});
|
||||
await client.channels.rotateConfig("channel_1", {} as never);
|
||||
expectLastCall(calls, {
|
||||
body: {},
|
||||
method: "PUT",
|
||||
url: "https://api.example.com/v1/channels/channel_1/config",
|
||||
});
|
||||
});
|
||||
});
|
||||
85
tests/integration/client.integration.test.ts
Normal file
85
tests/integration/client.integration.test.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import { WrennClient } from "../../src/client.js";
|
||||
import { DEFAULT_BASE_URL } from "../../src/config.js";
|
||||
|
||||
const baseUrl = process.env.WRENN_BASE_URL ?? DEFAULT_BASE_URL;
|
||||
const apiKey = process.env.WRENN_API_KEY;
|
||||
const testEmail = process.env.WRENN_TEST_EMAIL;
|
||||
const testPassword = process.env.WRENN_TEST_PASS;
|
||||
|
||||
const describeWithApiKey = apiKey ? describe : describe.skip;
|
||||
const describeWithLogin = testEmail && testPassword ? describe : describe.skip;
|
||||
|
||||
describeWithApiKey("WrennClient live API key integration", () => {
|
||||
const client = new WrennClient({ apiKey, baseUrl });
|
||||
|
||||
it("lists capsules from the real Wrenn API", async () => {
|
||||
const capsules = await client.capsules.list();
|
||||
|
||||
expect(Array.isArray(capsules)).toBe(true);
|
||||
});
|
||||
|
||||
it("lists snapshot templates from the real Wrenn API", async () => {
|
||||
const snapshots = await client.snapshots.list();
|
||||
|
||||
expect(Array.isArray(snapshots)).toBe(true);
|
||||
});
|
||||
|
||||
it("gets capsule stats from the real Wrenn API", async () => {
|
||||
const stats = await client.capsules.stats({ range: "1h" });
|
||||
|
||||
expect(stats).toBeTypeOf("object");
|
||||
});
|
||||
|
||||
it("gets capsule usage from the real Wrenn API", async () => {
|
||||
const usage = await client.capsules.usage();
|
||||
|
||||
expect(usage).toBeTypeOf("object");
|
||||
});
|
||||
});
|
||||
|
||||
describeWithLogin("WrennClient live login integration", () => {
|
||||
let client: WrennClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
const authClient = new WrennClient({ baseUrl });
|
||||
const auth = await authClient.auth.login({
|
||||
email: testEmail as string,
|
||||
password: testPassword as string,
|
||||
});
|
||||
|
||||
expect(auth.token).toBeTypeOf("string");
|
||||
client = new WrennClient({ baseUrl, token: auth.token });
|
||||
});
|
||||
|
||||
it("gets the current account profile from the real Wrenn API", async () => {
|
||||
const me = await client.account.getMe();
|
||||
|
||||
expect(me).toBeTypeOf("object");
|
||||
});
|
||||
|
||||
it("lists teams from the real Wrenn API", async () => {
|
||||
const teams = await client.teams.list();
|
||||
|
||||
expect(Array.isArray(teams)).toBe(true);
|
||||
});
|
||||
|
||||
it("lists API keys from the real Wrenn API", async () => {
|
||||
const keys = await client.apiKeys.list();
|
||||
|
||||
expect(Array.isArray(keys)).toBe(true);
|
||||
});
|
||||
|
||||
it("lists notification channels from the real Wrenn API", async () => {
|
||||
const channels = await client.channels.list();
|
||||
|
||||
expect(Array.isArray(channels)).toBe(true);
|
||||
});
|
||||
|
||||
it("lists hosts from the real Wrenn API", async () => {
|
||||
const hosts = await client.hosts.list();
|
||||
|
||||
expect(Array.isArray(hosts)).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -1,183 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user