Barazo AppView backend
barazo.forum
1import { describe, it, expect, vi, beforeEach } from 'vitest'
2import { createSetupService } from '../../../src/setup/service.js'
3import type { SetupService } from '../../../src/setup/service.js'
4import type { PlcDidService, GenerateDidResult } from '../../../src/services/plc-did.js'
5import type { Logger } from '../../../src/lib/logger.js'
6import { decrypt } from '../../../src/lib/encryption.js'
7
8// ---------------------------------------------------------------------------
9// Mock helpers
10// ---------------------------------------------------------------------------
11
12function createMockDb() {
13 // Select chain: db.select().from().where() -> Promise<rows[]>
14 const whereSelectFn = vi.fn<() => Promise<unknown[]>>()
15 const fromFn = vi.fn<() => { where: typeof whereSelectFn }>().mockReturnValue({
16 where: whereSelectFn,
17 })
18 const selectFn = vi.fn<() => { from: typeof fromFn }>().mockReturnValue({
19 from: fromFn,
20 })
21
22 // Upsert chain: db.insert().values().onConflictDoUpdate().returning() -> Promise<rows[]>
23 // Also supports: db.insert().values().onConflictDoNothing() -> Promise<rows[]>
24 const returningFn = vi.fn<() => Promise<unknown[]>>()
25 const onConflictDoUpdateFn = vi.fn<() => { returning: typeof returningFn }>().mockReturnValue({
26 returning: returningFn,
27 })
28 const onConflictDoNothingFn = vi.fn<() => Promise<unknown[]>>().mockResolvedValue([])
29 const valuesFn = vi
30 .fn<
31 () => {
32 onConflictDoUpdate: typeof onConflictDoUpdateFn
33 onConflictDoNothing: typeof onConflictDoNothingFn
34 }
35 >()
36 .mockReturnValue({
37 onConflictDoUpdate: onConflictDoUpdateFn,
38 onConflictDoNothing: onConflictDoNothingFn,
39 })
40 const insertFn = vi.fn<() => { values: typeof valuesFn }>().mockReturnValue({
41 values: valuesFn,
42 })
43
44 // Update chain: db.update().set().where() -> Promise<unknown[]>
45 const whereUpdateFn = vi.fn<() => Promise<unknown[]>>().mockResolvedValue([])
46 const setFn = vi.fn<() => { where: typeof whereUpdateFn }>().mockReturnValue({
47 where: whereUpdateFn,
48 })
49 const updateFn = vi.fn<() => { set: typeof setFn }>().mockReturnValue({
50 set: setFn,
51 })
52
53 return {
54 db: { select: selectFn, insert: insertFn, update: updateFn },
55 mocks: {
56 selectFn,
57 fromFn,
58 whereSelectFn,
59 insertFn,
60 valuesFn,
61 onConflictDoUpdateFn,
62 onConflictDoNothingFn,
63 returningFn,
64 updateFn,
65 setFn,
66 whereUpdateFn,
67 },
68 }
69}
70
71function createMockLogger(): Logger {
72 return {
73 info: vi.fn(),
74 error: vi.fn(),
75 warn: vi.fn(),
76 debug: vi.fn(),
77 fatal: vi.fn(),
78 trace: vi.fn(),
79 child: vi.fn(),
80 silent: vi.fn(),
81 level: 'silent',
82 } as unknown as Logger
83}
84
85function createMockPlcDidService(): PlcDidService & {
86 generateDid: ReturnType<typeof vi.fn>
87} {
88 return {
89 generateDid: vi.fn<() => Promise<GenerateDidResult>>(),
90 }
91}
92
93// ---------------------------------------------------------------------------
94// Fixtures
95// ---------------------------------------------------------------------------
96
97const TEST_DID = 'did:plc:test123456789'
98const DEFAULT_COMMUNITY_NAME = 'Barazo Community'
99const TEST_HANDLE = 'community.barazo.forum'
100const TEST_SERVICE_ENDPOINT = 'https://community.barazo.forum'
101const TEST_COMMUNITY_DID = 'did:plc:communityabc123456'
102const TEST_SIGNING_KEY = 'a'.repeat(64)
103const TEST_ROTATION_KEY = 'b'.repeat(64)
104const TEST_ENCRYPTION_KEY = 'c'.repeat(32)
105
106// ---------------------------------------------------------------------------
107// Test suite
108// ---------------------------------------------------------------------------
109
110describe('SetupService', () => {
111 let service: SetupService
112 let mocks: ReturnType<typeof createMockDb>['mocks']
113 let mockLogger: Logger
114 let mockPlcDidService: ReturnType<typeof createMockPlcDidService>
115
116 beforeEach(() => {
117 const { db, mocks: m } = createMockDb()
118 mocks = m
119 mockLogger = createMockLogger()
120 mockPlcDidService = createMockPlcDidService()
121 service = createSetupService(db as never, mockLogger, TEST_ENCRYPTION_KEY, mockPlcDidService)
122 })
123
124 // =========================================================================
125 // getStatus
126 // =========================================================================
127
128 describe('getStatus()', () => {
129 it('returns { initialized: false } when no settings row exists', async () => {
130 mocks.whereSelectFn.mockResolvedValueOnce([])
131
132 const result = await service.getStatus()
133
134 expect(result).toStrictEqual({ initialized: false })
135 })
136
137 it('returns { initialized: false } when settings exist but not initialized', async () => {
138 mocks.whereSelectFn.mockResolvedValueOnce([
139 {
140 initialized: false,
141 communityName: 'Test Community',
142 },
143 ])
144
145 const result = await service.getStatus()
146
147 expect(result).toStrictEqual({ initialized: false })
148 })
149
150 it('returns { initialized: true, communityName } when initialized', async () => {
151 mocks.whereSelectFn.mockResolvedValueOnce([
152 {
153 initialized: true,
154 communityName: 'My Forum',
155 },
156 ])
157
158 const result = await service.getStatus()
159
160 expect(result).toStrictEqual({
161 initialized: true,
162 communityName: 'My Forum',
163 })
164 })
165
166 it('propagates database errors', async () => {
167 mocks.whereSelectFn.mockRejectedValueOnce(new Error('Connection lost'))
168
169 await expect(service.getStatus()).rejects.toThrow('Connection lost')
170 })
171 })
172
173 // =========================================================================
174 // initialize (basic, without PLC DID)
175 // =========================================================================
176
177 describe('initialize() without PLC DID', () => {
178 it('returns success for first authenticated user when no row exists', async () => {
179 mocks.returningFn.mockResolvedValueOnce([
180 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: null },
181 ])
182
183 const result = await service.initialize({ did: TEST_DID })
184
185 expect(result).toStrictEqual({
186 initialized: true,
187 adminDid: TEST_DID,
188 communityName: DEFAULT_COMMUNITY_NAME,
189 })
190 expect(mocks.insertFn).toHaveBeenCalled()
191 expect(mockPlcDidService.generateDid).not.toHaveBeenCalled()
192 })
193
194 it('returns success when row exists but not initialized', async () => {
195 mocks.returningFn.mockResolvedValueOnce([
196 { communityName: 'Existing Name', communityDid: null },
197 ])
198
199 const result = await service.initialize({ did: TEST_DID })
200
201 expect(result).toStrictEqual({
202 initialized: true,
203 adminDid: TEST_DID,
204 communityName: 'Existing Name',
205 })
206 expect(mocks.insertFn).toHaveBeenCalled()
207 })
208
209 it('returns conflict error when already initialized', async () => {
210 mocks.returningFn.mockResolvedValueOnce([])
211
212 const result = await service.initialize({ did: TEST_DID })
213
214 expect(result).toStrictEqual({ alreadyInitialized: true })
215 })
216
217 it('accepts optional communityName', async () => {
218 mocks.returningFn.mockResolvedValueOnce([
219 { communityName: 'Custom Name', communityDid: null },
220 ])
221
222 const result = await service.initialize({
223 did: TEST_DID,
224 communityName: 'Custom Name',
225 })
226
227 expect(result).toStrictEqual({
228 initialized: true,
229 adminDid: TEST_DID,
230 communityName: 'Custom Name',
231 })
232 expect(mocks.insertFn).toHaveBeenCalled()
233 })
234
235 it('preserves existing communityName when no override provided', async () => {
236 mocks.returningFn.mockResolvedValueOnce([
237 { communityName: 'Keep This Name', communityDid: null },
238 ])
239
240 const result = await service.initialize({ did: TEST_DID })
241
242 expect(result).toStrictEqual({
243 initialized: true,
244 adminDid: TEST_DID,
245 communityName: 'Keep This Name',
246 })
247 })
248
249 it('propagates database errors', async () => {
250 mocks.returningFn.mockRejectedValueOnce(new Error('Connection lost'))
251
252 await expect(service.initialize({ did: TEST_DID })).rejects.toThrow('Connection lost')
253 })
254
255 it('does not call PLC DID service when only handle is provided (no serviceEndpoint)', async () => {
256 mocks.returningFn.mockResolvedValueOnce([
257 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: null },
258 ])
259
260 await service.initialize({
261 did: TEST_DID,
262 handle: TEST_HANDLE,
263 })
264
265 expect(mockPlcDidService.generateDid).not.toHaveBeenCalled()
266 })
267
268 it('does not call PLC DID service when only serviceEndpoint is provided (no handle)', async () => {
269 mocks.returningFn.mockResolvedValueOnce([
270 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: null },
271 ])
272
273 await service.initialize({
274 did: TEST_DID,
275 serviceEndpoint: TEST_SERVICE_ENDPOINT,
276 })
277
278 expect(mockPlcDidService.generateDid).not.toHaveBeenCalled()
279 })
280
281 it('promotes initializing user to admin role in users table', async () => {
282 mocks.returningFn.mockResolvedValueOnce([{ communityName: 'Test Forum', communityDid: null }])
283
284 const result = await service.initialize({
285 did: TEST_DID,
286 communityName: 'Test Forum',
287 })
288
289 expect(result).toStrictEqual({
290 initialized: true,
291 adminDid: TEST_DID,
292 communityName: 'Test Forum',
293 })
294 expect(mocks.updateFn).toHaveBeenCalledOnce()
295 })
296
297 it('does not promote user when community is already initialized', async () => {
298 mocks.returningFn.mockResolvedValueOnce([])
299
300 const result = await service.initialize({ did: TEST_DID })
301
302 expect(result).toStrictEqual({ alreadyInitialized: true })
303 expect(mocks.updateFn).not.toHaveBeenCalled()
304 })
305
306 it('seeds platform:age_confirmation onboarding field after initialization', async () => {
307 mocks.returningFn.mockResolvedValueOnce([
308 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
309 ])
310
311 await service.initialize({ did: TEST_DID, communityDid: TEST_COMMUNITY_DID })
312
313 // insert is called 6 times: settings, onboarding, pages, categories, topics, replies
314 expect(mocks.insertFn).toHaveBeenCalledTimes(6)
315
316 // The second insert's values call should contain the platform age field
317 const secondValuesCall = mocks.valuesFn.mock.calls[1]?.[0] as Record<string, unknown>
318 expect(secondValuesCall).toBeDefined()
319 expect(secondValuesCall.id).toBe('platform:age_confirmation')
320 expect(secondValuesCall.fieldType).toBe('age_confirmation')
321 expect(secondValuesCall.source).toBe('platform')
322 expect(secondValuesCall.isMandatory).toBe(true)
323 expect(secondValuesCall.sortOrder).toBe(-1)
324 expect(mocks.onConflictDoNothingFn).toHaveBeenCalled()
325 })
326
327 it('does not seed platform fields when community is already initialized', async () => {
328 mocks.returningFn.mockResolvedValueOnce([])
329
330 await service.initialize({ did: TEST_DID })
331
332 // Only one insert call (the upsert attempt), no seeding
333 expect(mocks.insertFn).toHaveBeenCalledTimes(1)
334 })
335 })
336
337 // =========================================================================
338 // initialize (with PLC DID generation)
339 // =========================================================================
340
341 describe('initialize() with PLC DID', () => {
342 it('generates PLC DID when handle and serviceEndpoint are provided', async () => {
343 mockPlcDidService.generateDid.mockResolvedValueOnce({
344 did: TEST_COMMUNITY_DID,
345 signingKey: TEST_SIGNING_KEY,
346 rotationKey: TEST_ROTATION_KEY,
347 })
348 mocks.returningFn.mockResolvedValueOnce([
349 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
350 ])
351
352 const result = await service.initialize({
353 did: TEST_DID,
354 handle: TEST_HANDLE,
355 serviceEndpoint: TEST_SERVICE_ENDPOINT,
356 })
357
358 expect(mockPlcDidService.generateDid).toHaveBeenCalledOnce()
359 expect(mockPlcDidService.generateDid).toHaveBeenCalledWith({
360 handle: TEST_HANDLE,
361 serviceEndpoint: TEST_SERVICE_ENDPOINT,
362 })
363
364 expect(result).toStrictEqual({
365 initialized: true,
366 adminDid: TEST_DID,
367 communityName: DEFAULT_COMMUNITY_NAME,
368 communityDid: TEST_COMMUNITY_DID,
369 })
370 })
371
372 it('includes communityDid in result when DID is generated', async () => {
373 mockPlcDidService.generateDid.mockResolvedValueOnce({
374 did: TEST_COMMUNITY_DID,
375 signingKey: TEST_SIGNING_KEY,
376 rotationKey: TEST_ROTATION_KEY,
377 })
378 mocks.returningFn.mockResolvedValueOnce([
379 { communityName: 'My Forum', communityDid: TEST_COMMUNITY_DID },
380 ])
381
382 const result = await service.initialize({
383 did: TEST_DID,
384 communityName: 'My Forum',
385 handle: TEST_HANDLE,
386 serviceEndpoint: TEST_SERVICE_ENDPOINT,
387 })
388
389 expect(result).toHaveProperty('communityDid', TEST_COMMUNITY_DID)
390 })
391
392 it('does not include communityDid in result when DID is null', async () => {
393 mocks.returningFn.mockResolvedValueOnce([
394 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: null },
395 ])
396
397 const result = await service.initialize({ did: TEST_DID })
398
399 expect(result).not.toHaveProperty('communityDid')
400 })
401
402 it('encrypts signing and rotation keys before storing in DB', async () => {
403 mockPlcDidService.generateDid.mockResolvedValueOnce({
404 did: TEST_COMMUNITY_DID,
405 signingKey: TEST_SIGNING_KEY,
406 rotationKey: TEST_ROTATION_KEY,
407 })
408 mocks.returningFn.mockResolvedValueOnce([
409 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
410 ])
411
412 await service.initialize({
413 did: TEST_DID,
414 handle: TEST_HANDLE,
415 serviceEndpoint: TEST_SERVICE_ENDPOINT,
416 })
417
418 // Extract the values passed to the DB insert
419 const callArgs = mocks.valuesFn.mock.calls[0]
420 expect(callArgs).toBeDefined()
421 const insertValues = (callArgs as unknown[][])[0] as Record<string, unknown>
422
423 // Keys should NOT be plaintext
424 expect(insertValues.signingKey).not.toBe(TEST_SIGNING_KEY)
425 expect(insertValues.rotationKey).not.toBe(TEST_ROTATION_KEY)
426
427 // Keys should be decryptable back to the originals
428 expect(decrypt(insertValues.signingKey as string, TEST_ENCRYPTION_KEY)).toBe(TEST_SIGNING_KEY)
429 expect(decrypt(insertValues.rotationKey as string, TEST_ENCRYPTION_KEY)).toBe(
430 TEST_ROTATION_KEY
431 )
432 })
433
434 it('propagates PLC DID generation errors', async () => {
435 mockPlcDidService.generateDid.mockRejectedValueOnce(
436 new Error('PLC directory returned 500: Internal Server Error')
437 )
438
439 await expect(
440 service.initialize({
441 did: TEST_DID,
442 handle: TEST_HANDLE,
443 serviceEndpoint: TEST_SERVICE_ENDPOINT,
444 })
445 ).rejects.toThrow('PLC directory returned 500: Internal Server Error')
446 })
447
448 it('logs info when generating PLC DID', async () => {
449 mockPlcDidService.generateDid.mockResolvedValueOnce({
450 did: TEST_COMMUNITY_DID,
451 signingKey: TEST_SIGNING_KEY,
452 rotationKey: TEST_ROTATION_KEY,
453 })
454 mocks.returningFn.mockResolvedValueOnce([
455 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
456 ])
457
458 await service.initialize({
459 did: TEST_DID,
460 handle: TEST_HANDLE,
461 serviceEndpoint: TEST_SERVICE_ENDPOINT,
462 })
463
464 const infoFn = mockLogger.info as ReturnType<typeof vi.fn>
465 expect(infoFn).toHaveBeenCalledWith(
466 expect.objectContaining({
467 handle: TEST_HANDLE,
468 serviceEndpoint: TEST_SERVICE_ENDPOINT,
469 }) as Record<string, unknown>,
470 'Generating PLC DID during community setup'
471 )
472 })
473 })
474
475 // =========================================================================
476 // initialize (page seeding)
477 // =========================================================================
478
479 describe('initialize() page seeding', () => {
480 it('seeds default pages after admin promotion', async () => {
481 mocks.returningFn.mockResolvedValueOnce([
482 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
483 ])
484
485 await service.initialize({
486 communityDid: TEST_COMMUNITY_DID,
487 did: TEST_DID,
488 })
489
490 // insert is called 6 times: settings, onboarding, pages, categories, topics, replies
491 expect(mocks.insertFn).toHaveBeenCalledTimes(6)
492 })
493
494 it('seeds exactly 5 default pages with correct slugs', async () => {
495 mocks.returningFn.mockResolvedValueOnce([
496 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
497 ])
498
499 // Capture the values passed to the pages insert call (pages have 'status' field)
500 let capturedPageValues: Array<{ slug: string; status: string; communityDid: string }> = []
501 mocks.valuesFn.mockImplementation((vals: unknown) => {
502 if (
503 Array.isArray(vals) &&
504 vals.length > 0 &&
505 'status' in (vals[0] as Record<string, unknown>) &&
506 'slug' in (vals[0] as Record<string, unknown>)
507 ) {
508 capturedPageValues = vals as typeof capturedPageValues
509 }
510 return {
511 onConflictDoUpdate: mocks.onConflictDoUpdateFn,
512 onConflictDoNothing: mocks.onConflictDoNothingFn,
513 }
514 })
515
516 await service.initialize({
517 communityDid: TEST_COMMUNITY_DID,
518 did: TEST_DID,
519 })
520
521 expect(capturedPageValues).toHaveLength(5)
522 const slugs = capturedPageValues.map((v) => v.slug)
523 expect(slugs).toContain('terms-of-service')
524 expect(slugs).toContain('privacy-policy')
525 expect(slugs).toContain('cookie-policy')
526 expect(slugs).toContain('accessibility')
527 expect(slugs).toContain('your-data')
528
529 for (const page of capturedPageValues) {
530 expect(page.status).toBe('published')
531 expect(page.communityDid).toBe(TEST_COMMUNITY_DID)
532 }
533 })
534
535 it('does not seed pages when community is already initialized', async () => {
536 mocks.returningFn.mockResolvedValueOnce([])
537
538 const result = await service.initialize({
539 communityDid: TEST_COMMUNITY_DID,
540 did: TEST_DID,
541 })
542
543 expect(result).toStrictEqual({ alreadyInitialized: true })
544 // Only 1 insert call (the upsert), no pages insert
545 expect(mocks.insertFn).toHaveBeenCalledTimes(1)
546 })
547
548 it('logs page seeding info', async () => {
549 mocks.returningFn.mockResolvedValueOnce([
550 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
551 ])
552
553 await service.initialize({
554 communityDid: TEST_COMMUNITY_DID,
555 did: TEST_DID,
556 })
557
558 const infoFn = mockLogger.info as ReturnType<typeof vi.fn>
559 const logCalls = infoFn.mock.calls as Array<[Record<string, unknown>, string]>
560 const seedLog = logCalls.find(
561 ([_ctx, msg]) => typeof msg === 'string' && msg.includes('Default pages seeded')
562 )
563 expect(seedLog).toBeDefined()
564 if (seedLog) {
565 expect(seedLog[0]).toHaveProperty('communityDid', TEST_COMMUNITY_DID)
566 expect(seedLog[0]).toHaveProperty('pageCount', 5)
567 }
568 })
569 })
570
571 // =========================================================================
572 // initialize (category and demo content seeding)
573 // =========================================================================
574
575 describe('initialize() category and demo content seeding', () => {
576 it('seeds categories with subcategories', async () => {
577 mocks.returningFn.mockResolvedValueOnce([
578 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
579 ])
580
581 let capturedCategoryValues: Array<{
582 slug: string
583 parentId: string | null
584 communityDid: string
585 }> = []
586 mocks.valuesFn.mockImplementation((vals: unknown) => {
587 if (
588 Array.isArray(vals) &&
589 vals.length > 0 &&
590 'maturityRating' in (vals[0] as Record<string, unknown>) &&
591 'slug' in (vals[0] as Record<string, unknown>)
592 ) {
593 capturedCategoryValues = vals as typeof capturedCategoryValues
594 }
595 return {
596 onConflictDoUpdate: mocks.onConflictDoUpdateFn,
597 onConflictDoNothing: mocks.onConflictDoNothingFn,
598 }
599 })
600
601 await service.initialize({
602 communityDid: TEST_COMMUNITY_DID,
603 did: TEST_DID,
604 })
605
606 expect(capturedCategoryValues.length).toBeGreaterThan(4)
607
608 // Root categories have null parentId
609 const roots = capturedCategoryValues.filter((c) => c.parentId === null)
610 expect(roots.length).toBeGreaterThanOrEqual(4)
611
612 // Subcategories have non-null parentId
613 const subs = capturedCategoryValues.filter((c) => c.parentId !== null)
614 expect(subs.length).toBeGreaterThanOrEqual(3)
615
616 // All categories belong to the correct community
617 for (const cat of capturedCategoryValues) {
618 expect(cat.communityDid).toBe(TEST_COMMUNITY_DID)
619 }
620 })
621
622 it('seeds demo topics across categories including subcategories', async () => {
623 mocks.returningFn.mockResolvedValueOnce([
624 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
625 ])
626
627 let capturedTopicValues: Array<{
628 category: string
629 title: string
630 authorDid: string
631 }> = []
632 mocks.valuesFn.mockImplementation((vals: unknown) => {
633 if (
634 Array.isArray(vals) &&
635 vals.length > 0 &&
636 'title' in (vals[0] as Record<string, unknown>) &&
637 'category' in (vals[0] as Record<string, unknown>)
638 ) {
639 capturedTopicValues = vals as typeof capturedTopicValues
640 }
641 return {
642 onConflictDoUpdate: mocks.onConflictDoUpdateFn,
643 onConflictDoNothing: mocks.onConflictDoNothingFn,
644 }
645 })
646
647 await service.initialize({
648 communityDid: TEST_COMMUNITY_DID,
649 did: TEST_DID,
650 })
651
652 expect(capturedTopicValues.length).toBeGreaterThanOrEqual(5)
653
654 // Topics should span both root and subcategories
655 const topicCategories = new Set(capturedTopicValues.map((t) => t.category))
656 expect(topicCategories.has('general')).toBe(true)
657 expect(topicCategories.has('frontend')).toBe(true)
658 expect(topicCategories.has('backend')).toBe(true)
659
660 // All topics use the admin DID as author
661 for (const topic of capturedTopicValues) {
662 expect(topic.authorDid).toBe(TEST_DID)
663 }
664 })
665
666 it('seeds demo replies for each topic', async () => {
667 mocks.returningFn.mockResolvedValueOnce([
668 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
669 ])
670
671 let capturedReplyValues: Array<{
672 rootUri: string
673 authorDid: string
674 depth: number
675 }> = []
676 mocks.valuesFn.mockImplementation((vals: unknown) => {
677 if (
678 Array.isArray(vals) &&
679 vals.length > 0 &&
680 'rootUri' in (vals[0] as Record<string, unknown>) &&
681 'depth' in (vals[0] as Record<string, unknown>)
682 ) {
683 capturedReplyValues = vals as typeof capturedReplyValues
684 }
685 return {
686 onConflictDoUpdate: mocks.onConflictDoUpdateFn,
687 onConflictDoNothing: mocks.onConflictDoNothingFn,
688 }
689 })
690
691 await service.initialize({
692 communityDid: TEST_COMMUNITY_DID,
693 did: TEST_DID,
694 })
695
696 // One reply per topic
697 expect(capturedReplyValues.length).toBeGreaterThanOrEqual(5)
698
699 for (const reply of capturedReplyValues) {
700 expect(reply.authorDid).toBe(TEST_DID)
701 expect(reply.depth).toBe(1)
702 }
703 })
704
705 it('logs category and demo content seeding', async () => {
706 mocks.returningFn.mockResolvedValueOnce([
707 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID },
708 ])
709
710 await service.initialize({
711 communityDid: TEST_COMMUNITY_DID,
712 did: TEST_DID,
713 })
714
715 const infoFn = mockLogger.info as ReturnType<typeof vi.fn>
716 const logCalls = infoFn.mock.calls as Array<[Record<string, unknown>, string]>
717
718 const catLog = logCalls.find(
719 ([_ctx, msg]) => typeof msg === 'string' && msg.includes('Default categories seeded')
720 )
721 expect(catLog).toBeDefined()
722
723 const contentLog = logCalls.find(
724 ([_ctx, msg]) => typeof msg === 'string' && msg.includes('Demo content seeded')
725 )
726 expect(contentLog).toBeDefined()
727 })
728 })
729
730 // =========================================================================
731 // initialize (without PlcDidService injected)
732 // =========================================================================
733
734 describe('initialize() without PlcDidService', () => {
735 it('logs warning when handle/serviceEndpoint provided but no PlcDidService', async () => {
736 // Create service without PlcDidService
737 const { db, mocks: m } = createMockDb()
738 const logger = createMockLogger()
739 const serviceWithoutPlc = createSetupService(db as never, logger, TEST_ENCRYPTION_KEY)
740
741 m.returningFn.mockResolvedValueOnce([
742 { communityName: DEFAULT_COMMUNITY_NAME, communityDid: null },
743 ])
744
745 await serviceWithoutPlc.initialize({
746 did: TEST_DID,
747 handle: TEST_HANDLE,
748 serviceEndpoint: TEST_SERVICE_ENDPOINT,
749 })
750
751 const warnFn = logger.warn as ReturnType<typeof vi.fn>
752 expect(warnFn).toHaveBeenCalledWith(
753 expect.objectContaining({
754 handle: TEST_HANDLE,
755 serviceEndpoint: TEST_SERVICE_ENDPOINT,
756 }) as Record<string, unknown>,
757 'PLC DID generation requested but PlcDidService not available'
758 )
759 })
760 })
761})