import { describe, expect, it, vi } from "vitest";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import type { BobbinContext } from "#lib/api/client.ts";
const agent = { sub: "did:plc:alice" } as unknown as OAuthUserAgent;
const BLOB = {
$type: "blob" as const,
ref: { $link: "bafkreifqrrwn3ioqkrquhibb3csu6i3kogd76uk6ktptc7s5exim6rfu6u" },
mimeType: "text/plain",
size: 12
};
const load = async () => {
vi.resetModules();
const writes: Record<string, unknown>[] = [];
const waits: Record<string, unknown>[] = [];
vi.doMock("$app/env", () => ({ browser: true }));
vi.doMock("#lib/auth/agent.ts", () => ({
createClient: () => ({
call: async (_schema: unknown, options: { input: Record<string, unknown> }) => {
writes.push(options.input);
return {
ok: true,
data: {
uri: "at://did:plc:alice/org.tangled.snippet.string/3l7abc",
cid: "bafyreicu44j74asmgef6cyxrpe5ege6cmovaqx6dzckgtawb7m2odv24gu"
}
};
}
})
}));
const strings = await import("./strings");
const graph = await import("./graph");
const records = await import("./records");
const bobbin = {
xrpc: {
call: async (_schema: unknown, options: { params: Record<string, unknown> }) => {
waits.push(options.params);
return {
ok: true,
data: options.params.cid
? { status: "indexed", cid: options.params.cid }
: { status: "deleted" }
};
}
}
} as unknown as BobbinContext;
return { strings, graph, records, writes, waits, bobbin };
};
describe("strings api", () => {
it("puts a string record into org.tangled.snippet.string collection with optional swapRecord", async () => {
const { strings, writes, waits, bobbin } = await load();
await strings.putString(
agent,
bobbin,
"3l7abc",
{
$type: "org.tangled.snippet.string",
snippet: {
$type: "org.tangled.snippet.string#textFile",
name: "main.rs",
content: BLOB
},
description: "test snippet",
createdAt: "2026-05-01T00:00:00Z"
},
"bafyreigrjynfxx5cdjylqloavwwdjsx3sfdssjqoeohh72jetcrnpgg6cy"
);
expect(writes).toHaveLength(1);
expect(writes[0]).toMatchObject({
repo: "did:plc:alice",
collection: "org.tangled.snippet.string",
rkey: "3l7abc",
swapRecord: "bafyreigrjynfxx5cdjylqloavwwdjsx3sfdssjqoeohh72jetcrnpgg6cy",
record: {
$type: "org.tangled.snippet.string",
snippet: { name: "main.rs", content: BLOB }
}
});
expect(waits).toEqual([
{
uri: "at://did:plc:alice/org.tangled.snippet.string/3l7abc",
cid: "bafyreicu44j74asmgef6cyxrpe5ege6cmovaqx6dzckgtawb7m2odv24gu"
}
]);
});
it("surfaces a write the appview did not index", async () => {
const { strings, bobbin } = await load();
(bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => ({
ok: true,
data: { status: "rejected", detail: "unknown collection" }
});
await expect(
strings.putString(agent, bobbin, "3l7abc", {
$type: "org.tangled.snippet.string",
snippet: {
$type: "org.tangled.snippet.string#textFile",
name: "main.rs",
content: BLOB
},
createdAt: "2026-05-01T00:00:00Z"
})
).rejects.toThrow("unknown collection");
});
it("deletes a string record from org.tangled.snippet.string collection with optional swapRecord", async () => {
const { strings, writes, waits, bobbin } = await load();
await strings.deleteString(
agent,
bobbin,
"at://did:plc:alice/org.tangled.snippet.string/3l7abc",
"bafyreigrjynfxx5cdjylqloavwwdjsx3sfdssjqoeohh72jetcrnpgg6cy"
);
expect(writes).toHaveLength(1);
expect(writes[0]).toMatchObject({
repo: "did:plc:alice",
collection: "org.tangled.snippet.string",
rkey: "3l7abc",
swapRecord: "bafyreigrjynfxx5cdjylqloavwwdjsx3sfdssjqoeohh72jetcrnpgg6cy"
});
expect(waits).toEqual([{ uri: "at://did:plc:alice/org.tangled.snippet.string/3l7abc" }]);
});
it("reports an unconfirmed delete instead of failing the delete", async () => {
vi.useFakeTimers();
try {
const { strings, bobbin } = await load();
(bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => ({
ok: true,
data: { status: "indexed", cid: "bafytest" }
});
const deleted = strings.deleteString(
agent,
bobbin,
"at://did:plc:alice/org.tangled.snippet.string/3l7abc"
);
await vi.advanceTimersByTimeAsync(6000);
await expect(deleted).resolves.toBe(false);
} finally {
vi.useRealTimers();
}
});
it("confirms a delete once the remembered write stops answering", async () => {
const { strings, bobbin } = await load();
let asks = 0;
(bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => {
asks += 1;
return asks === 1
? { ok: true, data: { status: "indexed", cid: "bafytest" } }
: { ok: true, data: { status: "deleted" } };
};
await expect(
strings.deleteString(
agent,
bobbin,
"at://did:plc:alice/org.tangled.snippet.string/3l7abc"
)
).resolves.toBe(true);
expect(asks).toBe(2);
});
it("stops asking after the budget when bobbin keeps answering with the write", async () => {
const { bobbin } = await load();
let asks = 0;
(bobbin as unknown as { xrpc: { call: unknown } }).xrpc.call = async () => {
asks += 1;
return { ok: true, data: { status: "indexed", cid: "bafytest" } };
};
const { awaitDeletedRecord } = await import("./awaitRecord");
await expect(
awaitDeletedRecord(bobbin, "at://did:plc:alice/org.tangled.snippet.string/3l7abc", {
budgetMs: 5,
pollMs: 1
})
).resolves.toBe(false);
expect(asks).toBeGreaterThan(1);
});
it("creates a star on a string AT-URI", async () => {
const { graph, writes } = await load();
const { rkey } = await graph.createStar(agent, {
kind: "string",
uri: "at://did:plc:bob/org.tangled.snippet.string/3l7xyz",
cid: "bafyreiasfuctyrni66wjjvcrvckwcjsheir3a56bx2y5j7ietypnsfd7gy"
});
expect(writes).toHaveLength(1);
expect(writes[0]).toMatchObject({
repo: "did:plc:alice",
collection: "org.tangled.feed.star",
record: {
$type: "org.tangled.feed.star",
subject: {
$type: "org.tangled.feed.star#record",
record: {
uri: "at://did:plc:bob/org.tangled.snippet.string/3l7xyz",
cid: "bafyreiasfuctyrni66wjjvcrvckwcjsheir3a56bx2y5j7ietypnsfd7gy"
}
}
}
});
expect(rkey).toBe("3l7abc");
});
it("fetches a string via getString", async () => {
const { records } = await load();
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
uri: "at://did:plc:alice/org.tangled.snippet.string/3l7abc",
cid: "bafyreicu44j74asmgef6cyxrpe5ege6cmovaqx6dzckgtawb7m2odv24gu",
value: {
$type: "org.tangled.snippet.string",
snippet: {
$type: "org.tangled.snippet.string#textFile",
name: "main.rs",
content: BLOB
},
description: "test snippet",
createdAt: "2026-05-01T00:00:00Z"
}
})
});
const ctx: BobbinContext = {
serviceUrl: "https://bobbin.tangled.local",
fetch: mockFetch,
xrpc: {} as unknown as BobbinContext["xrpc"]
};
const result = await records.getString(
ctx,
"at://did:plc:alice/org.tangled.snippet.string/3l7abc"
);
expect(mockFetch).toHaveBeenCalledTimes(1);
const url = new URL(mockFetch.mock.calls[0][0]);
expect(url.pathname).toBe("/xrpc/org.tangled.string.getString");
expect(url.searchParams.get("uri")).toBe(
"at://did:plc:alice/org.tangled.snippet.string/3l7abc"
);
expect(result.value.snippet.name).toBe("main.rs");
});
});
describe("fetchStringContents", () => {
const subject = (pds: string) =>
({
did: "did:plc:alice",
handle: "alice.test",
pds,
uri: "at://did:plc:alice/org.tangled.snippet.string/3l7abc",
record: { value: { snippet: { name: "a.txt", content: BLOB } } }
}) as never;
const load = async () => {
vi.resetModules();
vi.doMock("$app/env", () => ({ browser: false, dev: false }));
return import("#lib/api/strings.ts");
};
it("reads the blob from a public pds", async () => {
const { fetchStringContents } = await load();
const fetch = vi.fn(async () => new Response("hello"));
expect(await fetchStringContents(fetch, subject("https://pds.example.com"))).toBe("hello");
});
it("never fetches from a pds in the private network", async () => {
const { fetchStringContents } = await load();
const fetch = vi.fn(async () => new Response("secret"));
for (const pds of [
"http://169.254.169.254",
"https://localhost:2583",
"http://bobbin:8090"
])
await expect(fetchStringContents(fetch, subject(pds))).rejects.toMatchObject({
status: 502
});
expect(fetch).not.toHaveBeenCalled();
});
it("doesn't follow a public pds's redirect into the private network", async () => {
const { fetchStringContents } = await load();
const fetch = vi.fn(
async () =>
new Response(null, {
status: 302,
headers: { location: "http://169.254.169.254/latest/meta-data" }
})
);
await expect(
fetchStringContents(fetch, subject("https://evil.example.com"))
).rejects.toMatchObject({ status: 502 });
expect(fetch).toHaveBeenCalledTimes(1);
});
});