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