Files
stuffbox-sdk/tests/client.test.ts
T
cyph3rasi 9e1ed2c9cb Publish the unified MIT-licensed Stuffbox SDK source
Hop-State: A_06FPAF0M83E7T0JJ7EP2WQ0
Hop-Proposal: R_06FPAF04S0D4G8TA8TNK3C0
Hop-Task: T_06FPADTV2YD4PD5GMTK3150
Hop-Attempt: AT_06FPADTV2ZYKEY8GH3JTK98
2026-07-15 03:04:24 -07:00

401 lines
13 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
StuffboxClient,
StuffboxError,
type FetchLike,
} from "../src";
function json(value: unknown, init: ResponseInit = {}): Response {
const headers = new Headers(init.headers);
headers.set("content-type", "application/json");
return new Response(JSON.stringify(value), {
...init,
headers,
});
}
describe("StuffboxClient", () => {
it("creates a PKCE connection request using the protocol wire names", async () => {
const fetcher = vi.fn<FetchLike>().mockResolvedValue(
json({
request_id: "cr_1",
client_id: "app_0123456789abcdefghijklmnop",
callback_url: "https://synapsis.test/stuffbox/callback",
authorization_url: "https://stuffbox.test/authorize/cr_1",
expires_at: "2026-01-01T00:05:00.000Z",
}),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test/",
fetch: fetcher,
accessToken: "must-not-leak",
});
await expect(
client.createConnectionRequest({
clientId: "app_0123456789abcdefghijklmnop",
callbackUrl: "https://synapsis.test/stuffbox/callback",
codeChallenge: "c".repeat(43),
scopes: ["assets:read", "assets:write"],
state: "state-with-enough-entropy",
}),
).resolves.toEqual({
id: "cr_1",
clientId: "app_0123456789abcdefghijklmnop",
callbackUrl: "https://synapsis.test/stuffbox/callback",
authorizationUrl: "https://stuffbox.test/authorize/cr_1",
expiresAt: "2026-01-01T00:05:00.000Z",
});
const [url, init] = fetcher.mock.calls[0];
expect(url).toBe("https://stuffbox.test/api/v1/connection-requests");
expect(init?.method).toBe("POST");
expect(init?.headers).not.toHaveProperty("Authorization");
expect(JSON.parse(String(init?.body))).toEqual({
client_id: "app_0123456789abcdefghijklmnop",
callback_url: "https://synapsis.test/stuffbox/callback",
code_challenge: "c".repeat(43),
code_challenge_method: "S256",
scopes: ["assets:read", "assets:write"],
state: "state-with-enough-entropy",
});
});
it("automatically registers a self-hosted callback and returns its client ID", async () => {
const fetcher = vi.fn<FetchLike>().mockResolvedValue(
json({
request_id: "cr_self_hosted",
client_id: "app_self_hosted_0123456789",
callback_url: "https://synapsis.test/stuffbox/callback",
authorization_url: "https://stuffbox.test/authorize/cr_self_hosted",
expires_at: "2026-01-01T00:05:00.000Z",
}),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
});
await expect(
client.createConnectionRequest({
selfHosted: true,
callbackUrl: "https://synapsis.test/stuffbox/callback",
codeChallenge: "c".repeat(43),
scopes: ["assets:read", "assets:write"],
state: "state-with-enough-entropy",
}),
).resolves.toEqual({
id: "cr_self_hosted",
clientId: "app_self_hosted_0123456789",
callbackUrl: "https://synapsis.test/stuffbox/callback",
authorizationUrl: "https://stuffbox.test/authorize/cr_self_hosted",
expiresAt: "2026-01-01T00:05:00.000Z",
});
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
registration_mode: "self_hosted",
callback_url: "https://synapsis.test/stuffbox/callback",
code_challenge: "c".repeat(43),
code_challenge_method: "S256",
scopes: ["assets:read", "assets:write"],
state: "state-with-enough-entropy",
});
});
it("rejects a connection response that omits the client ID", async () => {
const fetcher = vi.fn<FetchLike>().mockResolvedValue(
json({
request_id: "cr_missing_client",
callback_url: "https://synapsis.test/stuffbox/callback",
authorization_url: "https://stuffbox.test/authorize/cr_missing_client",
expires_at: "2026-01-01T00:05:00.000Z",
}),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
});
const error = await client
.createConnectionRequest({
selfHosted: true,
callbackUrl: "https://synapsis.test/stuffbox/callback",
codeChallenge: "c".repeat(43),
scopes: ["assets:read"],
state: "state-with-enough-entropy",
})
.catch((cause: unknown) => cause);
expect(error).toBeInstanceOf(StuffboxError);
expect(error).toMatchObject({
code: "invalid_response",
message: "Missing string field: clientId",
});
});
it("rejects a connection response that omits the canonical callback URL", async () => {
const fetcher = vi.fn<FetchLike>().mockResolvedValue(
json({
request_id: "cr_missing_callback",
client_id: "app_self_hosted_0123456789",
authorization_url: "https://stuffbox.test/authorize/cr_missing_callback",
expires_at: "2026-01-01T00:05:00.000Z",
}),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
});
const error = await client
.createConnectionRequest({
selfHosted: true,
callbackUrl: "https://synapsis.test/stuffbox/callback",
codeChallenge: "c".repeat(43),
scopes: ["assets:read"],
state: "state-with-enough-entropy",
})
.catch((cause: unknown) => cause);
expect(error).toBeInstanceOf(StuffboxError);
expect(error).toMatchObject({
code: "invalid_response",
message: "Missing string field: callbackUrl",
});
});
it("dispatches authorization-code and refresh grants through one endpoint", async () => {
const fetcher = vi.fn<FetchLike>().mockImplementation(async () =>
json({
access_token: "at_new",
token_type: "bearer",
expires_in: 600,
refresh_token: "rt_new",
refresh_token_expires_in: 2_592_000,
scope: "assets:read assets:write",
}),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
});
const exchanged = await client.exchangeAuthorizationCode({
clientId: "app_0123456789abcdefghijklmnop",
code: "code",
codeVerifier: "verifier",
redirectUri: "https://node.test/callback",
});
const refreshed = await client.refreshTokens("rt_old");
expect(exchanged).toEqual(refreshed);
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
grant_type: "authorization_code",
client_id: "app_0123456789abcdefghijklmnop",
code: "code",
code_verifier: "verifier",
redirect_uri: "https://node.test/callback",
});
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual({
grant_type: "refresh_token",
refresh_token: "rt_old",
});
expect(fetcher.mock.calls[0][0]).toBe("https://stuffbox.test/api/v1/token");
expect(fetcher.mock.calls[1][0]).toBe("https://stuffbox.test/api/v1/token");
});
it("adds a lazy bearer token only to protected requests", async () => {
const accessToken = vi.fn().mockResolvedValue("at_secret");
const fetcher = vi.fn<FetchLike>().mockResolvedValue(
json({ items: [], next_cursor: "next" }),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
accessToken,
});
await expect(client.listAssets({ limit: 10 })).resolves.toEqual({
items: [],
nextCursor: "next",
});
expect(accessToken).toHaveBeenCalledOnce();
expect(fetcher.mock.calls[0][0]).toBe(
"https://stuffbox.test/api/v1/assets?limit=10",
);
expect(fetcher.mock.calls[0][1]?.headers).toHaveProperty(
"Authorization",
"Bearer at_secret",
);
});
it("rejects an asset page size outside the public API range before fetching", async () => {
const fetcher = vi.fn<FetchLike>();
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
accessToken: "at_secret",
});
await expect(client.listAssets({ limit: 101 })).rejects.toThrow(
"Asset list limit must be between 1 and 100",
);
expect(fetcher).not.toHaveBeenCalled();
});
it("creates and completes a direct upload without handling file bytes", async () => {
const fetcher = vi
.fn<FetchLike>()
.mockResolvedValueOnce(
json({
upload_id: "up_1",
upload_url: "https://objects.test/signed",
method: "PUT",
required_headers: { "Content-Type": "image/png" },
expires_at: "2026-01-01T00:10:00.000Z",
completion_token: "uct_1",
}),
)
.mockResolvedValueOnce(
json({
asset: {
id: "as_1",
public_id: "pub_1",
canonical_url: "https://stuffbox.test/f/pub_1",
original_filename: "image.png",
mime_type: "image/png",
byte_size: 12,
status: "active",
created_at: "2026-01-01T00:00:00.000Z",
},
}),
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
accessToken: "at_1",
});
const session = await client.createUpload({
filename: "image.png",
mimeType: "image/png",
size: 12,
});
const asset = await client.completeUpload(session.id, {
completionToken: session.completionToken,
});
expect(session.requiredHeaders).toEqual({ "Content-Type": "image/png" });
expect(asset.url).toBe("https://stuffbox.test/f/pub_1");
expect(JSON.parse(String(fetcher.mock.calls[1][1]?.body))).toEqual({
completion_token: "uct_1",
});
});
it("throws typed structured errors with retry metadata", async () => {
const fetcher: FetchLike = async () =>
json(
{
error: {
code: "quota_exceeded",
message: "Storage quota exceeded",
details: { remaining_bytes: 4 },
request_id: "req_1",
},
},
{ status: 409, headers: { "retry-after": "12" } },
);
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
accessToken: "at_1",
});
const error = await client
.createUpload({ filename: "large.mp4", mimeType: "video/mp4", size: 100 })
.catch((cause: unknown) => cause);
expect(error).toBeInstanceOf(StuffboxError);
expect(error).toMatchObject({
code: "quota_exceeded",
status: 409,
requestId: "req_1",
retryAfter: 12,
details: { remaining_bytes: 4 },
});
});
it("supports endpoint overrides and per-request bearer suppression", async () => {
const fetcher = vi.fn<FetchLike>().mockResolvedValue(json({ items: [] }));
const client = new StuffboxClient({
baseUrl: "https://example.test/custom",
fetch: fetcher,
accessToken: "at_default",
endpoints: { assets: "/assets" },
});
await client.listAssets({}, { accessToken: null });
expect(fetcher.mock.calls[0][0]).toBe("https://example.test/custom/assets");
expect(fetcher.mock.calls[0][1]?.headers).not.toHaveProperty("Authorization");
});
it("revokes opaque tokens without adding another bearer credential", async () => {
const fetcher = vi
.fn<FetchLike>()
.mockResolvedValue(new Response(null, { status: 204 }));
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
accessToken: "at_configured",
});
await client.revokeToken({
token: "rt_revoked",
tokenTypeHint: "refresh_token",
});
expect(fetcher.mock.calls[0][0]).toBe("https://stuffbox.test/api/v1/revoke");
expect(fetcher.mock.calls[0][1]?.headers).not.toHaveProperty("Authorization");
expect(JSON.parse(String(fetcher.mock.calls[0][1]?.body))).toEqual({
token: "rt_revoked",
token_type_hint: "refresh_token",
});
});
it("gets and deletes an owner-scoped asset by encoded ID", async () => {
const fetcher = vi
.fn<FetchLike>()
.mockResolvedValueOnce(
json({
asset: {
id: "asset/one",
public_asset_id: "pub_1",
url: "https://stuffbox.test/f/pub_1",
filename: "photo.jpg",
mimeType: "image/jpeg",
byteSize: 20,
status: "active",
createdAt: "2026-01-01T00:00:00.000Z",
},
}),
)
.mockResolvedValueOnce(new Response(null, { status: 204 }));
const client = new StuffboxClient({
baseUrl: "https://stuffbox.test",
fetch: fetcher,
accessToken: "at_1",
});
await expect(client.getAsset("asset/one")).resolves.toMatchObject({
id: "asset/one",
publicId: "pub_1",
});
await client.deleteAsset("asset/one");
expect(fetcher.mock.calls[0][0]).toBe(
"https://stuffbox.test/api/v1/assets/asset%2Fone",
);
expect(fetcher.mock.calls[1][1]?.method).toBe("DELETE");
});
});