Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
8.8 kB · 249 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250// The subscriber row against a stubbed network: the two at-uri forms a// subscription can name a publication by, the intersection with the viewer's// own follows, and the failure modes that have to cost the row rather than// the popup.
import { afterEach, describe, expect, it, vi } from 'vitest'import { CONSTELLATION, FOLLOWS_CAP, distinctDidsUrl, followedSubscribers, followingDids, scanSubscribers, subscriptionTargets, summarizeSubscribers,} from './subscribers'
const PUB_DID = 'did:plc:subscriberstest000000000'const PUB = `at://${PUB_DID}/site.standard.publication/self`const ME = 'did:plc:viewer000000000000000000'const FRIEND = 'did:plc:friend000000000000000000'const STRANGER = 'did:plc:stranger00000000000000'
/** A PDS + DID document for anyone, so faces and follows resolve. */function identityRoutes(url: string): Response | undefined { if (url.startsWith('https://plc.directory/')) { const did = url.slice('https://plc.directory/'.length) return new Response( JSON.stringify({ alsoKnownAs: [`at://${did.slice(-6)}.example`], service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.com', }, ], }), ) } return undefined}
interface NetworkOptions { /** Constellation's answer per target at-uri. */ byTarget: Record<string, string[]> /** DIDs the viewer follows; omit to fail the follows read. */ follows?: string[]}
function stubNetwork(opts: NetworkOptions) { const calls: string[] = [] const fetchMock = vi.fn(async (input: string) => { const url = String(input) calls.push(url) const identity = identityRoutes(url) if (identity) return identity if (url.startsWith(`${CONSTELLATION}/links/distinct-dids`)) { const target = new URL(url).searchParams.get('target') ?? '' const dids = opts.byTarget[target] ?? [] return new Response( JSON.stringify({ total: dids.length, linking_dids: dids, cursor: null }), ) } if (url.includes('com.atproto.repo.listRecords')) { if (!opts.follows) return new Response('nope', { status: 500, statusText: 'Server Error' }) return new Response( JSON.stringify({ records: opts.follows.map((subject, i) => ({ uri: `at://${ME}/app.bsky.graph.follow/${i}`, cid: 'bafcid', value: { subject }, })), }), ) } // No profile record: faces fall back to a monogram, which is not this // module's problem. return new Response('not found', { status: 404, statusText: 'Not Found' }) }) vi.stubGlobal('fetch', fetchMock) return calls}
/** The viewer the popup assembles before asking (see loadSubscribers). */function viewer(following: string[]) { return { did: ME, following: new Set(following) }}
afterEach(() => { vi.unstubAllGlobals()})
describe('subscriptionTargets', () => { it('asks under the handle form too, since clients store what they were handed', () => { expect(subscriptionTargets(PUB, 'pub.example')).toEqual([ PUB, 'at://pub.example/site.standard.publication/self', ]) })
it('asks only under the did form when the publication has no known handle', () => { expect(subscriptionTargets(PUB)).toEqual([PUB]) })})
describe('distinctDidsUrl', () => { it('pins the collection and the field the target has to appear in', () => { const u = new URL(distinctDidsUrl(PUB)) expect(u.searchParams.get('collection')).toBe('site.standard.graph.subscription') expect(u.searchParams.get('path')).toBe('.publication') expect(u.searchParams.get('target')).toBe(PUB) expect(u.searchParams.has('cursor')).toBe(false) })})
describe('followingDids', () => { it('reads the viewer’s own follow records, not an appview', async () => { const calls = stubNetwork({ byTarget: {}, follows: [FRIEND] }) expect(await followingDids(ME)).toEqual({ dids: [FRIEND], truncated: false }) expect(calls.some((u) => u.includes('bsky.app'))).toBe(false) })
it('says so when the walk stopped at the cap, so absence is not read as a no', async () => { // A repo that always has another page: the walk stops on the cap, not on // running out, which is exactly the case that used to be invisible. vi.stubGlobal( 'fetch', vi.fn(async (input: string) => { const url = String(input) const identity = identityRoutes(url) if (identity) return identity return new Response( JSON.stringify({ records: Array.from({ length: 100 }, (_, i) => ({ uri: `at://${ME}/app.bsky.graph.follow/${i}`, cid: 'bafcid', value: { subject: `did:plc:followed${i}` }, })), cursor: 'more', }), ) }), ) const list = await followingDids(ME) expect(list.truncated).toBe(true) expect(list.dids.length).toBeLessThanOrEqual(FOLLOWS_CAP) })})
describe('scanSubscribers', () => { it('takes the count from the first response and pages no further without faces', async () => { // Constellation with more to give: every page carries a cursor. const calls: string[] = [] vi.stubGlobal( 'fetch', vi.fn(async (input: string) => { calls.push(String(input)) return new Response( JSON.stringify({ total: 700, // A full page, so a caller that wanted DIDs would follow the cursor. linking_dids: Array.from({ length: 100 }, (_, i) => `did:plc:sub${i}`), cursor: 'more', }), ) }), )
const scan = await scanSubscribers(PUB) expect(scan?.total).toBe(700) // One request, not the five a walk to SCAN_CAP would have cost. expect(calls).toHaveLength(1) expect(scan?.dids).toEqual([]) // Truncation is a claim about faces, and this scan drew none. expect(scan?.truncated).toBe(false) })
it('pages for DIDs when faces were asked for', async () => { stubNetwork({ byTarget: { [PUB]: [STRANGER, FRIEND] } }) const scan = await scanSubscribers(PUB, undefined, { faces: true }) expect(scan?.dids).toEqual([STRANGER, FRIEND]) })
it('answers null when the index itself is unreachable', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response('down', { status: 503, statusText: 'Unavailable' })), ) expect(await scanSubscribers(PUB)).toBeNull() })})
describe('followedSubscribers', () => { it('picks the viewer’s follows out of a scan, and never the viewer', async () => { stubNetwork({ byTarget: {} }) const scan = { total: 3, dids: [ME, FRIEND, STRANGER], truncated: false } const { followed, followedTotal } = await followedSubscribers(scan, viewer([ME, FRIEND])) expect(followed.map((f) => f.did)).toEqual([FRIEND]) expect(followedTotal).toBe(1) })})
describe('summarizeSubscribers', () => { it('counts both target forms and picks the follows out of them', async () => { stubNetwork({ byTarget: { [PUB]: [STRANGER, FRIEND], 'at://pub.example/site.standard.publication/self': [STRANGER], }, follows: [FRIEND], }) const summary = await summarizeSubscribers(PUB, 'pub.example', viewer([FRIEND])) expect(summary?.total).toBe(3) expect(summary?.followedTotal).toBe(1) expect(summary?.followed.map((f) => f.did)).toEqual([FRIEND]) expect(summary?.truncated).toBe(false) })
it('never counts the viewer as somebody they follow', async () => { stubNetwork({ byTarget: { [PUB]: [ME, FRIEND] } }) const summary = await summarizeSubscribers(PUB, undefined, viewer([ME, FRIEND])) expect(summary?.total).toBe(2) expect(summary?.followed.map((f) => f.did)).toEqual([FRIEND]) })
it('keeps the count when the viewer’s follows could not be read', async () => { // The popup passes no viewer when the follow walk fails; the count stands. stubNetwork({ byTarget: { [PUB]: [FRIEND] } }) const summary = await summarizeSubscribers(PUB, undefined, undefined) expect(summary?.total).toBe(1) expect(summary?.followed).toEqual([]) })
it('answers null when the index itself is unreachable', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response('down', { status: 503, statusText: 'Unavailable' })), ) expect(await summarizeSubscribers(PUB, undefined, viewer([]))).toBeNull() })
it('still reports the count with nobody signed in', async () => { stubNetwork({ byTarget: { [PUB]: [STRANGER, FRIEND] }, follows: [FRIEND] }) const summary = await summarizeSubscribers(PUB) expect(summary?.total).toBe(2) expect(summary?.followed).toEqual([]) })})