import { describe, expect, test, vi } from 'vitest'; import { createCachedDidResolver, extractPdsEndpoint, resolveDid, } from '../src/did.js'; describe('did', () => { describe('extractPdsEndpoint', () => { test('extracts PDS endpoint from DID document', () => { const didDoc = { service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.com', }, ], }; expect(extractPdsEndpoint(didDoc)).toBe('https://pds.example.com'); }); test('finds PDS by type if id differs', () => { const didDoc = { service: [ { id: '#pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.com', }, ], }; expect(extractPdsEndpoint(didDoc)).toBe('https://pds.example.com'); }); test('returns null if no PDS service', () => { const didDoc = { service: [] }; expect(extractPdsEndpoint(didDoc)).toBeNull(); }); test('returns null if no service array', () => { const didDoc = {}; expect(extractPdsEndpoint(didDoc)).toBeNull(); }); }); describe('resolveDid', () => { test('resolves did:plc via PLC directory', async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://bsky.social', }, ], }), }); const endpoint = await resolveDid('did:plc:xyz123', { fetch: mockFetch }); expect(mockFetch).toHaveBeenCalledWith( 'https://plc.directory/did:plc:xyz123', ); expect(endpoint).toBe('https://bsky.social'); }); test('resolves did:web via .well-known', async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ service: [ { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://example.com', }, ], }), }); const endpoint = await resolveDid('did:web:example.com', { fetch: mockFetch, }); expect(mockFetch).toHaveBeenCalledWith( 'https://example.com/.well-known/did.json', ); expect(endpoint).toBe('https://example.com'); }); test('returns null for unknown DID method', async () => { const endpoint = await resolveDid('did:unknown:xyz', { fetch: vi.fn() }); expect(endpoint).toBeNull(); }); test('returns null on fetch error', async () => { const mockFetch = vi.fn().mockRejectedValue(new Error('Network error')); const endpoint = await resolveDid('did:plc:xyz', { fetch: mockFetch }); expect(endpoint).toBeNull(); }); }); describe('createCachedDidResolver', () => { const DID = 'did:plc:cachedaccount0000000000000'; /** * A fetch answering with `doc`, or 404 once `doc` is null. * @param {Object|null} doc */ function fetchServing(doc) { return vi.fn(async () => ({ ok: doc !== null, json: async () => doc, })); } /** * The mocks here answer with the two fields resolveDidDocument reads, not * a whole Response. * @param {Object} opts */ const cachedResolver = (opts) => createCachedDidResolver(/** @type {any} */ (opts)); test('reads the directory once for repeated resolutions', async () => { const fetch = fetchServing({ id: DID }); const resolve = cachedResolver({ fetch }); expect(await resolve(DID)).toEqual({ id: DID }); expect(await resolve(DID)).toEqual({ id: DID }); expect(fetch).toHaveBeenCalledTimes(1); }); test('reads once for resolutions that arrive together', async () => { const fetch = fetchServing({ id: DID }); const resolve = cachedResolver({ fetch }); const answers = await Promise.all([ resolve(DID), resolve(DID), resolve(DID), ]); expect(answers).toEqual([{ id: DID }, { id: DID }, { id: DID }]); expect(fetch).toHaveBeenCalledTimes(1); }); test('reads again once the answer has expired', async () => { vi.useFakeTimers(); try { const fetch = fetchServing({ id: DID }); const resolve = cachedResolver({ fetch, ttl: 1000 }); await resolve(DID); vi.advanceTimersByTime(1001); await resolve(DID); expect(fetch).toHaveBeenCalledTimes(2); } finally { vi.useRealTimers(); } }); test('forceRefresh reads past the held answer', async () => { const fetch = fetchServing({ id: DID }); const resolve = cachedResolver({ fetch }); await resolve(DID); await resolve(DID, { forceRefresh: true }); expect(fetch).toHaveBeenCalledTimes(2); }); test('serves a rotated document after forceRefresh', async () => { const fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ key: 'old' }) }) .mockResolvedValue({ ok: true, json: async () => ({ key: 'new' }) }); const resolve = cachedResolver({ fetch }); expect(await resolve(DID)).toEqual({ key: 'old' }); expect(await resolve(DID, { forceRefresh: true })).toEqual({ key: 'new', }); // The refreshed answer replaces the held one. expect(await resolve(DID)).toEqual({ key: 'new' }); }); test('holds an unresolvable DID for the shorter negative interval', async () => { vi.useFakeTimers(); try { const fetch = fetchServing(null); const resolve = cachedResolver({ fetch, ttl: 100000, negativeTtl: 1000, }); expect(await resolve(DID)).toBeNull(); expect(await resolve(DID)).toBeNull(); expect(fetch).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(1001); expect(await resolve(DID)).toBeNull(); expect(fetch).toHaveBeenCalledTimes(2); } finally { vi.useRealTimers(); } }); test('serves the held document when the directory stops answering', async () => { vi.useFakeTimers(); try { const fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ id: DID }) }) .mockRejectedValue(new Error('directory unreachable')); const resolve = cachedResolver({ fetch, ttl: 1000 }); await resolve(DID); vi.advanceTimersByTime(1001); expect(await resolve(DID)).toEqual({ id: DID }); } finally { vi.useRealTimers(); } }); test('stops serving a held document past maxTtl', async () => { vi.useFakeTimers(); try { const fetch = vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ id: DID }) }) .mockRejectedValue(new Error('directory unreachable')); const resolve = cachedResolver({ fetch, ttl: 1000, maxTtl: 5000, }); await resolve(DID); vi.advanceTimersByTime(5001); expect(await resolve(DID)).toBeNull(); } finally { vi.useRealTimers(); } }); }); });