import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { fetchProfile, fetchProfiles, getProfileOrFallback, clearProfileCache } from './profiles'; import type { AuthorProfile } from '$lib/types'; // Mock fetch const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); describe('fetchProfile', () => { beforeEach(() => { mockFetch.mockReset(); clearProfileCache(); }); it('should fetch and return a profile', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ did: 'did:plc:test123', handle: 'test.bsky.social', avatar: 'https://example.com/avatar.jpg' }) }); const profile = await fetchProfile('did:plc:test123'); expect(profile).toEqual({ did: 'did:plc:test123', handle: 'test.bsky.social', avatar: 'https://example.com/avatar.jpg' }); expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('did%3Aplc%3Atest123')); }); it('should return null on HTTP error', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 404 }); const profile = await fetchProfile('did:plc:notfound'); expect(profile).toBeNull(); }); it('should return null on network error', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); const profile = await fetchProfile('did:plc:test123'); expect(profile).toBeNull(); }); it('should handle profiles without avatars', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ did: 'did:plc:noavatar', handle: 'noavatar.bsky.social' // No avatar field }) }); const profile = await fetchProfile('did:plc:noavatar'); expect(profile).toEqual({ did: 'did:plc:noavatar', handle: 'noavatar.bsky.social', avatar: undefined }); }); }); describe('fetchProfiles', () => { beforeEach(() => { mockFetch.mockReset(); clearProfileCache(); }); it('should fetch multiple profiles in parallel', async () => { mockFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ did: 'did:plc:user1', handle: 'user1.bsky.social' }) }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ did: 'did:plc:user2', handle: 'user2.bsky.social' }) }); const profiles = await fetchProfiles(['did:plc:user1', 'did:plc:user2']); expect(profiles.size).toBe(2); expect(profiles.get('did:plc:user1')?.handle).toBe('user1.bsky.social'); expect(profiles.get('did:plc:user2')?.handle).toBe('user2.bsky.social'); }); it('should deduplicate DIDs', async () => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({ did: 'did:plc:duplicate', handle: 'duplicate.bsky.social' }) }); const profiles = await fetchProfiles([ 'did:plc:duplicate', 'did:plc:duplicate', 'did:plc:duplicate' ]); // Should only make one fetch call expect(mockFetch).toHaveBeenCalledTimes(1); expect(profiles.size).toBe(1); }); it('should handle partial failures', async () => { mockFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ did: 'did:plc:success', handle: 'success.bsky.social' }) }) .mockResolvedValueOnce({ ok: false, status: 404 }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ did: 'did:plc:success2', handle: 'success2.bsky.social' }) }); const profiles = await fetchProfiles([ 'did:plc:success', 'did:plc:notfound', 'did:plc:success2' ]); // Should have 2 profiles, failed one excluded expect(profiles.size).toBe(2); expect(profiles.has('did:plc:success')).toBe(true); expect(profiles.has('did:plc:notfound')).toBe(false); expect(profiles.has('did:plc:success2')).toBe(true); }); it('should return empty map for empty input', async () => { const profiles = await fetchProfiles([]); expect(profiles.size).toBe(0); expect(mockFetch).not.toHaveBeenCalled(); }); it('should handle all failures gracefully', async () => { mockFetch.mockRejectedValue(new Error('Network error')); const profiles = await fetchProfiles(['did:plc:fail1', 'did:plc:fail2']); // Should return empty map, not throw expect(profiles.size).toBe(0); }); }); describe('getProfileOrFallback', () => { it('should return profile from map when found', () => { const profiles = new Map(); profiles.set('did:plc:found', { did: 'did:plc:found' as `did:${string}:${string}`, handle: 'found.bsky.social' as `${string}.${string}`, avatar: 'https://example.com/avatar.jpg' }); const result = getProfileOrFallback(profiles, 'did:plc:found'); expect(result.handle).toBe('found.bsky.social'); expect(result.avatar).toBe('https://example.com/avatar.jpg'); }); it('should return fallback when profile not found', () => { const profiles = new Map(); const result = getProfileOrFallback(profiles, 'did:plc:notfound123'); expect(result.did).toBe('did:plc:notfound123'); expect(result.handle).toBe('did:plc:notfound123...'); }); it('should truncate long DIDs in fallback', () => { const profiles = new Map(); const longDid = 'did:plc:verylongidentifierthatexceedstwentycharacters'; const result = getProfileOrFallback(profiles, longDid); // slice(0, 20) gives first 20 chars, then '...' expect(result.handle).toBe('did:plc:verylongiden...'); expect(result.handle.length).toBe(23); // 20 chars + '...' }); });