Compare commits
2 Commits
5b3f2741a3
...
52282618bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 52282618bd | |||
| f1522eaa0b |
@ -29,6 +29,8 @@ export interface RequestOptions {
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
/** Return response text instead of parsing JSON. */
|
/** Return response text instead of parsing JSON. */
|
||||||
asText?: boolean;
|
asText?: boolean;
|
||||||
|
/** Fetch redirect behavior. Defaults to the runtime fetch default. */
|
||||||
|
redirect?: RequestRedirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Thin `fetch` wrapper with Wrenn authentication and error mapping. */
|
/** Thin `fetch` wrapper with Wrenn authentication and error mapping. */
|
||||||
@ -137,6 +139,29 @@ export class HttpClient {
|
|||||||
return res.body;
|
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.
|
* Sends a request and parses the response as JSON unless `asText` is set.
|
||||||
*
|
*
|
||||||
@ -188,6 +213,7 @@ export class HttpClient {
|
|||||||
method,
|
method,
|
||||||
headers: { ...headers, ...opts?.headers },
|
headers: { ...headers, ...opts?.headers },
|
||||||
};
|
};
|
||||||
|
if (opts?.redirect) init.redirect = opts.redirect;
|
||||||
|
|
||||||
if (body !== undefined) {
|
if (body !== undefined) {
|
||||||
init.body = JSON.stringify(body);
|
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 { HttpClient } from "./_shared/http.js";
|
||||||
export type { WsConnectionOpts } from "./_shared/websocket.js";
|
export type { WsConnectionOpts } from "./_shared/websocket.js";
|
||||||
export { WsConnection } 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 type { ClientConfig, ResolvedClientConfig } from "./config.js";
|
||||||
export {
|
export {
|
||||||
DEFAULT_BASE_URL,
|
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