import { afterEach, describe, expect, it, vi } from 'vitest'; import { didDocumentUrl, documentHandle, identityOf, pdsEndpoint, resolveIdentity, } from '#/lib/identity.js'; const DID = 'did:plc:c4igoshkxhzw62eme3iltpdi'; const document = { id: DID, alsoKnownAs: ['at://alice.example.com'], service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.com/', }, ], }; /** * Answer the two lookups a browser client makes, and nothing else. * @param {{txt?: string|null, doc?: object}} answers */ function stubNetwork({ txt = `"did=${DID}"`, doc = document } = {}) { vi.stubGlobal('fetch', async (input) => { const url = String(input?.url ?? input); if (url.startsWith('https://cloudflare-dns.com')) { return Response.json(txt === null ? {} : { Answer: [{ data: txt }] }); } if (url.startsWith('https://plc.directory/')) { return doc ? Response.json(doc) : new Response('gone', { status: 404 }); } return new Response('not found', { status: 404 }); }); } afterEach(() => vi.unstubAllGlobals()); describe('didDocumentUrl', () => { it('sends a did:plc to the directory', () => { expect(didDocumentUrl(DID)).toBe(`https://plc.directory/${DID}`); }); it('reads a did:web as the host serving its own document', () => { expect(didDocumentUrl('did:web:example.com')).toBe( 'https://example.com/.well-known/did.json', ); }); // A did:web carrying a port encodes the colon, and the document is served // from the host that colon belongs to. it('decodes a port in a did:web', () => { expect(didDocumentUrl('did:web:localhost%3A2583')).toBe( 'https://localhost:2583/.well-known/did.json', ); }); it('refuses a method it cannot fetch', () => { expect(() => didDocumentUrl('did:key:zQ3s')).toThrow(/Unsupported/); }); }); describe('pdsEndpoint', () => { it('drops the trailing slash, so a path joins onto it once', () => { expect(pdsEndpoint(document)).toBe('https://pds.example.com'); }); it('refuses a document naming no PDS', () => { expect(() => pdsEndpoint({ id: DID, service: [] })).toThrow(/no PDS/); }); }); describe('documentHandle', () => { it('reads the at:// alias', () => { expect(documentHandle(document)).toBe('alice.example.com'); }); // An account mid-handle-change claims none, and reads as itself. it('falls back to the DID', () => { expect(documentHandle({ id: DID, alsoKnownAs: [] })).toBe(DID); }); }); describe('resolveIdentity', () => { it('resolves a handle through DNS and confirms the document claims it', async () => { stubNetwork(); await expect(resolveIdentity('alice.example.com')).resolves.toMatchObject({ did: DID, handle: 'alice.example.com', pds: 'https://pds.example.com', }); }); it('accepts a handle typed as an at:// URI or with an @', async () => { stubNetwork(); await expect( resolveIdentity(' at://@Alice.Example.com/ '), ).resolves.toMatchObject({ did: DID }); }); // A TXT record is written by whoever holds the domain, and it can name any // DID at all. Only the document decides which handle is really its own. it('refuses a handle the document does not claim', async () => { stubNetwork({ doc: { ...document, alsoKnownAs: ['at://somebody.else.example'] }, }); await expect(resolveIdentity('alice.example.com')).rejects.toThrow( /does not claim/, ); }); it('reports a handle that resolves nowhere', async () => { stubNetwork({ txt: null }); await expect(resolveIdentity('alice.example.com')).rejects.toThrow( /Could not resolve/, ); }); it('takes a DID without asking DNS anything', async () => { stubNetwork({ txt: null }); await expect(resolveIdentity(DID)).resolves.toMatchObject({ did: DID, handle: 'alice.example.com', }); }); it('refuses an empty field', async () => { await expect(resolveIdentity(' ')).rejects.toThrow(/Enter a handle/); }); }); describe('identityOf', () => { it('confirms a handle that resolves back to the DID', async () => { stubNetwork(); await expect(identityOf(DID)).resolves.toEqual({ did: DID, handle: 'alice.example.com', verified: true, }); }); // Anybody may write any handle into their own DID document. Shown plainly it // would be somebody else's name on the page, so the round trip decides. it('reports a claimed handle that resolves to somebody else as unconfirmed', async () => { stubNetwork({ txt: '"did=did:plc:somebodyelse"' }); await expect(identityOf(DID)).resolves.toEqual({ did: DID, handle: 'alice.example.com', verified: false, }); }); it('reports a claimed handle that resolves nowhere as unconfirmed', async () => { stubNetwork({ txt: null }); await expect(identityOf(DID)).resolves.toMatchObject({ verified: false }); }); // An account mid-handle-change claims none, and there is nothing to show but // the DID. it('answers no handle for a document claiming none', async () => { stubNetwork({ doc: { id: DID, alsoKnownAs: [] } }); await expect(identityOf(DID)).resolves.toEqual({ did: DID, handle: null, verified: false, }); }); it('propagates a DID that does not resolve at all', async () => { stubNetwork({ doc: null }); await expect(identityOf('did:plc:missing')).rejects.toThrow(); }); });