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