b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
222 lines
7.2 KiB
TypeScript
222 lines
7.2 KiB
TypeScript
import { ClientResponseError } from "@atcute/client";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type { Did } from "../src/credentials/index.js";
|
|
import { PublishingClient } from "../src/publishing/index.js";
|
|
|
|
function buildHandler(responses: Record<string, { status: number; body: unknown }>): {
|
|
handler: (pathname: string, init: RequestInit) => Promise<Response>;
|
|
calls: Array<{ pathname: string; init: RequestInit }>;
|
|
} {
|
|
const calls: Array<{ pathname: string; init: RequestInit }> = [];
|
|
const handler = vi.fn(async (pathname: string, init: RequestInit) => {
|
|
calls.push({ pathname, init });
|
|
// `pathname` from atcute is `/xrpc/<nsid>` plus an optional `?...` query.
|
|
const cleanPath = pathname.split("?")[0]!;
|
|
const match = responses[cleanPath];
|
|
if (!match) {
|
|
return new Response(JSON.stringify({ error: "TestNotConfigured" }), {
|
|
status: 500,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify(match.body), {
|
|
status: match.status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
});
|
|
return { handler, calls };
|
|
}
|
|
|
|
describe("PublishingClient", () => {
|
|
const did = "did:plc:abc123" as Did;
|
|
const pds = "https://pds.example.com";
|
|
|
|
it("putRecord posts to com.atproto.repo.putRecord with the right body", async () => {
|
|
const { handler, calls } = buildHandler({
|
|
"/xrpc/com.atproto.repo.putRecord": {
|
|
status: 200,
|
|
body: {
|
|
uri: "at://did:plc:abc123/com.emdashcms.experimental.package.profile/gallery",
|
|
cid: "bafyreigh2akiscaildc4mscz4uzpcbap5jxg26eecmrf6cmnvkzkjmoixe",
|
|
},
|
|
},
|
|
});
|
|
|
|
const client = PublishingClient.fromHandler({ handler, did, pds });
|
|
const result = await client.putRecord({
|
|
collection: "com.emdashcms.experimental.package.profile",
|
|
rkey: "gallery",
|
|
record: {
|
|
$type: "com.emdashcms.experimental.package.profile",
|
|
type: "emdash-plugin",
|
|
license: "MIT",
|
|
authors: [{ name: "Alice" }],
|
|
security: [{ email: "security@example.com" }],
|
|
},
|
|
});
|
|
|
|
expect(result.uri).toContain("did:plc:abc123");
|
|
expect(result.cid).toBeTruthy();
|
|
|
|
const call = calls[0]!;
|
|
expect(call.pathname).toMatch(/^\/xrpc\/com\.atproto\.repo\.putRecord/);
|
|
// atcute issues procedures with a lowercase "post" method. We assert the
|
|
// literal value so a regression in either direction (atcute upper-cases,
|
|
// or our wrapper accidentally uses GET) trips the test.
|
|
expect(call.init.method).toBe("post");
|
|
|
|
const body = JSON.parse(call.init.body as string);
|
|
expect(body).toMatchObject({
|
|
repo: did,
|
|
collection: "com.emdashcms.experimental.package.profile",
|
|
rkey: "gallery",
|
|
// validate: true is the default. The PDS lexicon-validates every
|
|
// record we write; opting out (skipValidation: true) is a footgun.
|
|
validate: true,
|
|
});
|
|
});
|
|
|
|
it("putRecord opts out of lexicon validation when skipValidation is set", async () => {
|
|
const { handler, calls } = buildHandler({
|
|
"/xrpc/com.atproto.repo.putRecord": {
|
|
status: 200,
|
|
body: { uri: "at://did:plc:abc123/c/r", cid: "b" },
|
|
},
|
|
});
|
|
|
|
const client = PublishingClient.fromHandler({ handler, did, pds });
|
|
await client.unsafePutRecord({
|
|
collection: "com.emdashcms.experimental.package.profile",
|
|
rkey: "x",
|
|
record: {},
|
|
skipValidation: true,
|
|
});
|
|
|
|
const body = JSON.parse(calls[0]!.init.body as string);
|
|
expect(body.validate).toBe(false);
|
|
});
|
|
|
|
it("putRecord throws ClientResponseError on a non-2xx", async () => {
|
|
const { handler } = buildHandler({
|
|
"/xrpc/com.atproto.repo.putRecord": {
|
|
status: 401,
|
|
body: { error: "AuthRequired", message: "session expired" },
|
|
},
|
|
});
|
|
|
|
const client = PublishingClient.fromHandler({ handler, did, pds });
|
|
try {
|
|
// Use unsafePutRecord so we can pass an opaque record without
|
|
// satisfying the registry-lexicons typed shape -- this test is
|
|
// about the error-mapping path, not the body.
|
|
await client.unsafePutRecord({
|
|
collection: "com.emdashcms.experimental.package.profile",
|
|
rkey: "x",
|
|
record: {},
|
|
});
|
|
expect.fail("expected ClientResponseError");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(ClientResponseError);
|
|
const e = err as ClientResponseError;
|
|
expect(e.status).toBe(401);
|
|
expect(e.error).toBe("AuthRequired");
|
|
}
|
|
});
|
|
|
|
it("getRecord queries with repo + collection + rkey params", async () => {
|
|
const { handler, calls } = buildHandler({
|
|
"/xrpc/com.atproto.repo.getRecord": {
|
|
status: 200,
|
|
body: { uri: "at://did:plc:abc123/c/r", cid: "b", value: { foo: 1 } },
|
|
},
|
|
});
|
|
|
|
const client = PublishingClient.fromHandler({ handler, did, pds });
|
|
const result = await client.getRecord({
|
|
collection: "com.emdashcms.experimental.package.profile",
|
|
rkey: "gallery",
|
|
});
|
|
|
|
expect(result.value).toEqual({ foo: 1 });
|
|
// atcute issues queries with a lowercase "get" method.
|
|
expect(calls[0]!.init.method).toBe("get");
|
|
// The URL with query params is in the pathname for atcute handlers.
|
|
expect(calls[0]!.pathname).toContain("repo=did%3Aplc%3Aabc123");
|
|
expect(calls[0]!.pathname).toContain("collection=com.emdashcms.experimental.package.profile");
|
|
expect(calls[0]!.pathname).toContain("rkey=gallery");
|
|
});
|
|
|
|
it("listRecords paginates with cursor", async () => {
|
|
const { handler, calls } = buildHandler({
|
|
"/xrpc/com.atproto.repo.listRecords": {
|
|
status: 200,
|
|
body: { records: [], cursor: "next" },
|
|
},
|
|
});
|
|
|
|
const client = PublishingClient.fromHandler({ handler, did, pds });
|
|
const result = await client.listRecords({
|
|
collection: "com.emdashcms.experimental.package.release",
|
|
limit: 50,
|
|
cursor: "abc",
|
|
});
|
|
|
|
expect(result.cursor).toBe("next");
|
|
expect(calls[0]!.pathname).toContain("limit=50");
|
|
expect(calls[0]!.pathname).toContain("cursor=abc");
|
|
});
|
|
|
|
it("applyWrites batches create/update/delete in a single XRPC call", async () => {
|
|
const { handler, calls } = buildHandler({
|
|
"/xrpc/com.atproto.repo.applyWrites": {
|
|
status: 200,
|
|
body: {
|
|
results: [
|
|
{
|
|
$type: "com.atproto.repo.applyWrites#createResult",
|
|
uri: "at://did:plc:abc123/com.emdashcms.experimental.package.profile/x",
|
|
cid: "bafy...",
|
|
},
|
|
{
|
|
$type: "com.atproto.repo.applyWrites#updateResult",
|
|
uri: "at://did:plc:abc123/com.emdashcms.experimental.package.release/x:1.0.0",
|
|
cid: "bafy...",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
const client = PublishingClient.fromHandler({ handler, did, pds });
|
|
const result = await client.applyWrites({
|
|
writes: [
|
|
{
|
|
op: "create",
|
|
collection: "com.emdashcms.experimental.package.profile",
|
|
rkey: "x",
|
|
// Cast: real callers build records via the lexicon types; the
|
|
// test asserts shape, not record contents.
|
|
record: { foo: 1 } as never,
|
|
},
|
|
{
|
|
op: "update",
|
|
collection: "com.emdashcms.experimental.package.release",
|
|
rkey: "x:1.0.0",
|
|
record: { bar: 2 } as never,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.results).toHaveLength(2);
|
|
expect(result.results[0]).toMatchObject({ op: "create" });
|
|
expect(result.results[1]).toMatchObject({ op: "update" });
|
|
|
|
// One XRPC call, lexicon-validate enabled by default.
|
|
expect(calls).toHaveLength(1);
|
|
const body = JSON.parse(calls[0]!.init.body as string);
|
|
expect(body.validate).toBe(true);
|
|
expect(body.writes).toHaveLength(2);
|
|
});
|
|
});
|