Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
7.6 kB · 211 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212// The owner card against a stubbed network. The case that matters is the// handle: a DID document can claim any handle, and only a handle that// resolves back to the same DID may be shown as the account's.
import { afterEach, describe, expect, it, vi } from 'vitest'import { BLOCKED_NAME, type Owner, blockedIdentity, fetchOwner, handleMatches, ownerName, ownerSubtitle, showsFollowing,} from './profile'
const OTHER = 'did:plc:impostor00000000000000000'
// atproto.ts caches DID documents and handle resolutions for an hour, and the// cache is module state shared by every test in this file. Each test gets its// own identity rather than a way to clear it.let n = 0function fixture() { n += 1 return { did: `did:plc:ownercard${String(n).padStart(11, '0')}`, handle: `owner${n}.example` }}
interface NetworkOptions { /** What resolveHandle answers for the claimed handle; 'self' means the account itself. */ resolvesTo?: string | 'self' /** The account's app.bsky.actor.profile record, or none at all. */ profile?: Record<string, unknown> /** Omit the handle from the DID document. */ noHandle?: boolean}
function stubNetwork(opts: NetworkOptions = {}) { const { did, handle } = fixture() vi.stubGlobal( 'fetch', vi.fn(async (input: string) => { const url = String(input) if (url === `https://plc.directory/${did}`) { return new Response( JSON.stringify({ alsoKnownAs: opts.noHandle ? [] : [`at://${handle}`], service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.com', }, ], }), ) } if (url.includes('com.atproto.identity.resolveHandle')) { if (!opts.resolvesTo) return new Response('no', { status: 400, statusText: 'Bad Request' }) return new Response( JSON.stringify({ did: opts.resolvesTo === 'self' ? did : opts.resolvesTo }), ) } if (url.includes('com.atproto.repo.getRecord')) { if (!opts.profile) return new Response('no', { status: 404, statusText: 'Not Found' }) return new Response( JSON.stringify({ uri: `at://${did}/app.bsky.actor.profile/self`, cid: 'bafcid', value: opts.profile, }), ) } return new Response('not found', { status: 404, statusText: 'Not Found' }) }), ) return { did, handle }}
afterEach(() => { vi.unstubAllGlobals()})
describe('handleMatches', () => { it('is false when the handle resolves to somebody else', async () => { const { did, handle } = stubNetwork({ resolvesTo: OTHER }) expect(await handleMatches(handle, did)).toBe(false) })
it('is false when the handle does not resolve at all', async () => { const { did, handle } = stubNetwork() expect(await handleMatches(handle, did)).toBe(false) })})
describe('fetchOwner', () => { it('keeps a handle that resolves back to the same DID', async () => { const { did, handle } = stubNetwork({ resolvesTo: 'self', profile: { displayName: 'Wobbegong', description: 'Writes things.' }, }) const owner = await fetchOwner(did) expect(owner.handle).toBe(handle) expect(owner.displayName).toBe('Wobbegong') expect(owner.description).toBe('Writes things.') })
it('refuses a claimed handle that points somewhere else', async () => { const { did, handle } = stubNetwork({ resolvesTo: OTHER, profile: { displayName: 'Wobbegong' } }) const owner = await fetchOwner(did) expect(owner.claimedHandle).toBe(handle) expect(owner.handle).toBeUndefined() // Named, but named after the DID that really holds the records. expect(ownerSubtitle(owner)).toBe(did) })
it('builds the avatar url from a real blob ref only', async () => { const blob = stubNetwork({ resolvesTo: 'self', profile: { avatar: { $type: 'blob', ref: { $link: 'bafkreiavatar' }, mimeType: 'image/png', size: 9 }, }, }) expect((await fetchOwner(blob.did)).avatarUrl).toContain('bafkreiavatar')
vi.unstubAllGlobals() const path = stubNetwork({ resolvesTo: 'self', profile: { avatar: '/img/me.png' } }) expect((await fetchOwner(path.did)).avatarUrl).toBeUndefined() })
it('drops profile fields that are not strings, or are only whitespace', async () => { const { did } = stubNetwork({ resolvesTo: 'self', profile: { displayName: ' ', description: 42 } }) const owner = await fetchOwner(did) expect(owner.displayName).toBeUndefined() expect(owner.description).toBeUndefined() })
it('still produces a card for an account with no profile record', async () => { const { did, handle } = stubNetwork({ resolvesTo: 'self' }) const owner = await fetchOwner(did) expect(owner.handle).toBe(handle) expect(ownerName(owner)).toBe(`@${handle}`) expect(owner.displayName).toBeUndefined() })
it('falls back to the DID when the account has no handle at all', async () => { const { did } = stubNetwork({ noHandle: true }) const owner = await fetchOwner(did) expect(ownerName(owner)).toBe(did) expect(ownerSubtitle(owner)).toBeUndefined() })})
describe('blockedIdentity', () => { const full = { did: 'did:plc:blockedaccount0000000000', handle: 'blocked.example.com', claimedHandle: 'blocked.example.com', displayName: 'The Name They Chose', description: 'A bio the reader asked not to be shown.', avatarUrl: 'https://pds.example.com/xrpc/com.atproto.sync.getBlob?cid=bafkreiface', }
it('drops the avatar, the display name and the bio', () => { const shown = blockedIdentity(full) expect(shown.avatarUrl).toBeUndefined() expect(shown.description).toBeUndefined() expect(shown.displayName).toBe(BLOCKED_NAME) expect(ownerName(shown)).toBe(BLOCKED_NAME) })
it('keeps the handle, which is which account rather than how it presents', () => { expect(ownerSubtitle(blockedIdentity(full))).toBe(`@${full.handle}`) })
it('names the DID when the handle never proved itself', () => { const unproven = { ...full, handle: undefined } const shown = blockedIdentity(unproven) expect(shown.claimedHandle).toBe(full.claimedHandle) // Same rule as any other card: an unproven handle is not shown as a name. expect(ownerSubtitle(shown)).toBe(full.did) })})
describe('showsFollowing', () => { const ME = 'did:plc:thereader000000000000000' const THEM: Owner = { did: 'did:plc:thepublisher00000000000', handle: 'owner.example' } const viewer = (...dids: string[]) => ({ did: ME, following: new Set(dids) })
it('says so when the reader follows the account', () => { expect(showsFollowing(THEM, false, viewer(THEM.did))).toBe(true) })
it('stays quiet when they do not', () => { expect(showsFollowing(THEM, false, viewer(OTHER))).toBe(false) })
it('stays quiet with no viewer: signed out, or the follows would not read', () => { expect(showsFollowing(THEM, false, undefined)).toBe(false) })
it('never draws on a blocked account, follow record or not', () => { // A block does not delete the follow record it supersedes, so the repo can // still hold one; the card that is refusing to show them must not. expect(showsFollowing(THEM, true, viewer(THEM.did))).toBe(false) })
it('says nothing about the reader’s own publication', () => { const mine = { ...THEM, did: ME } expect(showsFollowing(mine, false, viewer(ME))).toBe(false) })})