Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
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});