Sifa professional network API (Fastify, AT Protocol, Jetstream)
sifa.id/
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});