// A PDS that exists only in a test file. // // Nothing imports this from the app — it is reachable only from `*.test.ts`, so it never enters the // bundle — but it lives beside the code it stands in for rather than in a test directory, because // what it implements is a contract: the exact `com.atproto.*` surface `FetchRepoTransport`, // `RepoPoller` and `RepoWriter` speak. Two test files share it so that "reads work" and "writes // work" are claims about the same server, not about two different pretend ones. // // It serves DID documents, handle resolution, repo heads, `listRecords`, `getRecord`, `createRecord`, // `putRecord`, `uploadBlob` and `getBlob`. It deliberately does not model authorization: a request's credentials are recorded // and returned to the test, which is what lets a test assert *what* was sent without this file // having an opinion about what a valid token looks like. // // It does model two things about a repo that matter to the fold, though, because a stub that got them // wrong would make a passing test meaningless: a repo holds exactly ONE value per rkey (so a `put` // replaces rather than accumulates — keeping both versions is the record store's job, not the // server's), and it refuses a `put` whose `swapRecord` is not the CID it currently holds. import type { StoredRecord } from '@radial/core' export const FAKE_PDS = 'https://pds.test' /** * A repo serves one value per record — the latest it holds — not the version history the fixture * carries. Collapsing to that is what makes a fixture into something a PDS could actually return. */ export function latestVersions(records: StoredRecord[]): StoredRecord[] { const latest = new Map() for (const record of records) { const key = `${record.did} ${record.collection} ${record.rkey}` const held = latest.get(key) if (!held || held.rev < record.rev) latest.set(key, record) } return [...latest.values()] } /** A `localStorage` that lives and dies with the test, so one test cannot warm another's cache. */ export function memoryStorage(): Pick & { keys(): string[] dump(): string } { const values = new Map() return { getItem: (key) => values.get(key) ?? null, setItem: (key, value) => void values.set(key, value), removeItem: (key) => void values.delete(key), keys: () => [...values.keys()].sort(), dump: () => JSON.stringify([...values]), } } export interface RecordedCall { method: string did: string detail: string authorization?: string } export class FakePds { readonly repos = new Map() readonly heads = new Map() readonly calls: RecordedCall[] = [] /** * DID → the blob CID in that repo's `app.bsky.actor.profile`. A profile record is not a Radial * record and lives outside `repos` for that reason: what the UI reads from it is one blob ref, and * a DID absent here answers `RecordNotFound` exactly as a repo with no profile would. */ readonly profiles: Record = {} /** * Blob CID → the bytes uploaded under it. A real PDS content-addresses the blob and reclaims one * no record references; this keeps them forever, which is what lets a test assert that the record * anchoring one was written rather than assert on a collection policy nobody implements here. */ readonly blobs = new Map() /** Every request throws while this is set: the member PDS, or the tab, has gone away. */ offline = false #minted = 0 constructor( records: StoredRecord[], readonly handles: Record = {}, profiles: Record = {}, ) { Object.assign(this.profiles, profiles) for (const record of records) this.append(record) this.calls.length = 0 } /** What a write does: put a record in its author's repo and move that repo's head. */ append(record: StoredRecord): void { this.repos.set(record.did, [...(this.repos.get(record.did) ?? []), record]) this.heads.set(record.did, (this.heads.get(record.did) ?? 0) + 1) } counts(method: string): number { return this.calls.filter((call) => call.method === method).length } reset(): void { this.calls.length = 0 } #latest(did: string, collection: string, rkey: string): StoredRecord | undefined { return (this.repos.get(did) ?? []) .filter((record) => record.collection === collection && record.rkey === rkey) .at(-1) } readonly fetch = async (input: string | URL, init?: RequestInit): Promise => { if (this.offline) throw new Error('offline') const url = new URL(input) const json = (value: unknown, status = 200): Response => new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json' } }) const authorization = new Headers(init?.headers).get('authorization') ?? undefined const record = (method: string, did: string, detail = ''): void => { this.calls.push({ method, did, detail, ...(authorization ? { authorization } : {}) }) } if (url.hostname === 'plc.directory') { const did = decodeURIComponent(url.pathname.slice(1)) record('didDocument', did) const handle = this.handles[did] return json({ id: did, ...(handle ? { alsoKnownAs: [`at://${handle}`] } : {}), service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: FAKE_PDS }], }) } const method = url.pathname.slice('/xrpc/'.length) if (method === 'com.atproto.identity.resolveHandle') { const handle = url.searchParams.get('handle') ?? '' record('resolveHandle', '', handle) const did = Object.entries(this.handles).find(([, value]) => value === handle)?.[0] return did ? json({ did }) : json({ error: 'InvalidRequest' }, 400) } if (method === 'com.atproto.sync.getLatestCommit') { const did = url.searchParams.get('did') ?? '' record('getLatestCommit', did) const head = this.heads.get(did) ?? 0 return json({ rev: String(head).padStart(13, '0'), cid: `commit-${did}-${head}` }) } if (method === 'com.atproto.repo.describeRepo') { const did = url.searchParams.get('repo') ?? '' record('describeRepo', did) return json({ collections: [...new Set((this.repos.get(did) ?? []).map((entry) => entry.collection))], }) } if (method === 'com.atproto.repo.listRecords') { const did = url.searchParams.get('repo') ?? '' const collection = url.searchParams.get('collection') ?? '' record('listRecords', did, collection) const rows = (this.repos.get(did) ?? []).filter((entry) => entry.collection === collection) return json({ records: rows.map(({ uri, cid, value }) => ({ uri, cid, value })) }) } if (method === 'com.atproto.repo.getRecord') { const did = url.searchParams.get('repo') ?? '' const collection = url.searchParams.get('collection') ?? '' const rkey = url.searchParams.get('rkey') ?? '' record('getRecord', did, `${collection}/${rkey}`) if (collection === 'app.bsky.actor.profile') { const blob = this.profiles[did] if (!blob || rkey !== 'self') return json({ error: 'RecordNotFound', message: 'not found' }, 400) return json({ uri: `at://${did}/${collection}/${rkey}`, cid: `cid-profile-${did}`, value: { $type: collection, avatar: { $type: 'blob', ref: { $link: blob }, mimeType: 'image/jpeg', size: 1024 }, }, }) } const found = this.#latest(did, collection, rkey) if (!found) return json({ error: 'RecordNotFound', message: 'not found' }, 400) return json({ uri: found.uri, cid: found.cid, value: found.value }) } if (method === 'com.atproto.repo.uploadBlob') { // A PDS answers with the ref a record then embeds; the content type is the request's own // header, which is why `RepoWriter` sets it rather than putting it in a body. const bytes = new Uint8Array(await new Response(init?.body as BodyInit).arrayBuffer()) const mimeType = new Headers(init?.headers).get('content-type') ?? 'application/octet-stream' this.#minted += 1 const link = `bafkreiblob${this.#minted}` record('uploadBlob', '', `${mimeType} ${bytes.length}`) this.blobs.set(link, bytes) return json({ blob: { $type: 'blob', ref: { $link: link }, mimeType, size: bytes.length } }) } if (method === 'com.atproto.sync.getBlob') { const cid = url.searchParams.get('cid') ?? '' record('getBlob', url.searchParams.get('did') ?? '', cid) const bytes = this.blobs.get(cid) if (!bytes) return json({ error: 'BlobNotFound', message: 'not found' }, 404) return new Response(bytes.slice().buffer as ArrayBuffer, { status: 200, headers: { 'content-type': 'application/octet-stream' }, }) } if (method === 'com.atproto.repo.createRecord') { const body = JSON.parse(String(init?.body ?? '{}')) as { repo: string collection: string rkey?: string record: StoredRecord['value'] } record('createRecord', body.repo, body.collection) this.#minted += 1 const rkey = body.rkey ?? `minted${this.#minted}` const uri = `at://${body.repo}/${body.collection}/${rkey}` const cid = `cid-${rkey}` // The `rev` a materializer folds on is the repo revision the record was *observed* at, which // the poller stamps on its next scan; what a PDS stores is just the value. this.append({ did: body.repo, collection: body.collection, rkey, uri, cid, rev: String(this.heads.get(body.repo) ?? 0).padStart(13, '0'), value: body.record, } as StoredRecord) return json({ uri, cid }) } if (method === 'com.atproto.repo.putRecord') { const body = JSON.parse(String(init?.body ?? '{}')) as { repo: string collection: string rkey: string swapRecord?: string record: StoredRecord['value'] } record('putRecord', body.repo, `${body.collection}/${body.rkey}`) const held = this.#latest(body.repo, body.collection, body.rkey) if (!held) return json({ error: 'RecordNotFound', message: 'not found' }, 400) // The whole of the concurrency story for the one sanctioned edit: a value derived from a stale // read loses the race here rather than silently overwriting the version it never saw. if (body.swapRecord && body.swapRecord !== held.cid) { return json({ error: 'InvalidSwap', message: `record is not ${body.swapRecord}` }, 400) } this.#minted += 1 const cid = `cid-${body.rkey}-e${this.#minted}` this.repos.set(body.repo, [ ...(this.repos.get(body.repo) ?? []).filter( (entry) => !(entry.collection === body.collection && entry.rkey === body.rkey), ), { ...held, cid, value: body.record }, ]) this.heads.set(body.repo, (this.heads.get(body.repo) ?? 0) + 1) return json({ uri: held.uri, cid }) } throw new Error(`unexpected request: ${url}`) } }