// The dev.pdsjs.git XRPC methods, driven through pds.fetch against fake // storage: anonymous reads of the public repo, the account session for // spaces and writes, bearer tokens checked against repo scopes. import { cborEncodeDagCbor } from '@pdsjs/core/repo'; import { describe, expect, it } from 'vitest'; import { createAccessJwt } from '../../../packages/core/src/auth.js'; import { apiGet, BASE, createActorStorage, createPds as createBasePds, DID, SECRET, sessionCookie, } from '../../../test/helpers/account-api-harness.js'; import { createFakeSpaceStorage } from '../../../test/helpers/fake-space-storage.js'; import { GIT_DEFS, GIT_METHODS, gitDefsLexicon, gitMethodLexicons, } from '../src/api-lexicon.js'; import { createGitHosting } from '../src/hosting.js'; import { createGitMethods } from '../src/methods.js'; const SPACE = `at://${DID}/space/com.example.team/main`; /** The space a private repository named proj keeps. */ /** The space a private repository lives in, keyed by its name. */ const ownSpace = (/** @type {string} */ rkey) => `at://${DID}/space/dev.pdsjs.git.space/${rkey}`; const SHA = 'a'.repeat(40); const REPO_RECORD = { $type: 'dev.pdsjs.git.repo', name: 'myrepo', defaultBranch: 'main', refs: [{ name: 'refs/heads/main', sha: SHA }], bundles: [], createdAt: '2026-08-01T00:00:00.000Z', }; function gitActorStorage() { const encoded = cborEncodeDagCbor(REPO_RECORD); return createActorStorage({ async getRecord(/** @type {string} */ uri) { return uri === `at://${DID}/dev.pdsjs.git.repo/myrepo` ? { cid: 'bafyrepo', value: encoded } : null; }, async listRecords(/** @type {string} */ collection) { if (collection !== 'dev.pdsjs.git.repo') { return { records: [], cursor: null }; } return { records: [ { uri: `at://${DID}/dev.pdsjs.git.repo/myrepo`, cid: 'bafyrepo', rkey: 'myrepo', value: encoded, }, ], cursor: null, }; }, }); } /** A browser port that records calls and answers canned views. */ function fakeBrowser() { /** @type {Array<{method: string, args: unknown[]}>} */ const calls = []; /** @param {string} method @param {unknown[]} args */ const note = (method, args) => calls.push({ method, args }); /** @param {string|undefined} ref */ const check = (ref) => { if (ref === 'huge') { throw Object.assign(new Error('repository huge has no object indexes'), { code: 'RepositoryTooLarge', }); } if (ref && ref !== 'main') throw new Error(`no such ref: ${ref}`); }; return { calls, async listTree(/** @type {unknown[]} */ ...args) { note('listTree', args); check(/** @type {string|undefined} */ (args[2])); return { ref: 'refs/heads/main', sha: SHA, path: args[3], entries: [ { name: 'src', type: 'dir', mode: '40000', sha: SHA, size: null }, ], }; }, async readFile(/** @type {unknown[]} */ ...args) { note('readFile', args); return { ref: 'refs/heads/main', path: args[3], size: 3, mode: '100644', mediaType: null, binary: false, truncated: false, text: 'hi\n', }; }, async readBytes(/** @type {unknown[]} */ ...args) { note('readBytes', args); const name = String(args[3]); return { ref: 'refs/heads/main', path: args[3], size: 3, mode: '100644', mediaType: name.endsWith('.png') ? 'image/png' : null, binary: name.endsWith('.bin') || name.endsWith('.png'), bytes: new Uint8Array([104, 105, 10]), }; }, async listFiles(/** @type {unknown[]} */ ...args) { note('listFiles', args); return { ref: 'refs/heads/main', files: [{ path: 'README.md', size: 3 }], truncated: false, }; }, async listCommits(/** @type {unknown[]} */ ...args) { note('listCommits', args); return { ref: 'refs/heads/main', commits: [], truncated: false }; }, async readCommit(/** @type {unknown[]} */ ...args) { note('readCommit', args); return { sha: SHA, parents: [], author: 'A ', authoredAt: 1, committedAt: 1, message: 'm', files: [ { path: 'logo.png', status: 'modified', binary: true, mediaType: 'image/png', oldImage: new Uint8Array([1]), newImage: new Uint8Array([2]), hunks: null, additions: 0, deletions: 0, }, ], }; }, async lastCommit(/** @type {unknown[]} */ ...args) { note('lastCommit', args); return { sha: SHA, parents: [], author: 'A', message: 'm' }; }, }; } /** * @param {Object} [options] * @param {Object} [options.actorStorage] * @param {Object} [options.spaceStorage] * @param {Object} [options.spaceAdmin] * @param {ReturnType|null} [options.browser] * @param {boolean} [options.readOnly] * @param {(who: {did: string, handle: string, pds: string}) => unknown} [options.openCopy] - a browser over another account's copy, for the reads that name one */ function createPds({ actorStorage = gitActorStorage(), spaceStorage, spaceAdmin, browser = fakeBrowser(), readOnly = false, openCopy, } = {}) { return createBasePds({ actorStorage, spaceStorage, readOnly, extensions: [ (host) => ({ name: 'git', collections: [ 'dev.pdsjs.git.repo', 'dev.pdsjs.git.config', 'dev.pdsjs.git.checkRequest', ], routes: createGitMethods({ host, browser: /** @type {any} */ (browser), openCopy: /** @type {any} */ (openCopy), hosting: createGitHosting({ host, httpEnabled: false, spaceBrowser: host.spaceBrowser ?? null, spaceAdmin: /** @type {any} */ (spaceAdmin ?? null), }), }), }), ], }); } /** @param {string} method @param {Record} [params] */ const path = (method, params = {}) => `/xrpc/dev.pdsjs.git.${method}?${new URLSearchParams(params)}`; /** * @param {ReturnType} pds * @param {string} method * @param {unknown} body * @param {Record} [headers] */ const call = (pds, method, body, headers = {}) => pds.fetch( new Request(`${BASE}/xrpc/dev.pdsjs.git.${method}`, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body: typeof body === 'string' ? body : JSON.stringify(body), }), ); /** @param {string} scope */ const bearer = async (scope) => ({ authorization: `Bearer ${await createAccessJwt(DID, SECRET, 3600, scope)}`, }); describe('git XRPC reads', () => { it('lists the public repositories to anyone, with CORS', async () => { const pds = createPds(); const res = await pds.fetch(new Request(BASE + path('listRepos'))); expect(res.status).toBe(200); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); expect(res.headers.get('Cache-Control')).toBe('no-cache'); const body = await res.json(); expect(body.httpEnabled).toBe(false); expect(body.repos.map((/** @type {{name: string}} */ r) => r.name)).toEqual( ['myrepo'], ); expect(body.repos[0].cloneUrl).toBe('atproto://test.handle/myrepo'); }); it('summarizes pull requests, issues and activity only when asked', async () => { const pds = createPds(); const plain = await ( await pds.fetch(new Request(BASE + path('listRepos'))) ).json(); expect(plain.repos[0].openPulls).toBeUndefined(); expect(plain.repos[0].openIssues).toBeUndefined(); expect(plain.repos[0].activity).toBeUndefined(); const summarized = await ( await pds.fetch( new Request(BASE + path('listRepos', { summary: 'true' })), ) ).json(); expect(summarized.repos[0].openPulls).toBe(0); expect(summarized.repos[0].openIssues).toBe(0); expect(Array.isArray(summarized.repos[0].activity)).toBe(true); }); it('lists the repositories in spaces to the account session only', async () => { const spaceStorage = createFakeSpaceStorage({ spaces: [{ uri: SPACE, deletedAt: null }], records: { [SPACE]: [ { collection: 'dev.pdsjs.git.repo', rkey: 'private', cid: 'bafyprivate', value: { name: 'private', refs: [], bundles: [] }, }, ], }, }); const pds = createPds({ spaceStorage }); const anonymous = await ( await pds.fetch(new Request(BASE + path('listRepos'))) ).json(); expect( anonymous.repos.map((/** @type {{name: string}} */ r) => r.name), ).toEqual(['myrepo']); const owner = await apiGet(pds, path('listRepos'), await sessionCookie()); expect(owner.headers.get('Cache-Control')).toBe('no-store'); const names = (await owner.json()).repos.map( (/** @type {{name: string, space: string|null}} */ r) => [ r.name, r.space, ], ); expect(names).toEqual([ ['myrepo', null], ['private', SPACE], ]); }); it('answers one repository, and names the missing ones', async () => { const pds = createPds(); const found = await pds.fetch( new Request(BASE + path('getRepo', { repo: 'myrepo' })), ); expect(found.status).toBe(200); expect((await found.json()).defaultBranch).toBe('main'); const missing = await pds.fetch( new Request(BASE + path('getRepo', { repo: 'nope' })), ); expect(missing.status).toBe(404); expect((await missing.json()).error).toBe('RepoNotFound'); const unnamed = await pds.fetch(new Request(BASE + path('getRepo'))); expect(unnamed.status).toBe(400); expect((await unnamed.json()).error).toBe('InvalidRequest'); }); it('reads a tree under the record cid as its ETag', async () => { const browser = fakeBrowser(); const pds = createPds({ browser }); const url = BASE + path('getTree', { repo: 'myrepo', path: 'src' }); const res = await pds.fetch(new Request(url)); expect(res.status).toBe(200); expect(res.headers.get('ETag')).toBe('"bafyrepo"'); expect(res.headers.get('Cache-Control')).toBe('no-cache'); expect((await res.json()).entries[0].name).toBe('src'); expect(browser.calls[0]).toEqual({ method: 'listTree', args: [DID, 'myrepo', undefined, 'src', undefined], }); const cached = await pds.fetch( new Request(url, { headers: { 'if-none-match': '"bafyrepo"' } }), ); expect(cached.status).toBe(304); expect(browser.calls).toHaveLength(1); }); it('answers immutably under the current cid and refuses a stale one', async () => { const pds = createPds(); const current = await pds.fetch( new Request(BASE + path('getTree', { repo: 'myrepo', cid: 'bafyrepo' })), ); expect(current.status).toBe(200); expect(current.headers.get('Cache-Control')).toBe( 'public, max-age=31536000, immutable', ); const stale = await pds.fetch( new Request(BASE + path('getTree', { repo: 'myrepo', cid: 'bafyold' })), ); expect(stale.status).toBe(404); expect((await stale.json()).error).toBe('RepoNotFound'); }); it('reads a space repository through the account session alone', async () => { const browser = fakeBrowser(); const spaceStorage = createFakeSpaceStorage({ spaces: [{ uri: SPACE, deletedAt: null }], records: { [SPACE]: [ { collection: 'dev.pdsjs.git.repo', rkey: 'private', cid: 'bafyprivate', value: { name: 'private', refs: [], bundles: [] }, }, ], }, }); const pds = createPds({ browser, spaceStorage }); const query = path('getTree', { repo: 'private', space: SPACE }); const anonymous = await pds.fetch(new Request(BASE + query)); expect(anonymous.status).toBe(401); expect((await anonymous.json()).error).toBe('AuthenticationRequired'); const owner = await apiGet(pds, query, await sessionCookie()); expect(owner.status).toBe(200); expect(owner.headers.get('Cache-Control')).toBe('no-store'); expect(owner.headers.get('ETag')).toBeNull(); expect(browser.calls[0].args).toEqual([ DID, 'private', undefined, '', SPACE, ]); }); it('reads a space repository with a bearer token naming the account', async () => { const browser = fakeBrowser(); const spaceStorage = createFakeSpaceStorage({ spaces: [{ uri: SPACE, deletedAt: null }], records: { [SPACE]: [ { collection: 'dev.pdsjs.git.repo', rkey: 'private', cid: 'bafyprivate', value: { name: 'private', refs: [], bundles: [] }, }, ], }, }); const pds = createPds({ browser, spaceStorage }); const query = path('getTree', { repo: 'private', space: SPACE }); const res = await pds.fetch( new Request(BASE + query, { headers: await bearer('atproto') }), ); expect(res.status).toBe(200); expect(browser.calls[0].args).toEqual([ DID, 'private', undefined, '', SPACE, ]); }); it('refuses a space read to another account’s bearer token', async () => { const browser = fakeBrowser(); const spaceStorage = createFakeSpaceStorage({ spaces: [{ uri: SPACE, deletedAt: null }], records: { [SPACE]: [ { collection: 'dev.pdsjs.git.repo', rkey: 'private', cid: 'bafyprivate', value: { name: 'private', refs: [], bundles: [] }, }, ], }, }); const pds = createPds({ browser, spaceStorage }); const query = path('getTree', { repo: 'private', space: SPACE }); const other = { authorization: `Bearer ${await createAccessJwt( 'did:plc:stranger', SECRET, 3600, 'atproto', )}`, }; const res = await pds.fetch(new Request(BASE + query, { headers: other })); expect(res.status).toBe(401); expect((await res.json()).error).toBe('AuthenticationRequired'); }); it('lists the spaces to a bearer token naming the account', async () => { const spaceStorage = createFakeSpaceStorage({ spaces: [{ uri: SPACE, deletedAt: null }], records: { [SPACE]: [ { collection: 'dev.pdsjs.git.repo', rkey: 'private', cid: 'bafyprivate', value: { name: 'private', refs: [], bundles: [] }, }, ], }, }); const pds = createPds({ spaceStorage }); const owner = await pds.fetch( new Request(BASE + path('listRepos'), { headers: await bearer('atproto'), }), ); const names = (await owner.json()).repos.map( (/** @type {{name: string, space: string|null}} */ r) => [ r.name, r.space, ], ); expect(names).toEqual([ ['myrepo', null], ['private', SPACE], ]); }); it('reports an unknown ref as NotFound', async () => { const pds = createPds(); const res = await pds.fetch( new Request(BASE + path('getTree', { repo: 'myrepo', ref: 'gone' })), ); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: 'NotFound', message: 'no such ref: gone', }); }); it('names a repository the server will not download whole', async () => { const pds = createPds(); const res = await pds.fetch( new Request(BASE + path('getTree', { repo: 'myrepo', ref: 'huge' })), ); expect(res.status).toBe(400); expect((await res.json()).error).toBe('RepositoryTooLarge'); }); it('lists every file at a ref', async () => { const browser = fakeBrowser(); const pds = createPds({ browser }); const res = await pds.fetch( new Request(BASE + path('listFiles', { repo: 'myrepo', ref: 'main' })), ); expect(res.status).toBe(200); expect((await res.json()).files).toEqual([{ path: 'README.md', size: 3 }]); expect(browser.calls[0].args).toEqual([DID, 'myrepo', 'main', undefined]); }); it('reads a file, and needs its path', async () => { const pds = createPds(); const unnamed = await pds.fetch( new Request(BASE + path('getFile', { repo: 'myrepo' })), ); expect(unnamed.status).toBe(400); expect((await unnamed.json()).message).toMatch(/path/); const res = await pds.fetch( new Request(BASE + path('getFile', { repo: 'myrepo', path: 'a.txt' })), ); expect(res.status).toBe(200); expect((await res.json()).text).toBe('hi\n'); }); it('serves raw bytes as text, an octet stream or an image, never as a page', async () => { const pds = createPds(); const text = await pds.fetch( new Request(BASE + path('getRaw', { repo: 'myrepo', path: 'x.html' })), ); expect(text.status).toBe(200); expect(text.headers.get('Content-Type')).toBe('text/plain; charset=utf-8'); expect(text.headers.get('X-Content-Type-Options')).toBe('nosniff'); expect(text.headers.get('Content-Security-Policy')).toContain('sandbox'); expect(text.headers.get('ETag')).toBe('"bafyrepo"'); expect(await text.text()).toBe('hi\n'); const binary = await pds.fetch( new Request(BASE + path('getRaw', { repo: 'myrepo', path: 'x.bin' })), ); expect(binary.headers.get('Content-Type')).toBe('application/octet-stream'); const image = await pds.fetch( new Request(BASE + path('getRaw', { repo: 'myrepo', path: 'x.png' })), ); expect(image.headers.get('Content-Type')).toBe('image/png'); expect(image.headers.get('Content-Security-Policy')).toContain('sandbox'); }); it('clamps the commit limit', async () => { const browser = fakeBrowser(); const pds = createPds({ browser }); await pds.fetch( new Request( BASE + path('listCommits', { repo: 'myrepo', limit: '9999' }), ), ); await pds.fetch( new Request(BASE + path('listCommits', { repo: 'myrepo' })), ); expect(browser.calls.map((c) => c.args[3])).toEqual([500, 50]); }); it('answers a commit without the image bytes of its diff', async () => { const pds = createPds(); const res = await pds.fetch( new Request(BASE + path('getCommit', { repo: 'myrepo', sha: SHA })), ); expect(res.status).toBe(200); const { files } = await res.json(); expect(files[0]).not.toHaveProperty('oldImage'); expect(files[0]).not.toHaveProperty('newImage'); expect(files[0].mediaType).toBe('image/png'); }); it('answers the last commit of a path', async () => { const browser = fakeBrowser(); const pds = createPds({ browser }); const res = await pds.fetch( new Request( BASE + path('getLastCommit', { repo: 'myrepo', path: 'README.md' }), ), ); expect((await res.json()).commit.sha).toBe(SHA); expect(browser.calls[0].args).toEqual([ DID, 'myrepo', undefined, 'README.md', undefined, undefined, ]); }); it('answers the checks without a validator, since they live elsewhere', async () => { const pds = createPds(); const res = await pds.fetch( new Request(BASE + path('listChecks', { repo: 'myrepo' })), ); expect(res.status).toBe(200); expect(res.headers.get('ETag')).toBeNull(); expect(res.headers.get('Cache-Control')).toBe('no-store'); expect(await res.json()).toEqual({ runner: null, checks: [] }); }); it('registers the bundle-chain reads only with a browser', async () => { const pds = createPds({ browser: null }); const res = await pds.fetch( new Request(BASE + path('getTree', { repo: 'myrepo' })), ); expect(res.status).toBe(501); const { methods } = await ( await pds.fetch( new Request(`${BASE}/xrpc/community.lexicon.service.describe`), ) ).json(); const names = methods.map((/** @type {{value: string}} */ m) => m.value); expect(names).toContain('dev.pdsjs.git.listRepos'); expect(names).toContain('dev.pdsjs.git.createRepo'); expect(names).not.toContain('dev.pdsjs.git.getTree'); }); }); describe('git XRPC procedures', () => { it('needs the account session or a bearer token', async () => { const pds = createPds(); const res = await call(pds, 'createRepo', { name: 'x' }); expect(res.status).toBe(401); expect((await res.json()).error).toBe('AuthenticationRequired'); }); it('refuses a cross-origin request on the session', async () => { const pds = createPds(); const res = await call( pds, 'createRepo', { name: 'x' }, { cookie: await sessionCookie(), origin: 'https://evil.example' }, ); expect(res.status).toBe(403); expect((await res.json()).error).toBe('Forbidden'); }); it('creates a private repository on the session', async () => { /** @type {string[]} */ const created = []; const pds = createPds({ spaceAdmin: { async createSpace(/** @type {{type: string, skey?: string}} */ opts) { created.push(`space ${opts.type}/${opts.skey ?? ''}`); return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` }; }, async createRecord( /** @type {string} */ space, /** @type {string} */ collection, /** @type {string} */ rkey, ) { created.push(`${space} ${collection}/${rkey}`); }, }, }); const res = await call( pds, 'createRepo', { name: 'proj', private: true }, { cookie: await sessionCookie() }, ); expect(res.status).toBe(200); expect(res.headers.get('Cache-Control')).toBe('no-store'); expect(await res.json()).toEqual({ name: 'proj', space: ownSpace('proj') }); expect(created).toEqual([ 'space dev.pdsjs.git.space/proj', `${ownSpace('proj')} dev.pdsjs.git.repo/proj`, ]); }); it('creates a private repository with the account’s bearer token', async () => { /** @type {string[]} */ const writes = []; const pds = createPds({ spaceAdmin: { async createSpace(/** @type {{type: string, skey?: string}} */ opts) { return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` }; }, async createRecord( /** @type {string} */ space, /** @type {string} */ collection, /** @type {string} */ rkey, ) { writes.push(`${space} ${collection}/${rkey}`); }, }, }); const res = await call( pds, 'createRepo', { name: 'proj', private: true }, await bearer('atproto'), ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ name: 'proj', space: ownSpace('proj') }); expect(writes).toEqual([`${ownSpace('proj')} dev.pdsjs.git.repo/proj`]); }); it('ignores a space the body names: the repository keeps its own', async () => { /** @type {string[]} */ const created = []; const pds = createPds({ spaceAdmin: { async createSpace(/** @type {{type: string, skey?: string}} */ opts) { created.push(`${opts.type}/${opts.skey ?? ''}`); return { uri: `at://${DID}/space/${opts.type}/${opts.skey}` }; }, async createRecord() {}, }, }); const res = await call( pds, 'createRepo', { name: 'proj', private: true, space: SPACE, type: 'com.example.other' }, await bearer('atproto'), ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ name: 'proj', space: ownSpace('proj') }); expect(created).toEqual(['dev.pdsjs.git.space/proj']); }); it('checks a bearer token against the repo scopes it writes', async () => { const pds = createPds(); const denied = await call( pds, 'createRepo', { name: 'myrepo' }, await bearer('repo:com.example.other'), ); expect(denied.status).toBe(403); expect(await denied.json()).toEqual({ error: 'InvalidScope', message: 'Missing required scope to create dev.pdsjs.git.repo', }); const allowed = await call( pds, 'createRepo', { name: 'myrepo' }, await bearer('repo:dev.pdsjs.git.repo'), ); expect(allowed.status).toBe(400); expect((await allowed.json()).error).toBe('RepoExists'); }); it('refuses every write on a read-only server', async () => { const pds = createPds({ readOnly: true }); const res = await call( pds, 'requestCheck', { name: 'myrepo', ref: 'refs/heads/main' }, { cookie: await sessionCookie() }, ); expect(res.status).toBe(401); expect((await res.json()).message).toBe('This PDS is read-only'); }); it('names the refusals the lexicons list', async () => { const pds = createPds(); const noRunner = await call( pds, 'requestCheck', { name: 'myrepo', ref: 'refs/heads/main' }, { cookie: await sessionCookie() }, ); expect(noRunner.status).toBe(400); expect((await noRunner.json()).error).toBe('NoRunner'); const missing = await call( pds, 'updateSettings', { name: 'nope', description: 'x' }, await bearer('com.atproto.access'), ); expect(missing.status).toBe(404); expect((await missing.json()).error).toBe('RepoNotFound'); }); it('refuses a body that is not a JSON object', async () => { const pds = createPds(); const cookie = await sessionCookie(); const broken = await call(pds, 'createRepo', '{not json', { cookie }); expect(broken.status).toBe(400); const list = await call(pds, 'createRepo', ['x'], { cookie }); expect(list.status).toBe(400); expect((await list.json()).message).toMatch(/JSON object/); }); }); describe('git method lexicons', () => { it('describe the methods the extension registers, and no others', async () => { const pds = createPds(); const { methods } = await ( await pds.fetch( new Request(`${BASE}/xrpc/community.lexicon.service.describe`), ) ).json(); const served = methods .map((/** @type {{value: string}} */ m) => m.value) .filter((/** @type {string} */ nsid) => nsid.startsWith('dev.pdsjs.git.'), ); expect(served.sort()).toEqual([...GIT_METHODS].sort()); }); it('resolve every local ref within the defs', () => { /** @param {unknown} node @param {string[]} into */ const refsOf = (node, into) => { if (Array.isArray(node)) { for (const item of node) refsOf(item, into); } else if (node && typeof node === 'object') { const record = /** @type {Record} */ (node); if (typeof record.ref === 'string') into.push(record.ref); for (const value of Object.values(record)) refsOf(value, into); } return into; }; const defs = Object.keys(gitDefsLexicon.defs); for (const lexicon of [gitDefsLexicon, ...gitMethodLexicons]) { for (const ref of refsOf(lexicon.defs, [])) { const [nsid, def] = ref.split('#'); if (nsid === '' || nsid === GIT_DEFS) { expect(defs, `${lexicon.id} refers to ${ref}`).toContain(def); } else { expect(nsid).toMatch(/^dev\.pdsjs\.git\./); } } expect(lexicon.lexicon).toBe(1); } for (const lexicon of gitMethodLexicons) { expect(['query', 'procedure']).toContain(lexicon.defs.main.type); } }); }); describe("a read of another account's copy", () => { const BOB = 'did:plc:bob'; /** * The world beyond this server: bob has a document and a copy, nobody else * does. The copy is listed rather than fetched by key, because a copy is * its holder's own record and carries their own key. */ function directory() { const real = globalThis.fetch; globalThis.fetch = async (/** @type {any} */ input) => { const url = new URL(String(input)); if (decodeURIComponent(url.pathname) === `/${BOB}`) { return Response.json({ alsoKnownAs: ['at://bob.test'], service: [ { id: '#atproto_pds', serviceEndpoint: 'https://bob.test' }, ], }); } if ( url.host === 'bob.test' && url.pathname === '/xrpc/com.atproto.repo.listRecords' && url.searchParams.get('collection') === 'dev.pdsjs.git.repo' ) { return Response.json({ records: [ { uri: `at://${BOB}/dev.pdsjs.git.repo/3lbobskey`, cid: 'bafybobcopy', value: { name: 'myrepo', refs: [], bundles: [] }, }, ], }); } return new Response('no', { status: 404 }); }; return () => (globalThis.fetch = real); } /** A browser over bob's copy, noting what it is asked. */ function copyBrowser() { /** @type {Array<{method: string, args: unknown[]}>} */ const calls = []; const browser = { readCommit: async (/** @type {unknown[]} */ ...args) => { calls.push({ method: 'readCommit', args }); return { sha: SHA, parents: [], message: 'in the copy', files: [] }; }, readRange: async (/** @type {unknown[]} */ ...args) => { calls.push({ method: 'readRange', args }); return { base: SHA, sha: SHA, parents: [], message: 'across', files: [], }; }, listRange: async (/** @type {unknown[]} */ ...args) => { calls.push({ method: 'listRange', args }); return { commits: [], truncated: false }; }, listTree: async (/** @type {unknown[]} */ ...args) => { calls.push({ method: 'listTree', args }); return { ref: 'main', sha: SHA, path: '', entries: [] }; }, }; return { calls, browser }; } it('reads the copy through the server, and says whose copy answered', async () => { const restore = directory(); const copy = copyBrowser(); try { const pds = createPds({ openCopy: () => copy.browser }); const res = await pds.fetch( new Request( BASE + path('getCommit', { repo: 'myrepo', sha: SHA, from: BOB }), ), ); expect(res.status).toBe(200); const body = await res.json(); expect(body.message).toBe('in the copy'); expect(body.copy).toEqual({ did: BOB, handle: 'bob.test', pds: 'https://bob.test', }); // The copy's account is what the browser reads, not this one, and it // is read at the key bob's own record carries. expect(copy.calls[0].method).toBe('readCommit'); expect(copy.calls[0].args.slice(0, 3)).toEqual([BOB, '3lbobskey', SHA]); // A copy's version is theirs, so nothing here validates it. expect(res.headers.get('etag')).toBeNull(); expect(res.headers.get('cache-control')).toBe('no-store'); } finally { restore(); } }); it('reads a range as one diff, and lists the commits across it', async () => { const restore = directory(); const copy = copyBrowser(); try { const pds = createPds({ openCopy: () => copy.browser }); const base = 'b'.repeat(40); const diff = await pds.fetch( new Request( BASE + path('getCommit', { repo: 'myrepo', sha: `${base}..${SHA}`, from: BOB, }), ), ); expect(diff.status).toBe(200); expect((await diff.json()).message).toBe('across'); const log = await pds.fetch( new Request( BASE + path('listCommits', { repo: 'myrepo', range: `${base}..${SHA}`, from: BOB, }), ), ); expect(log.status).toBe(200); expect(copy.calls.map((one) => one.method)).toEqual([ 'readRange', 'listRange', ]); expect(copy.calls[0].args.slice(0, 4)).toEqual([ BOB, '3lbobskey', base, SHA, ]); } finally { restore(); } }); it('answers NotFound for an account with no server, and refuses a copy in a space', async () => { const restore = directory(); try { const pds = createPds({ openCopy: () => copyBrowser().browser }); const nobody = await pds.fetch( new Request( BASE + path('getTree', { repo: 'myrepo', from: 'did:plc:nobody' }), ), ); expect(nobody.status).toBe(404); expect((await nobody.json()).error).toBe('NotFound'); const spaced = await pds.fetch( new Request( BASE + path('getTree', { repo: 'myrepo', from: BOB, space: 'at://did:plc:x/com.example.t/s', }), ), ); expect(spaced.status).toBe(400); } finally { restore(); } }); it("reads this account's own copy where from names it", async () => { const restore = directory(); const copy = copyBrowser(); try { const pds = createPds({ openCopy: () => copy.browser }); const res = await pds.fetch( new Request(BASE + path('getTree', { repo: 'myrepo', from: DID })), ); expect(res.status).toBe(200); expect(copy.calls).toEqual([]); expect(res.headers.get('etag')).not.toBeNull(); } finally { restore(); } }); });