Sifa professional network API (Fastify, AT Protocol, Jetstream) sifa.id/
at main 120 lines 4.2 kB view raw
1import { describe, it, expect, vi } from 'vitest'; 2import { buildFeaturedPost, postToBluesky } from '../../src/services/featured-poster.js'; 3import type { PostRecord } from '../../src/services/featured-poster.js'; 4 5describe('buildFeaturedPost', () => { 6 const input = { 7 displayName: 'Alice Example', 8 handle: 'alice.bsky.social', 9 did: 'did:plc:abc123', 10 dateStr: '2026-03-19', 11 profileUrl: 'https://sifa.id/profile/alice.bsky.social', 12 }; 13 14 it('creates post text containing displayName and @handle', () => { 15 const post = buildFeaturedPost(input); 16 expect(post.text).toContain('Alice Example'); 17 expect(post.text).toContain('@alice.bsky.social'); 18 }); 19 20 it('has exactly one mention facet', () => { 21 const post = buildFeaturedPost(input); 22 expect(post.facets).toHaveLength(1); 23 expect(post.facets[0]?.features).toHaveLength(1); 24 expect(post.facets[0]?.features[0]?.$type).toBe('app.bsky.richtext.facet#mention'); 25 expect(post.facets[0]?.features[0]?.did).toBe('did:plc:abc123'); 26 }); 27 28 it('has correct facet byte offsets', () => { 29 const post = buildFeaturedPost(input); 30 const encoder = new TextEncoder(); 31 const encoded = encoder.encode(post.text); 32 const facet = post.facets[0]; 33 expect(facet).toBeDefined(); 34 if (!facet) return; 35 const sliced = new TextDecoder().decode( 36 encoded.slice(facet.index.byteStart, facet.index.byteEnd), 37 ); 38 expect(sliced).toBe('@alice.bsky.social'); 39 }); 40 41 it('has correct facet byte offsets for multi-byte characters', () => { 42 const multiByteInput = { 43 ...input, 44 displayName: 'Ren\u00e9 M\u00fcller', 45 }; 46 const post = buildFeaturedPost(multiByteInput); 47 const encoder = new TextEncoder(); 48 const encoded = encoder.encode(post.text); 49 const facet = post.facets[0]; 50 expect(facet).toBeDefined(); 51 if (!facet) return; 52 const sliced = new TextDecoder().decode( 53 encoded.slice(facet.index.byteStart, facet.index.byteEnd), 54 ); 55 expect(sliced).toBe('@alice.bsky.social'); 56 }); 57 58 it('has external embed with correct URI', () => { 59 const post = buildFeaturedPost(input); 60 expect(post.embed.$type).toBe('app.bsky.embed.external'); 61 expect(post.embed.external.uri).toBe('https://sifa.id/profile/alice.bsky.social'); 62 expect(post.embed.external.title).toBe('Alice Example on Sifa'); 63 expect(post.embed.external.description).toBe( 64 "Check out Alice Example's professional profile on Sifa.", 65 ); 66 }); 67 68 it('has a valid ISO timestamp for createdAt', () => { 69 const post = buildFeaturedPost(input); 70 const parsed = new Date(post.createdAt); 71 expect(parsed.toISOString()).toBe(post.createdAt); 72 expect(Number.isNaN(parsed.getTime())).toBe(false); 73 }); 74}); 75 76describe('postToBluesky', () => { 77 const mockLog = { 78 info: vi.fn(), 79 warn: vi.fn(), 80 error: vi.fn(), 81 } as unknown as import('fastify').FastifyBaseLogger; 82 83 const samplePost: PostRecord = { 84 text: 'Test post\n\n@test.bsky.social', 85 facets: [ 86 { 87 index: { byteStart: 11, byteEnd: 29 }, 88 features: [{ $type: 'app.bsky.richtext.facet#mention', did: 'did:plc:test' }], 89 }, 90 ], 91 embed: { 92 $type: 'app.bsky.embed.external', 93 external: { 94 uri: 'https://sifa.id/profile/test.bsky.social', 95 title: 'Test on Sifa', 96 description: "Check out Test's professional profile on Sifa.", 97 }, 98 }, 99 createdAt: new Date().toISOString(), 100 }; 101 102 it('returns true on successful post', async () => { 103 const mockAgent = { 104 post: vi.fn().mockResolvedValue({}), 105 } as unknown as import('@atproto/api').AtpAgent; 106 const result = await postToBluesky(mockAgent, samplePost, mockLog); 107 expect(result).toBe(true); 108 expect(mockAgent.post).toHaveBeenCalledWith(samplePost); 109 expect(mockLog.info).toHaveBeenCalled(); 110 }); 111 112 it('returns false and logs error on failure', async () => { 113 const mockAgent = { 114 post: vi.fn().mockRejectedValue(new Error('network error')), 115 } as unknown as import('@atproto/api').AtpAgent; 116 const result = await postToBluesky(mockAgent, samplePost, mockLog); 117 expect(result).toBe(false); 118 expect(mockLog.error).toHaveBeenCalled(); 119 }); 120});