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