Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 120 lines 4.0 kB view raw
1import { describe, it, expect, vi, beforeEach } from 'vitest'; 2import { NextRequest } from 'next/server'; 3 4const mockFetchProfile = vi.fn(); 5const mockSanitize = vi.fn((input: string) => input); 6 7vi.mock('@/lib/api', () => ({ 8 fetchProfile: (id: string) => mockFetchProfile(id), 9})); 10 11vi.mock('@/lib/sanitize', () => ({ 12 sanitize: (input: string) => mockSanitize(input), 13})); 14 15import { GET } from '@/app/api/embed/[handleOrDid]/data/route'; 16 17function buildRequest( 18 handleOrDid: string, 19): [NextRequest, { params: Promise<{ handleOrDid: string }> }] { 20 const request = new NextRequest(`https://sifa.id/api/embed/${handleOrDid}/data`); 21 const params = Promise.resolve({ handleOrDid }); 22 return [request, { params }]; 23} 24 25const fullProfile = { 26 did: 'did:plc:abc123', 27 handle: 'alice.bsky.social', 28 displayName: 'Alice Smith', 29 avatar: 'https://cdn.example.com/alice.jpg', 30 headline: 'Senior Engineer at Acme', 31 locationCity: 'Amsterdam', 32 locationRegion: 'North Holland', 33 locationCountry: 'Netherlands', 34 website: 'https://alice.dev', 35 openTo: ['opportunities', 'mentoring'], 36 trustStats: [{ label: 'endorsements', value: 5 }], 37 verifiedAccounts: [{ platform: 'github', identifier: 'alice', url: 'https://github.com/alice' }], 38 claimed: true, 39}; 40 41describe('GET /api/embed/[handleOrDid]/data', () => { 42 beforeEach(() => { 43 vi.clearAllMocks(); 44 mockSanitize.mockImplementation((input: string) => input); 45 }); 46 47 it('returns card data for a valid profile', async () => { 48 mockFetchProfile.mockResolvedValue(fullProfile); 49 50 const [request, context] = buildRequest('alice.bsky.social'); 51 const response = await GET(request, context); 52 const body = await response.json(); 53 54 expect(response.status).toBe(200); 55 expect(body.did).toBe('did:plc:abc123'); 56 expect(body.handle).toBe('alice.bsky.social'); 57 expect(body.displayName).toBe('Alice Smith'); 58 expect(body.headline).toBe('Senior Engineer at Acme'); 59 expect(body.location).toEqual({ 60 country: 'Netherlands', 61 countryCode: undefined, 62 region: 'North Holland', 63 city: 'Amsterdam', 64 }); 65 expect(body.profileUrl).toBe('https://sifa.id/p/alice.bsky.social'); 66 67 const cacheControl = response.headers.get('Cache-Control'); 68 expect(cacheControl).toContain('max-age=3600'); 69 70 const cors = response.headers.get('Access-Control-Allow-Origin'); 71 expect(cors).toBe('*'); 72 }); 73 74 it('returns 404 for unknown profile', async () => { 75 mockFetchProfile.mockResolvedValue(null); 76 77 const [request, context] = buildRequest('nobody.bsky.social'); 78 const response = await GET(request, context); 79 const body = await response.json(); 80 81 expect(response.status).toBe(404); 82 expect(body.error).toBe('Profile not found'); 83 }); 84 85 it('sanitizes display name and headline', async () => { 86 mockFetchProfile.mockResolvedValue({ 87 ...fullProfile, 88 displayName: '<script>alert("xss")</script>', 89 headline: '<img onerror="hack">', 90 }); 91 92 const [request, context] = buildRequest('alice.bsky.social'); 93 await GET(request, context); 94 95 expect(mockSanitize).toHaveBeenCalledWith('<script>alert("xss")</script>'); 96 expect(mockSanitize).toHaveBeenCalledWith('<img onerror="hack">'); 97 }); 98 99 it('handles missing optional fields gracefully', async () => { 100 mockFetchProfile.mockResolvedValue({ 101 did: 'did:plc:minimal', 102 handle: 'minimal.bsky.social', 103 claimed: false, 104 }); 105 106 const [request, context] = buildRequest('minimal.bsky.social'); 107 const response = await GET(request, context); 108 const body = await response.json(); 109 110 expect(response.status).toBe(200); 111 expect(body.avatar).toBeNull(); 112 expect(body.displayName).toBeNull(); 113 expect(body.headline).toBeNull(); 114 expect(body.location).toBeNull(); 115 expect(body.website).toBeNull(); 116 expect(body.openTo).toEqual([]); 117 expect(body.trustStats).toEqual([]); 118 expect(body.verifiedAccounts).toEqual([]); 119 }); 120});