import { describe, expect, it } from 'vitest'; import { CONSTELLATION_URL, createConstellationDiscovery, noDiscovery, } from '../src/discovery.js'; const SUBJECT = 'at://did:plc:alice/dev.pdsjs.git.repo/demo-app'; /** * A fake index answering from a script of pages, recording each request. * @param {Array<{records?: unknown[], cursor?: unknown}|Response>} pages */ function fakeIndex(pages) { /** @type {Array<{url: URL, init: RequestInit|undefined}>} */ const asked = []; /** @type {typeof fetch} */ const send = async (input, init) => { asked.push({ url: new URL(String(input)), init }); const page = pages.shift(); if (page instanceof Response) return page; return Response.json(page ?? {}); }; return { asked, send }; } describe('createConstellationDiscovery', () => { it('asks for the collection on the subject.uri path, naming itself', async () => { const index = fakeIndex([ { records: [ { did: 'did:plc:bob', collection: 'dev.pdsjs.git.pull', rkey: '3a' }, ], cursor: null, }, ]); const discovery = createConstellationDiscovery({ fetch: index.send, userAgent: 'test (@example.com)', }); const found = await discovery.about(SUBJECT, 'dev.pdsjs.git.pull'); expect(found).toEqual([ { did: 'did:plc:bob', collection: 'dev.pdsjs.git.pull', rkey: '3a' }, ]); const [{ url, init }] = index.asked; expect(url.origin).toBe(CONSTELLATION_URL); expect(url.pathname).toBe('/xrpc/blue.microcosm.links.getBacklinks'); expect(url.searchParams.get('subject')).toBe(SUBJECT); expect(url.searchParams.get('source')).toBe( 'dev.pdsjs.git.pull:subject.uri', ); expect(url.searchParams.get('limit')).toBe('100'); expect(new Headers(init?.headers).get('user-agent')).toBe( 'test (@example.com)', ); }); it('asks on another path where the link is elsewhere in the record', async () => { const index = fakeIndex([{ records: [] }]); await createConstellationDiscovery({ fetch: index.send }).about( SUBJECT, 'dev.pdsjs.git.repo', 'upstream', ); expect(index.asked[0].url.searchParams.get('source')).toBe( 'dev.pdsjs.git.repo:upstream', ); }); it('follows the cursor until the index says there is no more', async () => { const index = fakeIndex([ { records: [{ did: 'did:plc:a', collection: 'c', rkey: '1' }], cursor: 'next', }, { records: [{ did: 'did:plc:b', collection: 'c', rkey: '2' }], cursor: null, }, ]); const discovery = createConstellationDiscovery({ fetch: index.send }); const found = await discovery.about(SUBJECT, 'c'); expect(found.map((one) => one.rkey)).toEqual(['1', '2']); expect(index.asked).toHaveLength(2); expect(index.asked[0].url.searchParams.has('cursor')).toBe(false); expect(index.asked[1].url.searchParams.get('cursor')).toBe('next'); }); it('stops at the page limit rather than following a cursor forever', async () => { const endless = Array.from({ length: 5 }, (_, at) => ({ records: [{ did: 'did:plc:a', collection: 'c', rkey: String(at) }], cursor: 'more', })); const index = fakeIndex(endless); const discovery = createConstellationDiscovery({ fetch: index.send, maxPages: 3, }); const found = await discovery.about(SUBJECT, 'c'); expect(found).toHaveLength(3); expect(index.asked).toHaveLength(3); }); it('drops an entry missing any part of its address', async () => { const index = fakeIndex([ { records: [ { did: 'did:plc:a', collection: 'c', rkey: '1' }, { did: 'did:plc:a', collection: 'c' }, { collection: 'c', rkey: '2' }, null, 'nonsense', ], }, ]); const discovery = createConstellationDiscovery({ fetch: index.send }); expect(await discovery.about(SUBJECT, 'c')).toEqual([ { did: 'did:plc:a', collection: 'c', rkey: '1' }, ]); }); it('answers what it has when the index refuses or fails', async () => { // A refusal on the second page keeps the first. const refused = fakeIndex([ { records: [{ did: 'did:plc:a', collection: 'c', rkey: '1' }], cursor: 'next', }, new Response('no', { status: 503 }), ]); expect( await createConstellationDiscovery({ fetch: refused.send }).about( SUBJECT, 'c', ), ).toEqual([{ did: 'did:plc:a', collection: 'c', rkey: '1' }]); // A network failure on the first page is an empty answer, not an error. const down = createConstellationDiscovery({ fetch: async () => { throw new TypeError('fetch failed'); }, }); expect(await down.about(SUBJECT, 'c')).toEqual([]); // A body that is not JSON reads the same way. const garbled = createConstellationDiscovery({ fetch: async () => new Response(''), }); expect(await garbled.about(SUBJECT, 'c')).toEqual([]); }); it('asks the service it was given, without a trailing slash doubled', async () => { const index = fakeIndex([{ records: [] }]); const discovery = createConstellationDiscovery({ service: 'https://index.example/', fetch: index.send, }); await discovery.about(SUBJECT, 'c'); expect( index.asked[0].url.href.startsWith('https://index.example/xrpc/'), ).toBe(true); }); }); describe('createConstellationDiscovery, on the global fetch', () => { it('carries the request on globalThis.fetch when given no carrier', async () => { const real = globalThis.fetch; /** @type {string[]} */ const asked = []; globalThis.fetch = async (input) => { asked.push(String(input)); return Response.json({ records: [], cursor: null }); }; try { expect(await createConstellationDiscovery().about(SUBJECT, 'c')).toEqual( [], ); expect(asked[0]?.startsWith(`${CONSTELLATION_URL}/xrpc/`)).toBe(true); } finally { globalThis.fetch = real; } }); }); describe('noDiscovery', () => { it('finds nothing', async () => { expect(await noDiscovery().about(SUBJECT, 'dev.pdsjs.git.pull')).toEqual( [], ); }); });