Skip to content

Commit fd17bad

Browse files
ascorbicclaude
andauthored
feat: add e2e test suite for PDS (#34)
* wip * fix: complete e2e test suite setup - Switch from programmatic Vite to subprocess for better isolation - Use test.local as handle (requires 2+ parts) - Fix BlobRef CID access (use .ref.toString() for CID object) - Skip getLatestCommit tests (endpoint not implemented) - Update deleteRecord test to expect error All 32 tests pass (2 skipped for unimplemented endpoint) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use static fixture for e2e tests - Create e2e/fixture/ directory with static template files - Copy fixture to temp directory instead of programmatic file creation - Use npm run dev instead of npx vite - Replace {{PDS_PACKAGE_PATH}} placeholder with actual path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: add e2e fixture .dev.vars (test credentials only) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * choire: run all tests --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c30312c commit fd17bad

15 files changed

Lines changed: 1259 additions & 2 deletions

packages/pds/e2e/blobs.e2e.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { describe, it, expect, beforeAll } from "vitest";
2+
import { AtpAgent } from "@atproto/api";
3+
import {
4+
createAgent,
5+
getBaseUrl,
6+
TEST_DID,
7+
TEST_HANDLE,
8+
TEST_PASSWORD,
9+
uniqueRkey,
10+
} from "./helpers";
11+
12+
describe("Blob Storage", () => {
13+
let agent: AtpAgent;
14+
15+
beforeAll(async () => {
16+
agent = createAgent();
17+
await agent.login({
18+
identifier: TEST_HANDLE,
19+
password: TEST_PASSWORD,
20+
});
21+
});
22+
23+
describe("uploadBlob", () => {
24+
it("uploads a blob", async () => {
25+
// Create a simple test blob (PNG header bytes)
26+
const pngBytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
27+
28+
const result = await agent.com.atproto.repo.uploadBlob(pngBytes, {
29+
encoding: "image/png",
30+
});
31+
32+
expect(result.success).toBe(true);
33+
// The blob reference structure from @atproto/api
34+
const blob = result.data.blob;
35+
expect(blob).toBeDefined();
36+
expect(blob.mimeType).toBe("image/png");
37+
expect(blob.size).toBe(pngBytes.length);
38+
// ref can be accessed as .ref.$link or just stringified
39+
expect(blob.ref).toBeDefined();
40+
});
41+
42+
it("uploads blob and associates with record", async () => {
43+
const testData = new Uint8Array([1, 2, 3, 4, 5]);
44+
45+
// Upload blob
46+
const uploadResult = await agent.com.atproto.repo.uploadBlob(testData, {
47+
encoding: "application/octet-stream",
48+
});
49+
50+
expect(uploadResult.success).toBe(true);
51+
const blobRef = uploadResult.data.blob;
52+
53+
// Create a post with the blob embedded
54+
const rkey = uniqueRkey();
55+
const postResult = await agent.com.atproto.repo.createRecord({
56+
repo: TEST_DID,
57+
collection: "app.bsky.feed.post",
58+
rkey,
59+
record: {
60+
$type: "app.bsky.feed.post",
61+
text: "Post with blob",
62+
createdAt: new Date().toISOString(),
63+
embed: {
64+
$type: "app.bsky.embed.images",
65+
images: [
66+
{
67+
image: blobRef,
68+
alt: "Test image",
69+
},
70+
],
71+
},
72+
},
73+
});
74+
75+
expect(postResult.success).toBe(true);
76+
77+
// Verify blob is retrievable via getBlob
78+
// BlobRef.ref is a CID object - call toString() to get the string
79+
const cid = blobRef.ref.toString();
80+
const response = await fetch(
81+
`${getBaseUrl()}/xrpc/com.atproto.sync.getBlob?did=${TEST_DID}&cid=${cid}`,
82+
);
83+
84+
expect(response.ok).toBe(true);
85+
const retrieved = new Uint8Array(await response.arrayBuffer());
86+
expect(retrieved).toEqual(testData);
87+
});
88+
});
89+
90+
describe("getBlob", () => {
91+
it("retrieves an uploaded blob", async () => {
92+
const testData = new Uint8Array([10, 20, 30, 40, 50]);
93+
94+
// Upload blob first
95+
const uploadResult = await agent.com.atproto.repo.uploadBlob(testData, {
96+
encoding: "application/octet-stream",
97+
});
98+
const blobRef = uploadResult.data.blob;
99+
// BlobRef.ref is a CID object - call toString() to get the string
100+
const cid = blobRef.ref.toString();
101+
102+
// Associate with a record so it's "committed"
103+
const rkey = uniqueRkey();
104+
await agent.com.atproto.repo.createRecord({
105+
repo: TEST_DID,
106+
collection: "app.bsky.feed.post",
107+
rkey,
108+
record: {
109+
$type: "app.bsky.feed.post",
110+
text: "Post for blob retrieval test",
111+
createdAt: new Date().toISOString(),
112+
embed: {
113+
$type: "app.bsky.embed.images",
114+
images: [
115+
{
116+
image: blobRef,
117+
alt: "Test",
118+
},
119+
],
120+
},
121+
},
122+
});
123+
124+
// Retrieve via HTTP
125+
const response = await fetch(
126+
`${getBaseUrl()}/xrpc/com.atproto.sync.getBlob?did=${TEST_DID}&cid=${cid}`,
127+
);
128+
129+
expect(response.ok).toBe(true);
130+
expect(response.headers.get("content-type")).toBe(
131+
"application/octet-stream",
132+
);
133+
134+
const retrieved = new Uint8Array(await response.arrayBuffer());
135+
expect(retrieved).toEqual(testData);
136+
});
137+
138+
it("returns error for non-existent blob", async () => {
139+
const fakeCid =
140+
"bafyreihwvs4crshs6ldcp73ue3cxrtzglohz6s7ks3dqv4i4t27bvzg2jq";
141+
142+
const response = await fetch(
143+
`${getBaseUrl()}/xrpc/com.atproto.sync.getBlob?did=${TEST_DID}&cid=${fakeCid}`,
144+
);
145+
146+
expect(response.ok).toBe(false);
147+
// BlobNotFound can return 400 or 404 depending on implementation
148+
expect([400, 404]).toContain(response.status);
149+
});
150+
});
151+
152+
describe("listBlobs", () => {
153+
it("lists blobs for a repo", async () => {
154+
// Upload a blob and associate it
155+
const testData = new Uint8Array([100, 101, 102]);
156+
const uploadResult = await agent.com.atproto.repo.uploadBlob(testData, {
157+
encoding: "image/png",
158+
});
159+
const blobRef = uploadResult.data.blob;
160+
// BlobRef.ref is a CID object - call toString() to get the string
161+
const uploadedCid = blobRef.ref.toString();
162+
163+
const rkey = uniqueRkey();
164+
await agent.com.atproto.repo.createRecord({
165+
repo: TEST_DID,
166+
collection: "app.bsky.feed.post",
167+
rkey,
168+
record: {
169+
$type: "app.bsky.feed.post",
170+
text: "Post for listBlobs test",
171+
createdAt: new Date().toISOString(),
172+
embed: {
173+
$type: "app.bsky.embed.images",
174+
images: [
175+
{
176+
image: blobRef,
177+
alt: "Test",
178+
},
179+
],
180+
},
181+
},
182+
});
183+
184+
// List blobs
185+
const response = await fetch(
186+
`${getBaseUrl()}/xrpc/com.atproto.sync.listBlobs?did=${TEST_DID}`,
187+
);
188+
189+
expect(response.ok).toBe(true);
190+
const data = (await response.json()) as { cids: string[] };
191+
expect(data.cids).toBeDefined();
192+
expect(Array.isArray(data.cids)).toBe(true);
193+
// Should contain our uploaded blob
194+
expect(data.cids).toContain(uploadedCid);
195+
});
196+
});
197+
});

0 commit comments

Comments
 (0)