feat: add low-level Wrenn client resources
Implement WrennClient with typed resource mappings for auth, account, API keys, users, teams, capsules, files, snapshots, hosts, and channels. Add endpoint mapping tests plus live integration tess that authenticate with WRENN_TEST_EMAIL/WRENN_TEST_PASS and use WRENN_API_KEY for API-key scoped endpoints
This commit is contained in:
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user