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 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}