Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions packages/pds/e2e/blobs.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { describe, it, expect, beforeAll } from "vitest";
import { AtpAgent } from "@atproto/api";
import {
createAgent,
getBaseUrl,
TEST_DID,
TEST_HANDLE,
TEST_PASSWORD,
uniqueRkey,
} from "./helpers";

describe("Blob Storage", () => {
let agent: AtpAgent;

beforeAll(async () => {
agent = createAgent();
await agent.login({
identifier: TEST_HANDLE,
password: TEST_PASSWORD,
});
});

describe("uploadBlob", () => {
it("uploads a blob", async () => {
// Create a simple test blob (PNG header bytes)
const pngBytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);

const result = await agent.com.atproto.repo.uploadBlob(pngBytes, {
encoding: "image/png",
});

expect(result.success).toBe(true);
// The blob reference structure from @atproto/api
const blob = result.data.blob;
expect(blob).toBeDefined();
expect(blob.mimeType).toBe("image/png");
expect(blob.size).toBe(pngBytes.length);
// ref can be accessed as .ref.$link or just stringified
expect(blob.ref).toBeDefined();
});

it("uploads blob and associates with record", async () => {
const testData = new Uint8Array([1, 2, 3, 4, 5]);

// Upload blob
const uploadResult = await agent.com.atproto.repo.uploadBlob(testData, {
encoding: "application/octet-stream",
});

expect(uploadResult.success).toBe(true);
const blobRef = uploadResult.data.blob;

// Create a post with the blob embedded
const rkey = uniqueRkey();
const postResult = await agent.com.atproto.repo.createRecord({
repo: TEST_DID,
collection: "app.bsky.feed.post",
rkey,
record: {
$type: "app.bsky.feed.post",
text: "Post with blob",
createdAt: new Date().toISOString(),
embed: {
$type: "app.bsky.embed.images",
images: [
{
image: blobRef,
alt: "Test image",
},
],
},
},
});

expect(postResult.success).toBe(true);

// Verify blob is retrievable via getBlob
// BlobRef.ref is a CID object - call toString() to get the string
const cid = blobRef.ref.toString();
const response = await fetch(
`${getBaseUrl()}/xrpc/com.atproto.sync.getBlob?did=${TEST_DID}&cid=${cid}`,
);

expect(response.ok).toBe(true);
const retrieved = new Uint8Array(await response.arrayBuffer());
expect(retrieved).toEqual(testData);
});
});

describe("getBlob", () => {
it("retrieves an uploaded blob", async () => {
const testData = new Uint8Array([10, 20, 30, 40, 50]);

// Upload blob first
const uploadResult = await agent.com.atproto.repo.uploadBlob(testData, {
encoding: "application/octet-stream",
});
const blobRef = uploadResult.data.blob;
// BlobRef.ref is a CID object - call toString() to get the string
const cid = blobRef.ref.toString();

// Associate with a record so it's "committed"
const rkey = uniqueRkey();
await agent.com.atproto.repo.createRecord({
repo: TEST_DID,
collection: "app.bsky.feed.post",
rkey,
record: {
$type: "app.bsky.feed.post",
text: "Post for blob retrieval test",
createdAt: new Date().toISOString(),
embed: {
$type: "app.bsky.embed.images",
images: [
{
image: blobRef,
alt: "Test",
},
],
},
},
});

// Retrieve via HTTP
const response = await fetch(
`${getBaseUrl()}/xrpc/com.atproto.sync.getBlob?did=${TEST_DID}&cid=${cid}`,
);

expect(response.ok).toBe(true);
expect(response.headers.get("content-type")).toBe(
"application/octet-stream",
);

const retrieved = new Uint8Array(await response.arrayBuffer());
expect(retrieved).toEqual(testData);
});

it("returns error for non-existent blob", async () => {
const fakeCid =
"bafyreihwvs4crshs6ldcp73ue3cxrtzglohz6s7ks3dqv4i4t27bvzg2jq";

const response = await fetch(
`${getBaseUrl()}/xrpc/com.atproto.sync.getBlob?did=${TEST_DID}&cid=${fakeCid}`,
);

expect(response.ok).toBe(false);
// BlobNotFound can return 400 or 404 depending on implementation
expect([400, 404]).toContain(response.status);
});
});

describe("listBlobs", () => {
it("lists blobs for a repo", async () => {
// Upload a blob and associate it
const testData = new Uint8Array([100, 101, 102]);
const uploadResult = await agent.com.atproto.repo.uploadBlob(testData, {
encoding: "image/png",
});
const blobRef = uploadResult.data.blob;
// BlobRef.ref is a CID object - call toString() to get the string
const uploadedCid = blobRef.ref.toString();

const rkey = uniqueRkey();
await agent.com.atproto.repo.createRecord({
repo: TEST_DID,
collection: "app.bsky.feed.post",
rkey,
record: {
$type: "app.bsky.feed.post",
text: "Post for listBlobs test",
createdAt: new Date().toISOString(),
embed: {
$type: "app.bsky.embed.images",
images: [
{
image: blobRef,
alt: "Test",
},
],
},
},
});

// List blobs
const response = await fetch(
`${getBaseUrl()}/xrpc/com.atproto.sync.listBlobs?did=${TEST_DID}`,
);

expect(response.ok).toBe(true);
const data = (await response.json()) as { cids: string[] };
expect(data.cids).toBeDefined();
expect(Array.isArray(data.cids)).toBe(true);
// Should contain our uploaded blob
expect(data.cids).toContain(uploadedCid);
});
});
});
Loading
Loading