A community based topic aggregation platform built on atproto
1package comments
2
3import (
4 "Coves/internal/core/communities"
5 "Coves/internal/core/posts"
6 "Coves/internal/core/users"
7 "context"
8 "errors"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/assert"
13)
14
15// Mock implementations for testing
16
17// mockCommentRepo is a mock implementation of the comment Repository interface
18type mockCommentRepo struct {
19 comments map[string]*Comment
20 listByParentWithHotRankFunc func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error)
21 listByParentsBatchFunc func(ctx context.Context, parentURIs []string, sort string, limitPerParent int) (map[string][]*Comment, error)
22 getVoteStateForCommentsFunc func(ctx context.Context, viewerDID string, commentURIs []string) (map[string]interface{}, error)
23}
24
25func newMockCommentRepo() *mockCommentRepo {
26 return &mockCommentRepo{
27 comments: make(map[string]*Comment),
28 }
29}
30
31func (m *mockCommentRepo) Create(ctx context.Context, comment *Comment) error {
32 m.comments[comment.URI] = comment
33 return nil
34}
35
36func (m *mockCommentRepo) Update(ctx context.Context, comment *Comment) error {
37 if _, ok := m.comments[comment.URI]; !ok {
38 return ErrCommentNotFound
39 }
40 m.comments[comment.URI] = comment
41 return nil
42}
43
44func (m *mockCommentRepo) GetByURI(ctx context.Context, uri string) (*Comment, error) {
45 if c, ok := m.comments[uri]; ok {
46 return c, nil
47 }
48 return nil, ErrCommentNotFound
49}
50
51func (m *mockCommentRepo) Delete(ctx context.Context, uri string) error {
52 delete(m.comments, uri)
53 return nil
54}
55
56func (m *mockCommentRepo) ListByRoot(ctx context.Context, rootURI string, limit, offset int) ([]*Comment, error) {
57 return nil, nil
58}
59
60func (m *mockCommentRepo) ListByParent(ctx context.Context, parentURI string, limit, offset int) ([]*Comment, error) {
61 return nil, nil
62}
63
64func (m *mockCommentRepo) CountByParent(ctx context.Context, parentURI string) (int, error) {
65 return 0, nil
66}
67
68func (m *mockCommentRepo) ListByCommenter(ctx context.Context, commenterDID string, limit, offset int) ([]*Comment, error) {
69 return nil, nil
70}
71
72func (m *mockCommentRepo) ListByParentWithHotRank(
73 ctx context.Context,
74 parentURI string,
75 sort string,
76 timeframe string,
77 limit int,
78 cursor *string,
79) ([]*Comment, *string, error) {
80 if m.listByParentWithHotRankFunc != nil {
81 return m.listByParentWithHotRankFunc(ctx, parentURI, sort, timeframe, limit, cursor)
82 }
83 return []*Comment{}, nil, nil
84}
85
86func (m *mockCommentRepo) GetByURIsBatch(ctx context.Context, uris []string) (map[string]*Comment, error) {
87 result := make(map[string]*Comment)
88 for _, uri := range uris {
89 if c, ok := m.comments[uri]; ok {
90 result[uri] = c
91 }
92 }
93 return result, nil
94}
95
96func (m *mockCommentRepo) GetVoteStateForComments(ctx context.Context, viewerDID string, commentURIs []string) (map[string]interface{}, error) {
97 if m.getVoteStateForCommentsFunc != nil {
98 return m.getVoteStateForCommentsFunc(ctx, viewerDID, commentURIs)
99 }
100 return make(map[string]interface{}), nil
101}
102
103func (m *mockCommentRepo) ListByParentsBatch(
104 ctx context.Context,
105 parentURIs []string,
106 sort string,
107 limitPerParent int,
108) (map[string][]*Comment, error) {
109 if m.listByParentsBatchFunc != nil {
110 return m.listByParentsBatchFunc(ctx, parentURIs, sort, limitPerParent)
111 }
112 return make(map[string][]*Comment), nil
113}
114
115// mockUserRepo is a mock implementation of the users.UserRepository interface
116type mockUserRepo struct {
117 users map[string]*users.User
118}
119
120func newMockUserRepo() *mockUserRepo {
121 return &mockUserRepo{
122 users: make(map[string]*users.User),
123 }
124}
125
126func (m *mockUserRepo) Create(ctx context.Context, user *users.User) (*users.User, error) {
127 m.users[user.DID] = user
128 return user, nil
129}
130
131func (m *mockUserRepo) GetByDID(ctx context.Context, did string) (*users.User, error) {
132 if u, ok := m.users[did]; ok {
133 return u, nil
134 }
135 return nil, errors.New("user not found")
136}
137
138func (m *mockUserRepo) GetByHandle(ctx context.Context, handle string) (*users.User, error) {
139 for _, u := range m.users {
140 if u.Handle == handle {
141 return u, nil
142 }
143 }
144 return nil, errors.New("user not found")
145}
146
147func (m *mockUserRepo) UpdateHandle(ctx context.Context, did, newHandle string) (*users.User, error) {
148 if u, ok := m.users[did]; ok {
149 u.Handle = newHandle
150 return u, nil
151 }
152 return nil, errors.New("user not found")
153}
154
155func (m *mockUserRepo) GetByDIDs(ctx context.Context, dids []string) (map[string]*users.User, error) {
156 result := make(map[string]*users.User, len(dids))
157 for _, did := range dids {
158 if u, ok := m.users[did]; ok {
159 result[did] = u
160 }
161 }
162 return result, nil
163}
164
165// mockPostRepo is a mock implementation of the posts.Repository interface
166type mockPostRepo struct {
167 posts map[string]*posts.Post
168}
169
170func newMockPostRepo() *mockPostRepo {
171 return &mockPostRepo{
172 posts: make(map[string]*posts.Post),
173 }
174}
175
176func (m *mockPostRepo) Create(ctx context.Context, post *posts.Post) error {
177 m.posts[post.URI] = post
178 return nil
179}
180
181func (m *mockPostRepo) GetByURI(ctx context.Context, uri string) (*posts.Post, error) {
182 if p, ok := m.posts[uri]; ok {
183 return p, nil
184 }
185 return nil, posts.NewNotFoundError("post", uri)
186}
187
188// mockCommunityRepo is a mock implementation of the communities.Repository interface
189type mockCommunityRepo struct {
190 communities map[string]*communities.Community
191}
192
193func newMockCommunityRepo() *mockCommunityRepo {
194 return &mockCommunityRepo{
195 communities: make(map[string]*communities.Community),
196 }
197}
198
199func (m *mockCommunityRepo) Create(ctx context.Context, community *communities.Community) (*communities.Community, error) {
200 m.communities[community.DID] = community
201 return community, nil
202}
203
204func (m *mockCommunityRepo) GetByDID(ctx context.Context, did string) (*communities.Community, error) {
205 if c, ok := m.communities[did]; ok {
206 return c, nil
207 }
208 return nil, communities.ErrCommunityNotFound
209}
210
211func (m *mockCommunityRepo) GetByHandle(ctx context.Context, handle string) (*communities.Community, error) {
212 for _, c := range m.communities {
213 if c.Handle == handle {
214 return c, nil
215 }
216 }
217 return nil, communities.ErrCommunityNotFound
218}
219
220func (m *mockCommunityRepo) Update(ctx context.Context, community *communities.Community) (*communities.Community, error) {
221 m.communities[community.DID] = community
222 return community, nil
223}
224
225func (m *mockCommunityRepo) Delete(ctx context.Context, did string) error {
226 delete(m.communities, did)
227 return nil
228}
229
230func (m *mockCommunityRepo) UpdateCredentials(ctx context.Context, did, accessToken, refreshToken string) error {
231 return nil
232}
233
234func (m *mockCommunityRepo) List(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, error) {
235 return nil, nil
236}
237
238func (m *mockCommunityRepo) Search(ctx context.Context, req communities.SearchCommunitiesRequest) ([]*communities.Community, int, error) {
239 return nil, 0, nil
240}
241
242func (m *mockCommunityRepo) Subscribe(ctx context.Context, subscription *communities.Subscription) (*communities.Subscription, error) {
243 return nil, nil
244}
245
246func (m *mockCommunityRepo) SubscribeWithCount(ctx context.Context, subscription *communities.Subscription) (*communities.Subscription, error) {
247 return nil, nil
248}
249
250func (m *mockCommunityRepo) Unsubscribe(ctx context.Context, userDID, communityDID string) error {
251 return nil
252}
253
254func (m *mockCommunityRepo) UnsubscribeWithCount(ctx context.Context, userDID, communityDID string) error {
255 return nil
256}
257
258func (m *mockCommunityRepo) GetSubscription(ctx context.Context, userDID, communityDID string) (*communities.Subscription, error) {
259 return nil, nil
260}
261
262func (m *mockCommunityRepo) GetSubscriptionByURI(ctx context.Context, recordURI string) (*communities.Subscription, error) {
263 return nil, nil
264}
265
266func (m *mockCommunityRepo) ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*communities.Subscription, error) {
267 return nil, nil
268}
269
270func (m *mockCommunityRepo) ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*communities.Subscription, error) {
271 return nil, nil
272}
273
274func (m *mockCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) {
275 return nil, nil
276}
277
278func (m *mockCommunityRepo) UnblockCommunity(ctx context.Context, userDID, communityDID string) error {
279 return nil
280}
281
282func (m *mockCommunityRepo) GetBlock(ctx context.Context, userDID, communityDID string) (*communities.CommunityBlock, error) {
283 return nil, nil
284}
285
286func (m *mockCommunityRepo) GetBlockByURI(ctx context.Context, recordURI string) (*communities.CommunityBlock, error) {
287 return nil, nil
288}
289
290func (m *mockCommunityRepo) ListBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*communities.CommunityBlock, error) {
291 return nil, nil
292}
293
294func (m *mockCommunityRepo) IsBlocked(ctx context.Context, userDID, communityDID string) (bool, error) {
295 return false, nil
296}
297
298func (m *mockCommunityRepo) CreateMembership(ctx context.Context, membership *communities.Membership) (*communities.Membership, error) {
299 return nil, nil
300}
301
302func (m *mockCommunityRepo) GetMembership(ctx context.Context, userDID, communityDID string) (*communities.Membership, error) {
303 return nil, nil
304}
305
306func (m *mockCommunityRepo) UpdateMembership(ctx context.Context, membership *communities.Membership) (*communities.Membership, error) {
307 return nil, nil
308}
309
310func (m *mockCommunityRepo) ListMembers(ctx context.Context, communityDID string, limit, offset int) ([]*communities.Membership, error) {
311 return nil, nil
312}
313
314func (m *mockCommunityRepo) CreateModerationAction(ctx context.Context, action *communities.ModerationAction) (*communities.ModerationAction, error) {
315 return nil, nil
316}
317
318func (m *mockCommunityRepo) ListModerationActions(ctx context.Context, communityDID string, limit, offset int) ([]*communities.ModerationAction, error) {
319 return nil, nil
320}
321
322func (m *mockCommunityRepo) IncrementMemberCount(ctx context.Context, communityDID string) error {
323 return nil
324}
325
326func (m *mockCommunityRepo) DecrementMemberCount(ctx context.Context, communityDID string) error {
327 return nil
328}
329
330func (m *mockCommunityRepo) IncrementSubscriberCount(ctx context.Context, communityDID string) error {
331 return nil
332}
333
334func (m *mockCommunityRepo) DecrementSubscriberCount(ctx context.Context, communityDID string) error {
335 return nil
336}
337
338func (m *mockCommunityRepo) IncrementPostCount(ctx context.Context, communityDID string) error {
339 return nil
340}
341
342// Helper functions to create test data
343
344func createTestPost(uri, authorDID, communityDID string) *posts.Post {
345 title := "Test Post"
346 content := "Test content"
347 return &posts.Post{
348 URI: uri,
349 CID: "bafytest123",
350 RKey: "testrkey",
351 AuthorDID: authorDID,
352 CommunityDID: communityDID,
353 Title: &title,
354 Content: &content,
355 CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
356 IndexedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
357 UpvoteCount: 10,
358 DownvoteCount: 2,
359 Score: 8,
360 CommentCount: 5,
361 }
362}
363
364func createTestComment(uri, commenterDID, commenterHandle, rootURI, parentURI string, replyCount int) *Comment {
365 return &Comment{
366 URI: uri,
367 CID: "bafycomment123",
368 RKey: "commentrkey",
369 CommenterDID: commenterDID,
370 CommenterHandle: commenterHandle,
371 Content: "Test comment content",
372 RootURI: rootURI,
373 RootCID: "bafyroot123",
374 ParentURI: parentURI,
375 ParentCID: "bafyparent123",
376 CreatedAt: time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
377 IndexedAt: time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
378 UpvoteCount: 5,
379 DownvoteCount: 1,
380 Score: 4,
381 ReplyCount: replyCount,
382 Langs: []string{"en"},
383 }
384}
385
386func createTestUser(did, handle string) *users.User {
387 return &users.User{
388 DID: did,
389 Handle: handle,
390 PDSURL: "https://test.pds.local",
391 CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
392 UpdatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
393 }
394}
395
396func createTestCommunity(did, handle string) *communities.Community {
397 return &communities.Community{
398 DID: did,
399 Handle: handle,
400 Name: "test",
401 DisplayName: "Test Community",
402 Description: "Test description",
403 Visibility: "public",
404 OwnerDID: did,
405 CreatedByDID: "did:plc:creator",
406 HostedByDID: "did:web:coves.social",
407 CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
408 UpdatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
409 }
410}
411
412// Test suite for GetComments
413
414func TestCommentService_GetComments_ValidRequest(t *testing.T) {
415 // Setup
416 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
417 authorDID := "did:plc:author123"
418 communityDID := "did:plc:community123"
419 commenterDID := "did:plc:commenter123"
420 viewerDID := "did:plc:viewer123"
421
422 commentRepo := newMockCommentRepo()
423 userRepo := newMockUserRepo()
424 postRepo := newMockPostRepo()
425 communityRepo := newMockCommunityRepo()
426
427 // Setup test data
428 post := createTestPost(postURI, authorDID, communityDID)
429 _ = postRepo.Create(context.Background(), post)
430
431 author := createTestUser(authorDID, "author.test")
432 _, _ = userRepo.Create(context.Background(), author)
433
434 community := createTestCommunity(communityDID, "test.community.coves.social")
435 _, _ = communityRepo.Create(context.Background(), community)
436
437 comment1 := createTestComment("at://did:plc:commenter123/comment/1", commenterDID, "commenter.test", postURI, postURI, 0)
438 comment2 := createTestComment("at://did:plc:commenter123/comment/2", commenterDID, "commenter.test", postURI, postURI, 0)
439
440 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) {
441 if parentURI == postURI {
442 return []*Comment{comment1, comment2}, nil, nil
443 }
444 return []*Comment{}, nil, nil
445 }
446
447 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
448
449 // Execute
450 req := &GetCommentsRequest{
451 PostURI: postURI,
452 ViewerDID: &viewerDID,
453 Sort: "hot",
454 Depth: 10,
455 Limit: 50,
456 }
457
458 resp, err := service.GetComments(context.Background(), req)
459
460 // Verify
461 assert.NoError(t, err)
462 assert.NotNil(t, resp)
463 assert.Len(t, resp.Comments, 2)
464 assert.NotNil(t, resp.Post)
465 assert.Nil(t, resp.Cursor)
466}
467
468func TestCommentService_GetComments_InvalidPostURI(t *testing.T) {
469 // Setup
470 commentRepo := newMockCommentRepo()
471 userRepo := newMockUserRepo()
472 postRepo := newMockPostRepo()
473 communityRepo := newMockCommunityRepo()
474
475 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
476
477 tests := []struct {
478 name string
479 postURI string
480 wantErr string
481 }{
482 {
483 name: "empty post URI",
484 postURI: "",
485 wantErr: "post URI is required",
486 },
487 {
488 name: "invalid URI format",
489 postURI: "http://invalid.com/post",
490 wantErr: "invalid AT-URI format",
491 },
492 }
493
494 for _, tt := range tests {
495 t.Run(tt.name, func(t *testing.T) {
496 req := &GetCommentsRequest{
497 PostURI: tt.postURI,
498 Sort: "hot",
499 Depth: 10,
500 Limit: 50,
501 }
502
503 resp, err := service.GetComments(context.Background(), req)
504
505 assert.Error(t, err)
506 assert.Nil(t, resp)
507 assert.Contains(t, err.Error(), tt.wantErr)
508 })
509 }
510}
511
512func TestCommentService_GetComments_PostNotFound(t *testing.T) {
513 // Setup
514 commentRepo := newMockCommentRepo()
515 userRepo := newMockUserRepo()
516 postRepo := newMockPostRepo()
517 communityRepo := newMockCommunityRepo()
518
519 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
520
521 // Execute
522 req := &GetCommentsRequest{
523 PostURI: "at://did:plc:post123/app.bsky.feed.post/nonexistent",
524 Sort: "hot",
525 Depth: 10,
526 Limit: 50,
527 }
528
529 resp, err := service.GetComments(context.Background(), req)
530
531 // Verify
532 assert.Error(t, err)
533 assert.Nil(t, resp)
534 assert.Equal(t, ErrRootNotFound, err)
535}
536
537func TestCommentService_GetComments_EmptyComments(t *testing.T) {
538 // Setup
539 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
540 authorDID := "did:plc:author123"
541 communityDID := "did:plc:community123"
542
543 commentRepo := newMockCommentRepo()
544 userRepo := newMockUserRepo()
545 postRepo := newMockPostRepo()
546 communityRepo := newMockCommunityRepo()
547
548 // Setup test data
549 post := createTestPost(postURI, authorDID, communityDID)
550 _ = postRepo.Create(context.Background(), post)
551
552 author := createTestUser(authorDID, "author.test")
553 _, _ = userRepo.Create(context.Background(), author)
554
555 community := createTestCommunity(communityDID, "test.community.coves.social")
556 _, _ = communityRepo.Create(context.Background(), community)
557
558 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) {
559 return []*Comment{}, nil, nil
560 }
561
562 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
563
564 // Execute
565 req := &GetCommentsRequest{
566 PostURI: postURI,
567 Sort: "hot",
568 Depth: 10,
569 Limit: 50,
570 }
571
572 resp, err := service.GetComments(context.Background(), req)
573
574 // Verify
575 assert.NoError(t, err)
576 assert.NotNil(t, resp)
577 assert.Len(t, resp.Comments, 0)
578 assert.NotNil(t, resp.Post)
579}
580
581func TestCommentService_GetComments_WithViewerVotes(t *testing.T) {
582 // Setup
583 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
584 authorDID := "did:plc:author123"
585 communityDID := "did:plc:community123"
586 commenterDID := "did:plc:commenter123"
587 viewerDID := "did:plc:viewer123"
588
589 commentRepo := newMockCommentRepo()
590 userRepo := newMockUserRepo()
591 postRepo := newMockPostRepo()
592 communityRepo := newMockCommunityRepo()
593
594 // Setup test data
595 post := createTestPost(postURI, authorDID, communityDID)
596 _ = postRepo.Create(context.Background(), post)
597
598 author := createTestUser(authorDID, "author.test")
599 _, _ = userRepo.Create(context.Background(), author)
600
601 community := createTestCommunity(communityDID, "test.community.coves.social")
602 _, _ = communityRepo.Create(context.Background(), community)
603
604 comment1URI := "at://did:plc:commenter123/comment/1"
605 comment1 := createTestComment(comment1URI, commenterDID, "commenter.test", postURI, postURI, 0)
606
607 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) {
608 if parentURI == postURI {
609 return []*Comment{comment1}, nil, nil
610 }
611 return []*Comment{}, nil, nil
612 }
613
614 // Mock vote state
615 commentRepo.getVoteStateForCommentsFunc = func(ctx context.Context, viewerDID string, commentURIs []string) (map[string]interface{}, error) {
616 voteURI := "at://did:plc:viewer123/vote/1"
617 return map[string]interface{}{
618 comment1URI: map[string]interface{}{
619 "direction": "up",
620 "uri": voteURI,
621 },
622 }, nil
623 }
624
625 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
626
627 // Execute
628 req := &GetCommentsRequest{
629 PostURI: postURI,
630 ViewerDID: &viewerDID,
631 Sort: "hot",
632 Depth: 10,
633 Limit: 50,
634 }
635
636 resp, err := service.GetComments(context.Background(), req)
637
638 // Verify
639 assert.NoError(t, err)
640 assert.NotNil(t, resp)
641 assert.Len(t, resp.Comments, 1)
642
643 // Check viewer state
644 commentView := resp.Comments[0].Comment
645 assert.NotNil(t, commentView.Viewer)
646 assert.NotNil(t, commentView.Viewer.Vote)
647 assert.Equal(t, "up", *commentView.Viewer.Vote)
648 assert.NotNil(t, commentView.Viewer.VoteURI)
649}
650
651func TestCommentService_GetComments_WithoutViewer(t *testing.T) {
652 // Setup
653 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
654 authorDID := "did:plc:author123"
655 communityDID := "did:plc:community123"
656 commenterDID := "did:plc:commenter123"
657
658 commentRepo := newMockCommentRepo()
659 userRepo := newMockUserRepo()
660 postRepo := newMockPostRepo()
661 communityRepo := newMockCommunityRepo()
662
663 // Setup test data
664 post := createTestPost(postURI, authorDID, communityDID)
665 _ = postRepo.Create(context.Background(), post)
666
667 author := createTestUser(authorDID, "author.test")
668 _, _ = userRepo.Create(context.Background(), author)
669
670 community := createTestCommunity(communityDID, "test.community.coves.social")
671 _, _ = communityRepo.Create(context.Background(), community)
672
673 comment1 := createTestComment("at://did:plc:commenter123/comment/1", commenterDID, "commenter.test", postURI, postURI, 0)
674
675 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) {
676 if parentURI == postURI {
677 return []*Comment{comment1}, nil, nil
678 }
679 return []*Comment{}, nil, nil
680 }
681
682 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
683
684 // Execute without viewer
685 req := &GetCommentsRequest{
686 PostURI: postURI,
687 ViewerDID: nil,
688 Sort: "hot",
689 Depth: 10,
690 Limit: 50,
691 }
692
693 resp, err := service.GetComments(context.Background(), req)
694
695 // Verify
696 assert.NoError(t, err)
697 assert.NotNil(t, resp)
698 assert.Len(t, resp.Comments, 1)
699
700 // Viewer state should be nil
701 commentView := resp.Comments[0].Comment
702 assert.Nil(t, commentView.Viewer)
703}
704
705func TestCommentService_GetComments_SortingOptions(t *testing.T) {
706 // Setup
707 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
708 authorDID := "did:plc:author123"
709 communityDID := "did:plc:community123"
710 commenterDID := "did:plc:commenter123"
711
712 tests := []struct {
713 name string
714 sort string
715 timeframe string
716 wantErr bool
717 }{
718 {"hot sorting", "hot", "", false},
719 {"top sorting", "top", "day", false},
720 {"new sorting", "new", "", false},
721 {"invalid sorting", "invalid", "", true},
722 }
723
724 for _, tt := range tests {
725 t.Run(tt.name, func(t *testing.T) {
726 commentRepo := newMockCommentRepo()
727 userRepo := newMockUserRepo()
728 postRepo := newMockPostRepo()
729 communityRepo := newMockCommunityRepo()
730
731 if !tt.wantErr {
732 post := createTestPost(postURI, authorDID, communityDID)
733 _ = postRepo.Create(context.Background(), post)
734
735 author := createTestUser(authorDID, "author.test")
736 _, _ = userRepo.Create(context.Background(), author)
737
738 community := createTestCommunity(communityDID, "test.community.coves.social")
739 _, _ = communityRepo.Create(context.Background(), community)
740
741 comment1 := createTestComment("at://did:plc:commenter123/comment/1", commenterDID, "commenter.test", postURI, postURI, 0)
742
743 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) {
744 return []*Comment{comment1}, nil, nil
745 }
746 }
747
748 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
749
750 req := &GetCommentsRequest{
751 PostURI: postURI,
752 Sort: tt.sort,
753 Timeframe: tt.timeframe,
754 Depth: 10,
755 Limit: 50,
756 }
757
758 resp, err := service.GetComments(context.Background(), req)
759
760 if tt.wantErr {
761 assert.Error(t, err)
762 assert.Nil(t, resp)
763 } else {
764 assert.NoError(t, err)
765 assert.NotNil(t, resp)
766 }
767 })
768 }
769}
770
771func TestCommentService_GetComments_RepositoryError(t *testing.T) {
772 // Setup
773 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
774 authorDID := "did:plc:author123"
775 communityDID := "did:plc:community123"
776
777 commentRepo := newMockCommentRepo()
778 userRepo := newMockUserRepo()
779 postRepo := newMockPostRepo()
780 communityRepo := newMockCommunityRepo()
781
782 // Setup test data
783 post := createTestPost(postURI, authorDID, communityDID)
784 _ = postRepo.Create(context.Background(), post)
785
786 author := createTestUser(authorDID, "author.test")
787 _, _ = userRepo.Create(context.Background(), author)
788
789 community := createTestCommunity(communityDID, "test.community.coves.social")
790 _, _ = communityRepo.Create(context.Background(), community)
791
792 // Mock repository error
793 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) {
794 return nil, nil, errors.New("database error")
795 }
796
797 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo)
798
799 // Execute
800 req := &GetCommentsRequest{
801 PostURI: postURI,
802 Sort: "hot",
803 Depth: 10,
804 Limit: 50,
805 }
806
807 resp, err := service.GetComments(context.Background(), req)
808
809 // Verify
810 assert.Error(t, err)
811 assert.Nil(t, resp)
812 assert.Contains(t, err.Error(), "failed to fetch top-level comments")
813}
814
815// Test suite for buildThreadViews
816
817func TestCommentService_buildThreadViews_EmptyInput(t *testing.T) {
818 // Setup
819 commentRepo := newMockCommentRepo()
820 userRepo := newMockUserRepo()
821 postRepo := newMockPostRepo()
822 communityRepo := newMockCommunityRepo()
823
824 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
825
826 // Execute
827 result := service.buildThreadViews(context.Background(), []*Comment{}, 10, "hot", nil)
828
829 // Verify - should return empty slice, not nil
830 assert.NotNil(t, result)
831 assert.Len(t, result, 0)
832}
833
834func TestCommentService_buildThreadViews_SkipsDeletedComments(t *testing.T) {
835 // Setup
836 commentRepo := newMockCommentRepo()
837 userRepo := newMockUserRepo()
838 postRepo := newMockPostRepo()
839 communityRepo := newMockCommunityRepo()
840
841 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
842 deletedAt := time.Now()
843
844 // Create a deleted comment
845 deletedComment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
846 deletedComment.DeletedAt = &deletedAt
847
848 // Create a normal comment
849 normalComment := createTestComment("at://did:plc:commenter123/comment/2", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
850
851 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
852
853 // Execute
854 result := service.buildThreadViews(context.Background(), []*Comment{deletedComment, normalComment}, 10, "hot", nil)
855
856 // Verify - should only include non-deleted comment
857 assert.Len(t, result, 1)
858 assert.Equal(t, normalComment.URI, result[0].Comment.URI)
859}
860
861func TestCommentService_buildThreadViews_WithNestedReplies(t *testing.T) {
862 // Setup
863 commentRepo := newMockCommentRepo()
864 userRepo := newMockUserRepo()
865 postRepo := newMockPostRepo()
866 communityRepo := newMockCommunityRepo()
867
868 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
869 parentURI := "at://did:plc:commenter123/comment/1"
870 childURI := "at://did:plc:commenter123/comment/2"
871
872 // Parent comment with replies
873 parentComment := createTestComment(parentURI, "did:plc:commenter123", "commenter.test", postURI, postURI, 1)
874
875 // Child comment
876 childComment := createTestComment(childURI, "did:plc:commenter123", "commenter.test", postURI, parentURI, 0)
877
878 // Mock batch loading of replies
879 commentRepo.listByParentsBatchFunc = func(ctx context.Context, parentURIs []string, sort string, limitPerParent int) (map[string][]*Comment, error) {
880 return map[string][]*Comment{
881 parentURI: {childComment},
882 }, nil
883 }
884
885 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
886
887 // Execute with depth > 0 to load replies
888 result := service.buildThreadViews(context.Background(), []*Comment{parentComment}, 1, "hot", nil)
889
890 // Verify
891 assert.Len(t, result, 1)
892 assert.Equal(t, parentURI, result[0].Comment.URI)
893
894 // Check nested replies
895 assert.NotNil(t, result[0].Replies)
896 assert.Len(t, result[0].Replies, 1)
897 assert.Equal(t, childURI, result[0].Replies[0].Comment.URI)
898}
899
900func TestCommentService_buildThreadViews_DepthLimit(t *testing.T) {
901 // Setup
902 commentRepo := newMockCommentRepo()
903 userRepo := newMockUserRepo()
904 postRepo := newMockPostRepo()
905 communityRepo := newMockCommunityRepo()
906
907 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
908
909 // Comment with replies but depth = 0
910 parentComment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 5)
911
912 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
913
914 // Execute with depth = 0 (should not load replies)
915 result := service.buildThreadViews(context.Background(), []*Comment{parentComment}, 0, "hot", nil)
916
917 // Verify
918 assert.Len(t, result, 1)
919 assert.Nil(t, result[0].Replies)
920 assert.True(t, result[0].HasMore) // Should indicate more replies exist
921}
922
923// Test suite for buildCommentView
924
925func TestCommentService_buildCommentView_BasicFields(t *testing.T) {
926 // Setup
927 commentRepo := newMockCommentRepo()
928 userRepo := newMockUserRepo()
929 postRepo := newMockPostRepo()
930 communityRepo := newMockCommunityRepo()
931
932 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
933 commentURI := "at://did:plc:commenter123/comment/1"
934
935 comment := createTestComment(commentURI, "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
936
937 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
938
939 // Execute
940 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
941
942 // Verify basic fields
943 assert.Equal(t, commentURI, result.URI)
944 assert.Equal(t, comment.CID, result.CID)
945 assert.Equal(t, comment.Content, result.Content)
946 assert.NotNil(t, result.Author)
947 assert.Equal(t, "did:plc:commenter123", result.Author.DID)
948 assert.Equal(t, "commenter.test", result.Author.Handle)
949 assert.NotNil(t, result.Stats)
950 assert.Equal(t, 5, result.Stats.Upvotes)
951 assert.Equal(t, 1, result.Stats.Downvotes)
952 assert.Equal(t, 4, result.Stats.Score)
953 assert.Equal(t, 0, result.Stats.ReplyCount)
954}
955
956func TestCommentService_buildCommentView_TopLevelComment(t *testing.T) {
957 // Setup
958 commentRepo := newMockCommentRepo()
959 userRepo := newMockUserRepo()
960 postRepo := newMockPostRepo()
961 communityRepo := newMockCommunityRepo()
962
963 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
964 commentURI := "at://did:plc:commenter123/comment/1"
965
966 // Top-level comment (parent = root)
967 comment := createTestComment(commentURI, "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
968
969 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
970
971 // Execute
972 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
973
974 // Verify - parent should be nil for top-level comments
975 assert.NotNil(t, result.Post)
976 assert.Equal(t, postURI, result.Post.URI)
977 assert.Nil(t, result.Parent)
978}
979
980func TestCommentService_buildCommentView_NestedComment(t *testing.T) {
981 // Setup
982 commentRepo := newMockCommentRepo()
983 userRepo := newMockUserRepo()
984 postRepo := newMockPostRepo()
985 communityRepo := newMockCommunityRepo()
986
987 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
988 parentCommentURI := "at://did:plc:commenter123/comment/1"
989 childCommentURI := "at://did:plc:commenter123/comment/2"
990
991 // Nested comment (parent != root)
992 comment := createTestComment(childCommentURI, "did:plc:commenter123", "commenter.test", postURI, parentCommentURI, 0)
993
994 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
995
996 // Execute
997 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
998
999 // Verify - both post and parent should be present
1000 assert.NotNil(t, result.Post)
1001 assert.Equal(t, postURI, result.Post.URI)
1002 assert.NotNil(t, result.Parent)
1003 assert.Equal(t, parentCommentURI, result.Parent.URI)
1004}
1005
1006func TestCommentService_buildCommentView_WithViewerVote(t *testing.T) {
1007 // Setup
1008 commentRepo := newMockCommentRepo()
1009 userRepo := newMockUserRepo()
1010 postRepo := newMockPostRepo()
1011 communityRepo := newMockCommunityRepo()
1012
1013 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1014 commentURI := "at://did:plc:commenter123/comment/1"
1015 viewerDID := "did:plc:viewer123"
1016 voteURI := "at://did:plc:viewer123/vote/1"
1017
1018 comment := createTestComment(commentURI, "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1019
1020 // Mock vote state
1021 voteStates := map[string]interface{}{
1022 commentURI: map[string]interface{}{
1023 "direction": "down",
1024 "uri": voteURI,
1025 },
1026 }
1027
1028 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1029
1030 // Execute
1031 result := service.buildCommentView(comment, &viewerDID, voteStates, make(map[string]*users.User))
1032
1033 // Verify viewer state
1034 assert.NotNil(t, result.Viewer)
1035 assert.NotNil(t, result.Viewer.Vote)
1036 assert.Equal(t, "down", *result.Viewer.Vote)
1037 assert.NotNil(t, result.Viewer.VoteURI)
1038 assert.Equal(t, voteURI, *result.Viewer.VoteURI)
1039}
1040
1041func TestCommentService_buildCommentView_NoViewerVote(t *testing.T) {
1042 // Setup
1043 commentRepo := newMockCommentRepo()
1044 userRepo := newMockUserRepo()
1045 postRepo := newMockPostRepo()
1046 communityRepo := newMockCommunityRepo()
1047
1048 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1049 commentURI := "at://did:plc:commenter123/comment/1"
1050 viewerDID := "did:plc:viewer123"
1051
1052 comment := createTestComment(commentURI, "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1053
1054 // Empty vote states
1055 voteStates := map[string]interface{}{}
1056
1057 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1058
1059 // Execute
1060 result := service.buildCommentView(comment, &viewerDID, voteStates, make(map[string]*users.User))
1061
1062 // Verify viewer state exists but has no votes
1063 assert.NotNil(t, result.Viewer)
1064 assert.Nil(t, result.Viewer.Vote)
1065 assert.Nil(t, result.Viewer.VoteURI)
1066}
1067
1068// Test suite for validateGetCommentsRequest
1069
1070func TestValidateGetCommentsRequest_NilRequest(t *testing.T) {
1071 err := validateGetCommentsRequest(nil)
1072 assert.Error(t, err)
1073 assert.Contains(t, err.Error(), "request cannot be nil")
1074}
1075
1076func TestValidateGetCommentsRequest_Defaults(t *testing.T) {
1077 req := &GetCommentsRequest{
1078 PostURI: "at://did:plc:post123/app.bsky.feed.post/test",
1079 // Depth and Limit are 0 (zero values)
1080 }
1081
1082 err := validateGetCommentsRequest(req)
1083 assert.NoError(t, err)
1084
1085 // Check defaults applied
1086 assert.Equal(t, "hot", req.Sort)
1087 // Depth 0 is valid (means no replies), only negative values get set to 10
1088 assert.Equal(t, 0, req.Depth)
1089 // Limit <= 0 gets set to 50
1090 assert.Equal(t, 50, req.Limit)
1091}
1092
1093func TestValidateGetCommentsRequest_BoundsEnforcement(t *testing.T) {
1094 tests := []struct {
1095 name string
1096 depth int
1097 limit int
1098 expectedDepth int
1099 expectedLimit int
1100 }{
1101 {"negative depth", -1, 10, 10, 10},
1102 {"depth too high", 150, 10, 100, 10},
1103 {"limit too low", 10, 0, 10, 50},
1104 {"limit too high", 10, 200, 10, 100},
1105 }
1106
1107 for _, tt := range tests {
1108 t.Run(tt.name, func(t *testing.T) {
1109 req := &GetCommentsRequest{
1110 PostURI: "at://did:plc:post123/app.bsky.feed.post/test",
1111 Depth: tt.depth,
1112 Limit: tt.limit,
1113 }
1114
1115 err := validateGetCommentsRequest(req)
1116 assert.NoError(t, err)
1117 assert.Equal(t, tt.expectedDepth, req.Depth)
1118 assert.Equal(t, tt.expectedLimit, req.Limit)
1119 })
1120 }
1121}
1122
1123func TestValidateGetCommentsRequest_InvalidSort(t *testing.T) {
1124 req := &GetCommentsRequest{
1125 PostURI: "at://did:plc:post123/app.bsky.feed.post/test",
1126 Sort: "invalid",
1127 Depth: 10,
1128 Limit: 50,
1129 }
1130
1131 err := validateGetCommentsRequest(req)
1132 assert.Error(t, err)
1133 assert.Contains(t, err.Error(), "invalid sort")
1134}
1135
1136func TestValidateGetCommentsRequest_InvalidTimeframe(t *testing.T) {
1137 req := &GetCommentsRequest{
1138 PostURI: "at://did:plc:post123/app.bsky.feed.post/test",
1139 Sort: "top",
1140 Timeframe: "invalid",
1141 Depth: 10,
1142 Limit: 50,
1143 }
1144
1145 err := validateGetCommentsRequest(req)
1146 assert.Error(t, err)
1147 assert.Contains(t, err.Error(), "invalid timeframe")
1148}
1149
1150// Test suite for mockUserRepo.GetByDIDs
1151
1152func TestMockUserRepo_GetByDIDs_EmptyArray(t *testing.T) {
1153 userRepo := newMockUserRepo()
1154 ctx := context.Background()
1155
1156 result, err := userRepo.GetByDIDs(ctx, []string{})
1157
1158 assert.NoError(t, err)
1159 assert.NotNil(t, result)
1160 assert.Len(t, result, 0)
1161}
1162
1163func TestMockUserRepo_GetByDIDs_SingleDID(t *testing.T) {
1164 userRepo := newMockUserRepo()
1165 ctx := context.Background()
1166
1167 // Add test user
1168 testUser := createTestUser("did:plc:user1", "user1.test")
1169 _, _ = userRepo.Create(ctx, testUser)
1170
1171 result, err := userRepo.GetByDIDs(ctx, []string{"did:plc:user1"})
1172
1173 assert.NoError(t, err)
1174 assert.Len(t, result, 1)
1175 assert.Equal(t, "user1.test", result["did:plc:user1"].Handle)
1176}
1177
1178func TestMockUserRepo_GetByDIDs_MultipleDIDs(t *testing.T) {
1179 userRepo := newMockUserRepo()
1180 ctx := context.Background()
1181
1182 // Add multiple test users
1183 user1 := createTestUser("did:plc:user1", "user1.test")
1184 user2 := createTestUser("did:plc:user2", "user2.test")
1185 user3 := createTestUser("did:plc:user3", "user3.test")
1186 _, _ = userRepo.Create(ctx, user1)
1187 _, _ = userRepo.Create(ctx, user2)
1188 _, _ = userRepo.Create(ctx, user3)
1189
1190 result, err := userRepo.GetByDIDs(ctx, []string{"did:plc:user1", "did:plc:user2", "did:plc:user3"})
1191
1192 assert.NoError(t, err)
1193 assert.Len(t, result, 3)
1194 assert.Equal(t, "user1.test", result["did:plc:user1"].Handle)
1195 assert.Equal(t, "user2.test", result["did:plc:user2"].Handle)
1196 assert.Equal(t, "user3.test", result["did:plc:user3"].Handle)
1197}
1198
1199func TestMockUserRepo_GetByDIDs_MissingDIDs(t *testing.T) {
1200 userRepo := newMockUserRepo()
1201 ctx := context.Background()
1202
1203 // Add only one user
1204 user1 := createTestUser("did:plc:user1", "user1.test")
1205 _, _ = userRepo.Create(ctx, user1)
1206
1207 // Query for two users, one missing
1208 result, err := userRepo.GetByDIDs(ctx, []string{"did:plc:user1", "did:plc:missing"})
1209
1210 assert.NoError(t, err)
1211 assert.Len(t, result, 1)
1212 assert.Equal(t, "user1.test", result["did:plc:user1"].Handle)
1213 assert.Nil(t, result["did:plc:missing"]) // Missing users not in map
1214}
1215
1216func TestMockUserRepo_GetByDIDs_PreservesAllFields(t *testing.T) {
1217 userRepo := newMockUserRepo()
1218 ctx := context.Background()
1219
1220 // Create user with all fields populated
1221 testUser := &users.User{
1222 DID: "did:plc:user1",
1223 Handle: "user1.test",
1224 PDSURL: "https://pds.example.com",
1225 CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
1226 UpdatedAt: time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
1227 }
1228 _, _ = userRepo.Create(ctx, testUser)
1229
1230 result, err := userRepo.GetByDIDs(ctx, []string{"did:plc:user1"})
1231
1232 assert.NoError(t, err)
1233 user := result["did:plc:user1"]
1234 assert.Equal(t, "did:plc:user1", user.DID)
1235 assert.Equal(t, "user1.test", user.Handle)
1236 assert.Equal(t, "https://pds.example.com", user.PDSURL)
1237 assert.Equal(t, testUser.CreatedAt, user.CreatedAt)
1238 assert.Equal(t, testUser.UpdatedAt, user.UpdatedAt)
1239}
1240
1241// Test suite for JSON deserialization in buildCommentView and buildCommentRecord
1242
1243func TestBuildCommentView_ValidFacetsDeserialization(t *testing.T) {
1244 commentRepo := newMockCommentRepo()
1245 userRepo := newMockUserRepo()
1246 postRepo := newMockPostRepo()
1247 communityRepo := newMockCommunityRepo()
1248
1249 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1250 facetsJSON := `[{"index":{"byteStart":0,"byteEnd":10},"features":[{"$type":"app.bsky.richtext.facet#mention","did":"did:plc:user123"}]}]`
1251
1252 comment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1253 comment.ContentFacets = &facetsJSON
1254
1255 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1256
1257 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
1258
1259 assert.NotNil(t, result.ContentFacets)
1260 assert.Len(t, result.ContentFacets, 1)
1261}
1262
1263func TestBuildCommentView_ValidEmbedDeserialization(t *testing.T) {
1264 commentRepo := newMockCommentRepo()
1265 userRepo := newMockUserRepo()
1266 postRepo := newMockPostRepo()
1267 communityRepo := newMockCommunityRepo()
1268
1269 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1270 embedJSON := `{"$type":"app.bsky.embed.images","images":[{"alt":"test","image":{"$type":"blob","ref":"bafytest"}}]}`
1271
1272 comment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1273 comment.Embed = &embedJSON
1274
1275 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1276
1277 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
1278
1279 assert.NotNil(t, result.Embed)
1280 embedMap, ok := result.Embed.(map[string]interface{})
1281 assert.True(t, ok)
1282 assert.Equal(t, "app.bsky.embed.images", embedMap["$type"])
1283}
1284
1285func TestBuildCommentRecord_ValidLabelsDeserialization(t *testing.T) {
1286 commentRepo := newMockCommentRepo()
1287 userRepo := newMockUserRepo()
1288 postRepo := newMockPostRepo()
1289 communityRepo := newMockCommunityRepo()
1290
1291 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1292 labelsJSON := `{"$type":"com.atproto.label.defs#selfLabels","values":[{"val":"nsfw"}]}`
1293
1294 comment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1295 comment.ContentLabels = &labelsJSON
1296
1297 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1298
1299 record := service.buildCommentRecord(comment)
1300
1301 assert.NotNil(t, record.Labels)
1302}
1303
1304func TestBuildCommentView_MalformedJSONLogsWarning(t *testing.T) {
1305 commentRepo := newMockCommentRepo()
1306 userRepo := newMockUserRepo()
1307 postRepo := newMockPostRepo()
1308 communityRepo := newMockCommunityRepo()
1309
1310 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1311 malformedJSON := `{"invalid": json`
1312
1313 comment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1314 comment.ContentFacets = &malformedJSON
1315
1316 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1317
1318 // Should not panic, should log warning and return view with nil facets
1319 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
1320
1321 assert.NotNil(t, result)
1322 assert.Nil(t, result.ContentFacets)
1323}
1324
1325func TestBuildCommentView_EmptyStringVsNilHandling(t *testing.T) {
1326 commentRepo := newMockCommentRepo()
1327 userRepo := newMockUserRepo()
1328 postRepo := newMockPostRepo()
1329 communityRepo := newMockCommunityRepo()
1330
1331 postURI := "at://did:plc:post123/app.bsky.feed.post/test"
1332
1333 tests := []struct {
1334 facetsValue *string
1335 embedValue *string
1336 labelsValue *string
1337 name string
1338 expectFacetsNil bool
1339 expectEmbedNil bool
1340 expectRecordLabels bool
1341 }{
1342 {
1343 name: "All nil",
1344 facetsValue: nil,
1345 embedValue: nil,
1346 labelsValue: nil,
1347 expectFacetsNil: true,
1348 expectEmbedNil: true,
1349 expectRecordLabels: false,
1350 },
1351 {
1352 name: "All empty strings",
1353 facetsValue: strPtr(""),
1354 embedValue: strPtr(""),
1355 labelsValue: strPtr(""),
1356 expectFacetsNil: true,
1357 expectEmbedNil: true,
1358 expectRecordLabels: false,
1359 },
1360 {
1361 name: "Valid JSON strings",
1362 facetsValue: strPtr(`[]`),
1363 embedValue: strPtr(`{}`),
1364 labelsValue: strPtr(`{"$type":"com.atproto.label.defs#selfLabels","values":[]}`),
1365 expectFacetsNil: false,
1366 expectEmbedNil: false,
1367 expectRecordLabels: true,
1368 },
1369 }
1370
1371 for _, tt := range tests {
1372 t.Run(tt.name, func(t *testing.T) {
1373 comment := createTestComment("at://did:plc:commenter123/comment/1", "did:plc:commenter123", "commenter.test", postURI, postURI, 0)
1374 comment.ContentFacets = tt.facetsValue
1375 comment.Embed = tt.embedValue
1376 comment.ContentLabels = tt.labelsValue
1377
1378 service := NewCommentService(commentRepo, userRepo, postRepo, communityRepo).(*commentService)
1379
1380 result := service.buildCommentView(comment, nil, nil, make(map[string]*users.User))
1381
1382 if tt.expectFacetsNil {
1383 assert.Nil(t, result.ContentFacets)
1384 } else {
1385 assert.NotNil(t, result.ContentFacets)
1386 }
1387
1388 if tt.expectEmbedNil {
1389 assert.Nil(t, result.Embed)
1390 } else {
1391 assert.NotNil(t, result.Embed)
1392 }
1393
1394 record := service.buildCommentRecord(comment)
1395 if tt.expectRecordLabels {
1396 assert.NotNil(t, record.Labels)
1397 } else {
1398 assert.Nil(t, record.Labels)
1399 }
1400 })
1401 }
1402}
1403
1404// Helper function to create string pointers
1405func strPtr(s string) *string {
1406 return &s
1407}