A community based topic aggregation platform built on atproto
at main 46 kB view raw
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}