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}