A community based topic aggregation platform built on atproto
1package comments 2 3import ( 4 "Coves/internal/atproto/pds" 5 "context" 6 "errors" 7 "fmt" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/auth/oauth" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14) 15 16// ================================================================================ 17// Mock PDS Client for Write Operations Testing 18// ================================================================================ 19 20// mockPDSClient implements the pds.Client interface for testing 21// It stores records in memory and allows simulating various PDS error conditions 22type mockPDSClient struct { 23 records map[string]map[string]interface{} // collection -> rkey -> record 24 createError error // Error to return on CreateRecord 25 getError error // Error to return on GetRecord 26 deleteError error // Error to return on DeleteRecord 27 did string // DID of the authenticated user 28 hostURL string // PDS host URL 29} 30 31// newMockPDSClient creates a new mock PDS client for testing 32func newMockPDSClient(did string) *mockPDSClient { 33 return &mockPDSClient{ 34 records: make(map[string]map[string]interface{}), 35 did: did, 36 hostURL: "https://pds.test.local", 37 } 38} 39 40func (m *mockPDSClient) DID() string { 41 return m.did 42} 43 44func (m *mockPDSClient) HostURL() string { 45 return m.hostURL 46} 47 48func (m *mockPDSClient) CreateRecord(ctx context.Context, collection, rkey string, record interface{}) (string, string, error) { 49 if m.createError != nil { 50 return "", "", m.createError 51 } 52 53 // Generate rkey if not provided 54 if rkey == "" { 55 rkey = fmt.Sprintf("test_%d", time.Now().UnixNano()) 56 } 57 58 // Store record 59 if m.records[collection] == nil { 60 m.records[collection] = make(map[string]interface{}) 61 } 62 m.records[collection][rkey] = record 63 64 // Generate response 65 uri := fmt.Sprintf("at://%s/%s/%s", m.did, collection, rkey) 66 cid := fmt.Sprintf("bafytest%d", time.Now().UnixNano()) 67 68 return uri, cid, nil 69} 70 71func (m *mockPDSClient) GetRecord(ctx context.Context, collection, rkey string) (*pds.RecordResponse, error) { 72 if m.getError != nil { 73 return nil, m.getError 74 } 75 76 if m.records[collection] == nil { 77 return nil, pds.ErrNotFound 78 } 79 80 record, ok := m.records[collection][rkey] 81 if !ok { 82 return nil, pds.ErrNotFound 83 } 84 85 uri := fmt.Sprintf("at://%s/%s/%s", m.did, collection, rkey) 86 cid := fmt.Sprintf("bafytest%d", time.Now().UnixNano()) 87 88 return &pds.RecordResponse{ 89 URI: uri, 90 CID: cid, 91 Value: record.(map[string]interface{}), 92 }, nil 93} 94 95func (m *mockPDSClient) DeleteRecord(ctx context.Context, collection, rkey string) error { 96 if m.deleteError != nil { 97 return m.deleteError 98 } 99 100 if m.records[collection] == nil { 101 return pds.ErrNotFound 102 } 103 104 if _, ok := m.records[collection][rkey]; !ok { 105 return pds.ErrNotFound 106 } 107 108 delete(m.records[collection], rkey) 109 return nil 110} 111 112func (m *mockPDSClient) ListRecords(ctx context.Context, collection string, limit int, cursor string) (*pds.ListRecordsResponse, error) { 113 return &pds.ListRecordsResponse{}, nil 114} 115 116// mockPDSClientFactory creates mock PDS clients for testing 117type mockPDSClientFactory struct { 118 client *mockPDSClient 119 err error 120} 121 122func (f *mockPDSClientFactory) create(ctx context.Context, session *oauth.ClientSessionData) (pds.Client, error) { 123 if f.err != nil { 124 return nil, f.err 125 } 126 if f.client == nil { 127 f.client = newMockPDSClient(session.AccountDID.String()) 128 } 129 return f.client, nil 130} 131 132// ================================================================================ 133// Helper Functions 134// ================================================================================ 135 136// createTestSession creates a test OAuth session for a given DID 137func createTestSession(did string) *oauth.ClientSessionData { 138 parsedDID, _ := syntax.ParseDID(did) 139 return &oauth.ClientSessionData{ 140 AccountDID: parsedDID, 141 SessionID: "test-session-123", 142 AccessToken: "test-access-token", 143 HostURL: "https://pds.test.local", 144 } 145} 146 147// ================================================================================ 148// CreateComment Tests 149// ================================================================================ 150 151func TestCreateComment_Success(t *testing.T) { 152 // Setup 153 ctx := context.Background() 154 mockClient := newMockPDSClient("did:plc:test123") 155 factory := &mockPDSClientFactory{client: mockClient} 156 157 commentRepo := newMockCommentRepo() 158 userRepo := newMockUserRepo() 159 postRepo := newMockPostRepo() 160 communityRepo := newMockCommunityRepo() 161 162 service := NewCommentServiceWithPDSFactory( 163 commentRepo, 164 userRepo, 165 postRepo, 166 communityRepo, 167 nil, 168 factory.create, 169 ) 170 171 // Create request 172 req := CreateCommentRequest{ 173 Reply: ReplyRef{ 174 Root: StrongRef{ 175 URI: "at://did:plc:author/social.coves.community.post/root123", 176 CID: "bafyroot", 177 }, 178 Parent: StrongRef{ 179 URI: "at://did:plc:author/social.coves.community.post/root123", 180 CID: "bafyroot", 181 }, 182 }, 183 Content: "This is a test comment", 184 Langs: []string{"en"}, 185 } 186 187 session := createTestSession("did:plc:test123") 188 189 // Execute 190 resp, err := service.CreateComment(ctx, session, req) 191 192 // Verify 193 if err != nil { 194 t.Fatalf("Expected no error, got: %v", err) 195 } 196 if resp == nil { 197 t.Fatal("Expected response, got nil") 198 } 199 if resp.URI == "" { 200 t.Error("Expected URI to be set") 201 } 202 if resp.CID == "" { 203 t.Error("Expected CID to be set") 204 } 205 if !strings.HasPrefix(resp.URI, "at://did:plc:test123") { 206 t.Errorf("Expected URI to start with user's DID, got: %s", resp.URI) 207 } 208} 209 210func TestCreateComment_EmptyContent(t *testing.T) { 211 // Setup 212 ctx := context.Background() 213 mockClient := newMockPDSClient("did:plc:test123") 214 factory := &mockPDSClientFactory{client: mockClient} 215 216 commentRepo := newMockCommentRepo() 217 userRepo := newMockUserRepo() 218 postRepo := newMockPostRepo() 219 communityRepo := newMockCommunityRepo() 220 221 service := NewCommentServiceWithPDSFactory( 222 commentRepo, 223 userRepo, 224 postRepo, 225 communityRepo, 226 nil, 227 factory.create, 228 ) 229 230 req := CreateCommentRequest{ 231 Reply: ReplyRef{ 232 Root: StrongRef{ 233 URI: "at://did:plc:author/social.coves.community.post/root123", 234 CID: "bafyroot", 235 }, 236 Parent: StrongRef{ 237 URI: "at://did:plc:author/social.coves.community.post/root123", 238 CID: "bafyroot", 239 }, 240 }, 241 Content: "", 242 } 243 244 session := createTestSession("did:plc:test123") 245 246 // Execute 247 _, err := service.CreateComment(ctx, session, req) 248 249 // Verify 250 if !errors.Is(err, ErrContentEmpty) { 251 t.Errorf("Expected ErrContentEmpty, got: %v", err) 252 } 253} 254 255func TestCreateComment_ContentTooLong(t *testing.T) { 256 // Setup 257 ctx := context.Background() 258 mockClient := newMockPDSClient("did:plc:test123") 259 factory := &mockPDSClientFactory{client: mockClient} 260 261 commentRepo := newMockCommentRepo() 262 userRepo := newMockUserRepo() 263 postRepo := newMockPostRepo() 264 communityRepo := newMockCommunityRepo() 265 266 service := NewCommentServiceWithPDSFactory( 267 commentRepo, 268 userRepo, 269 postRepo, 270 communityRepo, 271 nil, 272 factory.create, 273 ) 274 275 // Create content with >10000 graphemes (using Unicode characters) 276 longContent := strings.Repeat("あ", 10001) // Japanese character = 1 grapheme 277 278 req := CreateCommentRequest{ 279 Reply: ReplyRef{ 280 Root: StrongRef{ 281 URI: "at://did:plc:author/social.coves.community.post/root123", 282 CID: "bafyroot", 283 }, 284 Parent: StrongRef{ 285 URI: "at://did:plc:author/social.coves.community.post/root123", 286 CID: "bafyroot", 287 }, 288 }, 289 Content: longContent, 290 } 291 292 session := createTestSession("did:plc:test123") 293 294 // Execute 295 _, err := service.CreateComment(ctx, session, req) 296 297 // Verify 298 if !errors.Is(err, ErrContentTooLong) { 299 t.Errorf("Expected ErrContentTooLong, got: %v", err) 300 } 301} 302 303func TestCreateComment_InvalidReplyRootURI(t *testing.T) { 304 // Setup 305 ctx := context.Background() 306 mockClient := newMockPDSClient("did:plc:test123") 307 factory := &mockPDSClientFactory{client: mockClient} 308 309 commentRepo := newMockCommentRepo() 310 userRepo := newMockUserRepo() 311 postRepo := newMockPostRepo() 312 communityRepo := newMockCommunityRepo() 313 314 service := NewCommentServiceWithPDSFactory( 315 commentRepo, 316 userRepo, 317 postRepo, 318 communityRepo, 319 nil, 320 factory.create, 321 ) 322 323 req := CreateCommentRequest{ 324 Reply: ReplyRef{ 325 Root: StrongRef{ 326 URI: "invalid-uri", // Invalid AT-URI 327 CID: "bafyroot", 328 }, 329 Parent: StrongRef{ 330 URI: "at://did:plc:author/social.coves.community.post/root123", 331 CID: "bafyroot", 332 }, 333 }, 334 Content: "Test comment", 335 } 336 337 session := createTestSession("did:plc:test123") 338 339 // Execute 340 _, err := service.CreateComment(ctx, session, req) 341 342 // Verify 343 if !errors.Is(err, ErrInvalidReply) { 344 t.Errorf("Expected ErrInvalidReply, got: %v", err) 345 } 346} 347 348func TestCreateComment_InvalidReplyRootCID(t *testing.T) { 349 // Setup 350 ctx := context.Background() 351 mockClient := newMockPDSClient("did:plc:test123") 352 factory := &mockPDSClientFactory{client: mockClient} 353 354 commentRepo := newMockCommentRepo() 355 userRepo := newMockUserRepo() 356 postRepo := newMockPostRepo() 357 communityRepo := newMockCommunityRepo() 358 359 service := NewCommentServiceWithPDSFactory( 360 commentRepo, 361 userRepo, 362 postRepo, 363 communityRepo, 364 nil, 365 factory.create, 366 ) 367 368 req := CreateCommentRequest{ 369 Reply: ReplyRef{ 370 Root: StrongRef{ 371 URI: "at://did:plc:author/social.coves.community.post/root123", 372 CID: "", // Empty CID 373 }, 374 Parent: StrongRef{ 375 URI: "at://did:plc:author/social.coves.community.post/root123", 376 CID: "bafyroot", 377 }, 378 }, 379 Content: "Test comment", 380 } 381 382 session := createTestSession("did:plc:test123") 383 384 // Execute 385 _, err := service.CreateComment(ctx, session, req) 386 387 // Verify 388 if !errors.Is(err, ErrInvalidReply) { 389 t.Errorf("Expected ErrInvalidReply, got: %v", err) 390 } 391} 392 393func TestCreateComment_InvalidReplyParentURI(t *testing.T) { 394 // Setup 395 ctx := context.Background() 396 mockClient := newMockPDSClient("did:plc:test123") 397 factory := &mockPDSClientFactory{client: mockClient} 398 399 commentRepo := newMockCommentRepo() 400 userRepo := newMockUserRepo() 401 postRepo := newMockPostRepo() 402 communityRepo := newMockCommunityRepo() 403 404 service := NewCommentServiceWithPDSFactory( 405 commentRepo, 406 userRepo, 407 postRepo, 408 communityRepo, 409 nil, 410 factory.create, 411 ) 412 413 req := CreateCommentRequest{ 414 Reply: ReplyRef{ 415 Root: StrongRef{ 416 URI: "at://did:plc:author/social.coves.community.post/root123", 417 CID: "bafyroot", 418 }, 419 Parent: StrongRef{ 420 URI: "invalid-uri", // Invalid AT-URI 421 CID: "bafyparent", 422 }, 423 }, 424 Content: "Test comment", 425 } 426 427 session := createTestSession("did:plc:test123") 428 429 // Execute 430 _, err := service.CreateComment(ctx, session, req) 431 432 // Verify 433 if !errors.Is(err, ErrInvalidReply) { 434 t.Errorf("Expected ErrInvalidReply, got: %v", err) 435 } 436} 437 438func TestCreateComment_InvalidReplyParentCID(t *testing.T) { 439 // Setup 440 ctx := context.Background() 441 mockClient := newMockPDSClient("did:plc:test123") 442 factory := &mockPDSClientFactory{client: mockClient} 443 444 commentRepo := newMockCommentRepo() 445 userRepo := newMockUserRepo() 446 postRepo := newMockPostRepo() 447 communityRepo := newMockCommunityRepo() 448 449 service := NewCommentServiceWithPDSFactory( 450 commentRepo, 451 userRepo, 452 postRepo, 453 communityRepo, 454 nil, 455 factory.create, 456 ) 457 458 req := CreateCommentRequest{ 459 Reply: ReplyRef{ 460 Root: StrongRef{ 461 URI: "at://did:plc:author/social.coves.community.post/root123", 462 CID: "bafyroot", 463 }, 464 Parent: StrongRef{ 465 URI: "at://did:plc:author/social.coves.community.post/root123", 466 CID: "", // Empty CID 467 }, 468 }, 469 Content: "Test comment", 470 } 471 472 session := createTestSession("did:plc:test123") 473 474 // Execute 475 _, err := service.CreateComment(ctx, session, req) 476 477 // Verify 478 if !errors.Is(err, ErrInvalidReply) { 479 t.Errorf("Expected ErrInvalidReply, got: %v", err) 480 } 481} 482 483func TestCreateComment_PDSError(t *testing.T) { 484 // Setup 485 ctx := context.Background() 486 mockClient := newMockPDSClient("did:plc:test123") 487 mockClient.createError = errors.New("PDS connection failed") 488 factory := &mockPDSClientFactory{client: mockClient} 489 490 commentRepo := newMockCommentRepo() 491 userRepo := newMockUserRepo() 492 postRepo := newMockPostRepo() 493 communityRepo := newMockCommunityRepo() 494 495 service := NewCommentServiceWithPDSFactory( 496 commentRepo, 497 userRepo, 498 postRepo, 499 communityRepo, 500 nil, 501 factory.create, 502 ) 503 504 req := CreateCommentRequest{ 505 Reply: ReplyRef{ 506 Root: StrongRef{ 507 URI: "at://did:plc:author/social.coves.community.post/root123", 508 CID: "bafyroot", 509 }, 510 Parent: StrongRef{ 511 URI: "at://did:plc:author/social.coves.community.post/root123", 512 CID: "bafyroot", 513 }, 514 }, 515 Content: "Test comment", 516 } 517 518 session := createTestSession("did:plc:test123") 519 520 // Execute 521 _, err := service.CreateComment(ctx, session, req) 522 523 // Verify 524 if err == nil { 525 t.Fatal("Expected error, got nil") 526 } 527 if !strings.Contains(err.Error(), "failed to create comment") { 528 t.Errorf("Expected PDS error to be wrapped, got: %v", err) 529 } 530} 531 532// ================================================================================ 533// UpdateComment Tests 534// ================================================================================ 535 536func TestUpdateComment_Success(t *testing.T) { 537 // Setup 538 ctx := context.Background() 539 mockClient := newMockPDSClient("did:plc:test123") 540 factory := &mockPDSClientFactory{client: mockClient} 541 542 commentRepo := newMockCommentRepo() 543 userRepo := newMockUserRepo() 544 postRepo := newMockPostRepo() 545 communityRepo := newMockCommunityRepo() 546 547 service := NewCommentServiceWithPDSFactory( 548 commentRepo, 549 userRepo, 550 postRepo, 551 communityRepo, 552 nil, 553 factory.create, 554 ) 555 556 // Pre-create a comment in the mock PDS 557 rkey := "testcomment123" 558 existingRecord := map[string]interface{}{ 559 "$type": "social.coves.community.comment", 560 "content": "Original content", 561 "reply": map[string]interface{}{ 562 "root": map[string]interface{}{ 563 "uri": "at://did:plc:author/social.coves.community.post/root123", 564 "cid": "bafyroot", 565 }, 566 "parent": map[string]interface{}{ 567 "uri": "at://did:plc:author/social.coves.community.post/root123", 568 "cid": "bafyroot", 569 }, 570 }, 571 "createdAt": time.Now().Format(time.RFC3339), 572 } 573 if mockClient.records["social.coves.community.comment"] == nil { 574 mockClient.records["social.coves.community.comment"] = make(map[string]interface{}) 575 } 576 mockClient.records["social.coves.community.comment"][rkey] = existingRecord 577 578 req := UpdateCommentRequest{ 579 URI: fmt.Sprintf("at://did:plc:test123/social.coves.community.comment/%s", rkey), 580 Content: "Updated content", 581 } 582 583 session := createTestSession("did:plc:test123") 584 585 // Execute 586 resp, err := service.UpdateComment(ctx, session, req) 587 588 // Verify 589 if err != nil { 590 t.Fatalf("Expected no error, got: %v", err) 591 } 592 if resp == nil { 593 t.Fatal("Expected response, got nil") 594 } 595 if resp.CID == "" { 596 t.Error("Expected new CID to be set") 597 } 598} 599 600func TestUpdateComment_EmptyURI(t *testing.T) { 601 // Setup 602 ctx := context.Background() 603 mockClient := newMockPDSClient("did:plc:test123") 604 factory := &mockPDSClientFactory{client: mockClient} 605 606 commentRepo := newMockCommentRepo() 607 userRepo := newMockUserRepo() 608 postRepo := newMockPostRepo() 609 communityRepo := newMockCommunityRepo() 610 611 service := NewCommentServiceWithPDSFactory( 612 commentRepo, 613 userRepo, 614 postRepo, 615 communityRepo, 616 nil, 617 factory.create, 618 ) 619 620 req := UpdateCommentRequest{ 621 URI: "", 622 Content: "Updated content", 623 } 624 625 session := createTestSession("did:plc:test123") 626 627 // Execute 628 _, err := service.UpdateComment(ctx, session, req) 629 630 // Verify 631 if !errors.Is(err, ErrCommentNotFound) { 632 t.Errorf("Expected ErrCommentNotFound, got: %v", err) 633 } 634} 635 636func TestUpdateComment_InvalidURIFormat(t *testing.T) { 637 // Setup 638 ctx := context.Background() 639 mockClient := newMockPDSClient("did:plc:test123") 640 factory := &mockPDSClientFactory{client: mockClient} 641 642 commentRepo := newMockCommentRepo() 643 userRepo := newMockUserRepo() 644 postRepo := newMockPostRepo() 645 communityRepo := newMockCommunityRepo() 646 647 service := NewCommentServiceWithPDSFactory( 648 commentRepo, 649 userRepo, 650 postRepo, 651 communityRepo, 652 nil, 653 factory.create, 654 ) 655 656 req := UpdateCommentRequest{ 657 URI: "invalid-uri", 658 Content: "Updated content", 659 } 660 661 session := createTestSession("did:plc:test123") 662 663 // Execute 664 _, err := service.UpdateComment(ctx, session, req) 665 666 // Verify 667 if !errors.Is(err, ErrCommentNotFound) { 668 t.Errorf("Expected ErrCommentNotFound, got: %v", err) 669 } 670} 671 672func TestUpdateComment_NotOwner(t *testing.T) { 673 // Setup 674 ctx := context.Background() 675 mockClient := newMockPDSClient("did:plc:test123") 676 factory := &mockPDSClientFactory{client: mockClient} 677 678 commentRepo := newMockCommentRepo() 679 userRepo := newMockUserRepo() 680 postRepo := newMockPostRepo() 681 communityRepo := newMockCommunityRepo() 682 683 service := NewCommentServiceWithPDSFactory( 684 commentRepo, 685 userRepo, 686 postRepo, 687 communityRepo, 688 nil, 689 factory.create, 690 ) 691 692 // Try to update a comment owned by a different user 693 req := UpdateCommentRequest{ 694 URI: "at://did:plc:otheruser/social.coves.community.comment/test123", 695 Content: "Updated content", 696 } 697 698 session := createTestSession("did:plc:test123") 699 700 // Execute 701 _, err := service.UpdateComment(ctx, session, req) 702 703 // Verify 704 if !errors.Is(err, ErrNotAuthorized) { 705 t.Errorf("Expected ErrNotAuthorized, got: %v", err) 706 } 707} 708 709func TestUpdateComment_EmptyContent(t *testing.T) { 710 // Setup 711 ctx := context.Background() 712 mockClient := newMockPDSClient("did:plc:test123") 713 factory := &mockPDSClientFactory{client: mockClient} 714 715 commentRepo := newMockCommentRepo() 716 userRepo := newMockUserRepo() 717 postRepo := newMockPostRepo() 718 communityRepo := newMockCommunityRepo() 719 720 service := NewCommentServiceWithPDSFactory( 721 commentRepo, 722 userRepo, 723 postRepo, 724 communityRepo, 725 nil, 726 factory.create, 727 ) 728 729 req := UpdateCommentRequest{ 730 URI: "at://did:plc:test123/social.coves.community.comment/test123", 731 Content: "", 732 } 733 734 session := createTestSession("did:plc:test123") 735 736 // Execute 737 _, err := service.UpdateComment(ctx, session, req) 738 739 // Verify 740 if !errors.Is(err, ErrContentEmpty) { 741 t.Errorf("Expected ErrContentEmpty, got: %v", err) 742 } 743} 744 745func TestUpdateComment_ContentTooLong(t *testing.T) { 746 // Setup 747 ctx := context.Background() 748 mockClient := newMockPDSClient("did:plc:test123") 749 factory := &mockPDSClientFactory{client: mockClient} 750 751 commentRepo := newMockCommentRepo() 752 userRepo := newMockUserRepo() 753 postRepo := newMockPostRepo() 754 communityRepo := newMockCommunityRepo() 755 756 service := NewCommentServiceWithPDSFactory( 757 commentRepo, 758 userRepo, 759 postRepo, 760 communityRepo, 761 nil, 762 factory.create, 763 ) 764 765 longContent := strings.Repeat("あ", 10001) 766 767 req := UpdateCommentRequest{ 768 URI: "at://did:plc:test123/social.coves.community.comment/test123", 769 Content: longContent, 770 } 771 772 session := createTestSession("did:plc:test123") 773 774 // Execute 775 _, err := service.UpdateComment(ctx, session, req) 776 777 // Verify 778 if !errors.Is(err, ErrContentTooLong) { 779 t.Errorf("Expected ErrContentTooLong, got: %v", err) 780 } 781} 782 783func TestUpdateComment_CommentNotFound(t *testing.T) { 784 // Setup 785 ctx := context.Background() 786 mockClient := newMockPDSClient("did:plc:test123") 787 mockClient.getError = pds.ErrNotFound 788 factory := &mockPDSClientFactory{client: mockClient} 789 790 commentRepo := newMockCommentRepo() 791 userRepo := newMockUserRepo() 792 postRepo := newMockPostRepo() 793 communityRepo := newMockCommunityRepo() 794 795 service := NewCommentServiceWithPDSFactory( 796 commentRepo, 797 userRepo, 798 postRepo, 799 communityRepo, 800 nil, 801 factory.create, 802 ) 803 804 req := UpdateCommentRequest{ 805 URI: "at://did:plc:test123/social.coves.community.comment/nonexistent", 806 Content: "Updated content", 807 } 808 809 session := createTestSession("did:plc:test123") 810 811 // Execute 812 _, err := service.UpdateComment(ctx, session, req) 813 814 // Verify 815 if !errors.Is(err, ErrCommentNotFound) { 816 t.Errorf("Expected ErrCommentNotFound, got: %v", err) 817 } 818} 819 820func TestUpdateComment_PreservesReplyRefs(t *testing.T) { 821 // Setup 822 ctx := context.Background() 823 mockClient := newMockPDSClient("did:plc:test123") 824 factory := &mockPDSClientFactory{client: mockClient} 825 826 commentRepo := newMockCommentRepo() 827 userRepo := newMockUserRepo() 828 postRepo := newMockPostRepo() 829 communityRepo := newMockCommunityRepo() 830 831 service := NewCommentServiceWithPDSFactory( 832 commentRepo, 833 userRepo, 834 postRepo, 835 communityRepo, 836 nil, 837 factory.create, 838 ) 839 840 // Pre-create a comment in the mock PDS 841 rkey := "testcomment123" 842 originalRootURI := "at://did:plc:author/social.coves.community.post/originalroot" 843 originalRootCID := "bafyoriginalroot" 844 existingRecord := map[string]interface{}{ 845 "$type": "social.coves.community.comment", 846 "content": "Original content", 847 "reply": map[string]interface{}{ 848 "root": map[string]interface{}{ 849 "uri": originalRootURI, 850 "cid": originalRootCID, 851 }, 852 "parent": map[string]interface{}{ 853 "uri": originalRootURI, 854 "cid": originalRootCID, 855 }, 856 }, 857 "createdAt": time.Now().Format(time.RFC3339), 858 } 859 if mockClient.records["social.coves.community.comment"] == nil { 860 mockClient.records["social.coves.community.comment"] = make(map[string]interface{}) 861 } 862 mockClient.records["social.coves.community.comment"][rkey] = existingRecord 863 864 req := UpdateCommentRequest{ 865 URI: fmt.Sprintf("at://did:plc:test123/social.coves.community.comment/%s", rkey), 866 Content: "Updated content", 867 } 868 869 session := createTestSession("did:plc:test123") 870 871 // Execute 872 resp, err := service.UpdateComment(ctx, session, req) 873 874 // Verify 875 if err != nil { 876 t.Fatalf("Expected no error, got: %v", err) 877 } 878 879 // Verify reply refs were preserved by checking the updated record 880 updatedRecordInterface := mockClient.records["social.coves.community.comment"][rkey] 881 updatedRecord, ok := updatedRecordInterface.(CommentRecord) 882 if !ok { 883 // Try as map (from pre-existing record) 884 recordMap := updatedRecordInterface.(map[string]interface{}) 885 reply := recordMap["reply"].(map[string]interface{}) 886 root := reply["root"].(map[string]interface{}) 887 888 if root["uri"] != originalRootURI { 889 t.Errorf("Expected root URI to be preserved as %s, got %s", originalRootURI, root["uri"]) 890 } 891 if root["cid"] != originalRootCID { 892 t.Errorf("Expected root CID to be preserved as %s, got %s", originalRootCID, root["cid"]) 893 } 894 895 // Verify content was updated 896 if recordMap["content"] != "Updated content" { 897 t.Errorf("Expected content to be updated to 'Updated content', got %s", recordMap["content"]) 898 } 899 } else { 900 // CommentRecord struct 901 if updatedRecord.Reply.Root.URI != originalRootURI { 902 t.Errorf("Expected root URI to be preserved as %s, got %s", originalRootURI, updatedRecord.Reply.Root.URI) 903 } 904 if updatedRecord.Reply.Root.CID != originalRootCID { 905 t.Errorf("Expected root CID to be preserved as %s, got %s", originalRootCID, updatedRecord.Reply.Root.CID) 906 } 907 908 // Verify content was updated 909 if updatedRecord.Content != "Updated content" { 910 t.Errorf("Expected content to be updated to 'Updated content', got %s", updatedRecord.Content) 911 } 912 } 913 914 // Verify response 915 if resp == nil { 916 t.Fatal("Expected response, got nil") 917 } 918} 919 920// ================================================================================ 921// DeleteComment Tests 922// ================================================================================ 923 924func TestDeleteComment_Success(t *testing.T) { 925 // Setup 926 ctx := context.Background() 927 mockClient := newMockPDSClient("did:plc:test123") 928 factory := &mockPDSClientFactory{client: mockClient} 929 930 commentRepo := newMockCommentRepo() 931 userRepo := newMockUserRepo() 932 postRepo := newMockPostRepo() 933 communityRepo := newMockCommunityRepo() 934 935 service := NewCommentServiceWithPDSFactory( 936 commentRepo, 937 userRepo, 938 postRepo, 939 communityRepo, 940 nil, 941 factory.create, 942 ) 943 944 // Pre-create a comment in the mock PDS 945 rkey := "testcomment123" 946 existingRecord := map[string]interface{}{ 947 "$type": "social.coves.community.comment", 948 "content": "Test content", 949 } 950 if mockClient.records["social.coves.community.comment"] == nil { 951 mockClient.records["social.coves.community.comment"] = make(map[string]interface{}) 952 } 953 mockClient.records["social.coves.community.comment"][rkey] = existingRecord 954 955 req := DeleteCommentRequest{ 956 URI: fmt.Sprintf("at://did:plc:test123/social.coves.community.comment/%s", rkey), 957 } 958 959 session := createTestSession("did:plc:test123") 960 961 // Execute 962 err := service.DeleteComment(ctx, session, req) 963 964 // Verify 965 if err != nil { 966 t.Fatalf("Expected no error, got: %v", err) 967 } 968 969 // Verify comment was deleted from mock PDS 970 _, exists := mockClient.records["social.coves.community.comment"][rkey] 971 if exists { 972 t.Error("Expected comment to be deleted from PDS") 973 } 974} 975 976func TestDeleteComment_EmptyURI(t *testing.T) { 977 // Setup 978 ctx := context.Background() 979 mockClient := newMockPDSClient("did:plc:test123") 980 factory := &mockPDSClientFactory{client: mockClient} 981 982 commentRepo := newMockCommentRepo() 983 userRepo := newMockUserRepo() 984 postRepo := newMockPostRepo() 985 communityRepo := newMockCommunityRepo() 986 987 service := NewCommentServiceWithPDSFactory( 988 commentRepo, 989 userRepo, 990 postRepo, 991 communityRepo, 992 nil, 993 factory.create, 994 ) 995 996 req := DeleteCommentRequest{ 997 URI: "", 998 } 999 1000 session := createTestSession("did:plc:test123") 1001 1002 // Execute 1003 err := service.DeleteComment(ctx, session, req) 1004 1005 // Verify 1006 if !errors.Is(err, ErrCommentNotFound) { 1007 t.Errorf("Expected ErrCommentNotFound, got: %v", err) 1008 } 1009} 1010 1011func TestDeleteComment_InvalidURIFormat(t *testing.T) { 1012 // Setup 1013 ctx := context.Background() 1014 mockClient := newMockPDSClient("did:plc:test123") 1015 factory := &mockPDSClientFactory{client: mockClient} 1016 1017 commentRepo := newMockCommentRepo() 1018 userRepo := newMockUserRepo() 1019 postRepo := newMockPostRepo() 1020 communityRepo := newMockCommunityRepo() 1021 1022 service := NewCommentServiceWithPDSFactory( 1023 commentRepo, 1024 userRepo, 1025 postRepo, 1026 communityRepo, 1027 nil, 1028 factory.create, 1029 ) 1030 1031 req := DeleteCommentRequest{ 1032 URI: "invalid-uri", 1033 } 1034 1035 session := createTestSession("did:plc:test123") 1036 1037 // Execute 1038 err := service.DeleteComment(ctx, session, req) 1039 1040 // Verify 1041 if !errors.Is(err, ErrCommentNotFound) { 1042 t.Errorf("Expected ErrCommentNotFound, got: %v", err) 1043 } 1044} 1045 1046func TestDeleteComment_NotOwner(t *testing.T) { 1047 // Setup 1048 ctx := context.Background() 1049 mockClient := newMockPDSClient("did:plc:test123") 1050 factory := &mockPDSClientFactory{client: mockClient} 1051 1052 commentRepo := newMockCommentRepo() 1053 userRepo := newMockUserRepo() 1054 postRepo := newMockPostRepo() 1055 communityRepo := newMockCommunityRepo() 1056 1057 service := NewCommentServiceWithPDSFactory( 1058 commentRepo, 1059 userRepo, 1060 postRepo, 1061 communityRepo, 1062 nil, 1063 factory.create, 1064 ) 1065 1066 // Try to delete a comment owned by a different user 1067 req := DeleteCommentRequest{ 1068 URI: "at://did:plc:otheruser/social.coves.community.comment/test123", 1069 } 1070 1071 session := createTestSession("did:plc:test123") 1072 1073 // Execute 1074 err := service.DeleteComment(ctx, session, req) 1075 1076 // Verify 1077 if !errors.Is(err, ErrNotAuthorized) { 1078 t.Errorf("Expected ErrNotAuthorized, got: %v", err) 1079 } 1080} 1081 1082func TestDeleteComment_CommentNotFound(t *testing.T) { 1083 // Setup 1084 ctx := context.Background() 1085 mockClient := newMockPDSClient("did:plc:test123") 1086 mockClient.getError = pds.ErrNotFound 1087 factory := &mockPDSClientFactory{client: mockClient} 1088 1089 commentRepo := newMockCommentRepo() 1090 userRepo := newMockUserRepo() 1091 postRepo := newMockPostRepo() 1092 communityRepo := newMockCommunityRepo() 1093 1094 service := NewCommentServiceWithPDSFactory( 1095 commentRepo, 1096 userRepo, 1097 postRepo, 1098 communityRepo, 1099 nil, 1100 factory.create, 1101 ) 1102 1103 req := DeleteCommentRequest{ 1104 URI: "at://did:plc:test123/social.coves.community.comment/nonexistent", 1105 } 1106 1107 session := createTestSession("did:plc:test123") 1108 1109 // Execute 1110 err := service.DeleteComment(ctx, session, req) 1111 1112 // Verify 1113 if !errors.Is(err, ErrCommentNotFound) { 1114 t.Errorf("Expected ErrCommentNotFound, got: %v", err) 1115 } 1116} 1117 1118// TestCreateComment_GraphemeCounting tests that we count graphemes correctly, not runes 1119// Flag emoji 🇺🇸 is 2 runes but 1 grapheme 1120// Emoji with skin tone 👋🏽 is 2 runes but 1 grapheme 1121func TestCreateComment_GraphemeCounting(t *testing.T) { 1122 ctx := context.Background() 1123 mockClient := newMockPDSClient("did:plc:test123") 1124 factory := &mockPDSClientFactory{client: mockClient} 1125 1126 commentRepo := newMockCommentRepo() 1127 userRepo := newMockUserRepo() 1128 postRepo := newMockPostRepo() 1129 communityRepo := newMockCommunityRepo() 1130 1131 service := NewCommentServiceWithPDSFactory( 1132 commentRepo, 1133 userRepo, 1134 postRepo, 1135 communityRepo, 1136 nil, 1137 factory.create, 1138 ) 1139 1140 // Flag emoji 🇺🇸 is 2 runes but 1 grapheme 1141 // 10000 flag emojis = 10000 graphemes but 20000 runes 1142 // This should succeed because we count graphemes 1143 content := strings.Repeat("🇺🇸", 10000) 1144 1145 req := CreateCommentRequest{ 1146 Reply: ReplyRef{ 1147 Root: StrongRef{ 1148 URI: "at://did:plc:author/social.coves.community.post/root123", 1149 CID: "bafyroot", 1150 }, 1151 Parent: StrongRef{ 1152 URI: "at://did:plc:author/social.coves.community.post/root123", 1153 CID: "bafyroot", 1154 }, 1155 }, 1156 Content: content, 1157 } 1158 1159 session := createTestSession("did:plc:test123") 1160 1161 // Should succeed - 10000 graphemes is exactly at the limit 1162 _, err := service.CreateComment(ctx, session, req) 1163 if err != nil { 1164 t.Errorf("Expected success for 10000 graphemes, got error: %v", err) 1165 } 1166 1167 // Now test that 10001 graphemes fails 1168 contentTooLong := strings.Repeat("🇺🇸", 10001) 1169 reqTooLong := CreateCommentRequest{ 1170 Reply: ReplyRef{ 1171 Root: StrongRef{ 1172 URI: "at://did:plc:author/social.coves.community.post/root123", 1173 CID: "bafyroot", 1174 }, 1175 Parent: StrongRef{ 1176 URI: "at://did:plc:author/social.coves.community.post/root123", 1177 CID: "bafyroot", 1178 }, 1179 }, 1180 Content: contentTooLong, 1181 } 1182 1183 _, err = service.CreateComment(ctx, session, reqTooLong) 1184 if !errors.Is(err, ErrContentTooLong) { 1185 t.Errorf("Expected ErrContentTooLong for 10001 graphemes, got: %v", err) 1186 } 1187 1188 // Also test emoji with skin tone modifier: 👋🏽 is 2 runes but 1 grapheme 1189 contentWithSkinTone := strings.Repeat("👋🏽", 10000) 1190 reqWithSkinTone := CreateCommentRequest{ 1191 Reply: ReplyRef{ 1192 Root: StrongRef{ 1193 URI: "at://did:plc:author/social.coves.community.post/root123", 1194 CID: "bafyroot", 1195 }, 1196 Parent: StrongRef{ 1197 URI: "at://did:plc:author/social.coves.community.post/root123", 1198 CID: "bafyroot", 1199 }, 1200 }, 1201 Content: contentWithSkinTone, 1202 } 1203 1204 _, err = service.CreateComment(ctx, session, reqWithSkinTone) 1205 if err != nil { 1206 t.Errorf("Expected success for 10000 graphemes with skin tone modifier, got error: %v", err) 1207 } 1208}