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}