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