A community based topic aggregation platform built on atproto
1package actor
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10 "time"
11
12 "Coves/internal/core/comments"
13 "Coves/internal/core/posts"
14 "Coves/internal/core/users"
15 "Coves/internal/core/votes"
16
17 oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth"
18)
19
20// mockCommentService implements a comment service interface for testing
21type mockCommentService struct {
22 getActorCommentsFunc func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error)
23}
24
25func (m *mockCommentService) GetActorComments(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
26 if m.getActorCommentsFunc != nil {
27 return m.getActorCommentsFunc(ctx, req)
28 }
29 return &comments.GetActorCommentsResponse{
30 Comments: []*comments.CommentView{},
31 Cursor: nil,
32 }, nil
33}
34
35// Implement other Service methods as no-ops
36func (m *mockCommentService) GetComments(ctx context.Context, req *comments.GetCommentsRequest) (*comments.GetCommentsResponse, error) {
37 return nil, nil
38}
39
40func (m *mockCommentService) CreateComment(ctx context.Context, session *oauthlib.ClientSessionData, req comments.CreateCommentRequest) (*comments.CreateCommentResponse, error) {
41 return nil, nil
42}
43
44func (m *mockCommentService) UpdateComment(ctx context.Context, session *oauthlib.ClientSessionData, req comments.UpdateCommentRequest) (*comments.UpdateCommentResponse, error) {
45 return nil, nil
46}
47
48func (m *mockCommentService) DeleteComment(ctx context.Context, session *oauthlib.ClientSessionData, req comments.DeleteCommentRequest) error {
49 return nil
50}
51
52// mockUserServiceForComments implements users.UserService for testing getComments
53type mockUserServiceForComments struct {
54 resolveHandleToDIDFunc func(ctx context.Context, handle string) (string, error)
55}
56
57func (m *mockUserServiceForComments) CreateUser(ctx context.Context, req users.CreateUserRequest) (*users.User, error) {
58 return nil, nil
59}
60
61func (m *mockUserServiceForComments) GetUserByDID(ctx context.Context, did string) (*users.User, error) {
62 return nil, nil
63}
64
65func (m *mockUserServiceForComments) GetUserByHandle(ctx context.Context, handle string) (*users.User, error) {
66 return nil, nil
67}
68
69func (m *mockUserServiceForComments) UpdateHandle(ctx context.Context, did, newHandle string) (*users.User, error) {
70 return nil, nil
71}
72
73func (m *mockUserServiceForComments) ResolveHandleToDID(ctx context.Context, handle string) (string, error) {
74 if m.resolveHandleToDIDFunc != nil {
75 return m.resolveHandleToDIDFunc(ctx, handle)
76 }
77 return "did:plc:testuser", nil
78}
79
80func (m *mockUserServiceForComments) RegisterAccount(ctx context.Context, req users.RegisterAccountRequest) (*users.RegisterAccountResponse, error) {
81 return nil, nil
82}
83
84func (m *mockUserServiceForComments) IndexUser(ctx context.Context, did, handle, pdsURL string) error {
85 return nil
86}
87
88func (m *mockUserServiceForComments) GetProfile(ctx context.Context, did string) (*users.ProfileViewDetailed, error) {
89 return nil, nil
90}
91
92func (m *mockUserServiceForComments) DeleteAccount(ctx context.Context, did string) error {
93 return nil
94}
95
96func (m *mockUserServiceForComments) UpdateProfile(ctx context.Context, did string, input users.UpdateProfileInput) (*users.User, error) {
97 return nil, nil
98}
99
100// mockVoteServiceForComments implements votes.Service for testing getComments
101type mockVoteServiceForComments struct{}
102
103func (m *mockVoteServiceForComments) CreateVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) {
104 return nil, nil
105}
106
107func (m *mockVoteServiceForComments) DeleteVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error {
108 return nil
109}
110
111func (m *mockVoteServiceForComments) EnsureCachePopulated(ctx context.Context, session *oauthlib.ClientSessionData) error {
112 return nil
113}
114
115func (m *mockVoteServiceForComments) GetViewerVote(userDID, subjectURI string) *votes.CachedVote {
116 return nil
117}
118
119func (m *mockVoteServiceForComments) GetViewerVotesForSubjects(userDID string, subjectURIs []string) map[string]*votes.CachedVote {
120 return nil
121}
122
123func TestGetCommentsHandler_Success(t *testing.T) {
124 createdAt := time.Now().Format(time.RFC3339)
125 indexedAt := time.Now().Format(time.RFC3339)
126
127 mockComments := &mockCommentService{
128 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
129 return &comments.GetActorCommentsResponse{
130 Comments: []*comments.CommentView{
131 {
132 URI: "at://did:plc:testuser/social.coves.community.comment/abc123",
133 CID: "bafytest123",
134 Record: &comments.CommentRecord{
135 Type: "social.coves.community.comment",
136 Content: "Test comment content",
137 CreatedAt: createdAt,
138 },
139 CreatedAt: createdAt,
140 IndexedAt: indexedAt,
141 Author: &posts.AuthorView{
142 DID: "did:plc:testuser",
143 Handle: "test.user",
144 },
145 Stats: &comments.CommentStats{
146 Upvotes: 5,
147 Downvotes: 1,
148 Score: 4,
149 ReplyCount: 2,
150 },
151 },
152 },
153 }, nil
154 },
155 }
156 mockUsers := &mockUserServiceForComments{}
157 mockVotes := &mockVoteServiceForComments{}
158
159 handler := NewGetCommentsHandler(mockComments, mockUsers, mockVotes)
160
161 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:testuser", nil)
162 rec := httptest.NewRecorder()
163
164 handler.HandleGetComments(rec, req)
165
166 if rec.Code != http.StatusOK {
167 t.Errorf("Expected status 200, got %d", rec.Code)
168 }
169
170 var response comments.GetActorCommentsResponse
171 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
172 t.Fatalf("Failed to decode response: %v", err)
173 }
174
175 if len(response.Comments) != 1 {
176 t.Errorf("Expected 1 comment in response, got %d", len(response.Comments))
177 }
178
179 if response.Comments[0].URI != "at://did:plc:testuser/social.coves.community.comment/abc123" {
180 t.Errorf("Expected correct comment URI, got '%s'", response.Comments[0].URI)
181 }
182
183 // After JSON marshal/unmarshal, Record becomes map[string]interface{} instead
184 // of the original *CommentRecord type because json.Unmarshal doesn't preserve
185 // Go struct types for interface{} fields.
186 if response.Comments[0].Record == nil {
187 t.Fatal("Expected Record to be non-nil after JSON round-trip")
188 }
189 record, ok := response.Comments[0].Record.(map[string]interface{})
190 if !ok {
191 t.Fatalf("Expected Record to be map[string]interface{}, got %T", response.Comments[0].Record)
192 }
193 if record["content"] != "Test comment content" {
194 t.Errorf("Expected correct comment content, got '%s'", record["content"])
195 }
196}
197
198func TestGetCommentsHandler_MissingActor(t *testing.T) {
199 handler := NewGetCommentsHandler(
200 &mockCommentService{},
201 &mockUserServiceForComments{},
202 &mockVoteServiceForComments{},
203 )
204
205 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments", nil)
206 rec := httptest.NewRecorder()
207
208 handler.HandleGetComments(rec, req)
209
210 if rec.Code != http.StatusBadRequest {
211 t.Errorf("Expected status 400, got %d", rec.Code)
212 }
213
214 var response ErrorResponse
215 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
216 t.Fatalf("Failed to decode response: %v", err)
217 }
218
219 if response.Error != "InvalidRequest" {
220 t.Errorf("Expected error 'InvalidRequest', got '%s'", response.Error)
221 }
222}
223
224func TestGetCommentsHandler_InvalidLimit(t *testing.T) {
225 handler := NewGetCommentsHandler(
226 &mockCommentService{},
227 &mockUserServiceForComments{},
228 &mockVoteServiceForComments{},
229 )
230
231 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:test&limit=abc", nil)
232 rec := httptest.NewRecorder()
233
234 handler.HandleGetComments(rec, req)
235
236 if rec.Code != http.StatusBadRequest {
237 t.Errorf("Expected status 400, got %d", rec.Code)
238 }
239
240 var response ErrorResponse
241 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
242 t.Fatalf("Failed to decode response: %v", err)
243 }
244
245 if response.Error != "InvalidRequest" {
246 t.Errorf("Expected error 'InvalidRequest', got '%s'", response.Error)
247 }
248}
249
250func TestGetCommentsHandler_ActorNotFound(t *testing.T) {
251 mockUsers := &mockUserServiceForComments{
252 resolveHandleToDIDFunc: func(ctx context.Context, handle string) (string, error) {
253 return "", posts.ErrActorNotFound
254 },
255 }
256
257 handler := NewGetCommentsHandler(
258 &mockCommentService{},
259 mockUsers,
260 &mockVoteServiceForComments{},
261 )
262
263 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=nonexistent.user", nil)
264 rec := httptest.NewRecorder()
265
266 handler.HandleGetComments(rec, req)
267
268 if rec.Code != http.StatusNotFound {
269 t.Errorf("Expected status 404, got %d", rec.Code)
270 }
271
272 var response ErrorResponse
273 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
274 t.Fatalf("Failed to decode response: %v", err)
275 }
276
277 if response.Error != "ActorNotFound" {
278 t.Errorf("Expected error 'ActorNotFound', got '%s'", response.Error)
279 }
280}
281
282func TestGetCommentsHandler_ActorLengthExceedsMax(t *testing.T) {
283 handler := NewGetCommentsHandler(
284 &mockCommentService{},
285 &mockUserServiceForComments{},
286 &mockVoteServiceForComments{},
287 )
288
289 // Create an actor parameter that exceeds 2048 characters using valid URL characters
290 longActorBytes := make([]byte, 2100)
291 for i := range longActorBytes {
292 longActorBytes[i] = 'a'
293 }
294 longActor := "did:plc:" + string(longActorBytes)
295 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor="+longActor, nil)
296 rec := httptest.NewRecorder()
297
298 handler.HandleGetComments(rec, req)
299
300 if rec.Code != http.StatusBadRequest {
301 t.Errorf("Expected status 400, got %d", rec.Code)
302 }
303}
304
305func TestGetCommentsHandler_InvalidCursor(t *testing.T) {
306 // The handleCommentServiceError function checks for "invalid request" in error message
307 // to return a BadRequest. An invalid cursor error falls under this category.
308 mockComments := &mockCommentService{
309 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
310 return nil, errors.New("invalid request: invalid cursor format")
311 },
312 }
313
314 handler := NewGetCommentsHandler(
315 mockComments,
316 &mockUserServiceForComments{},
317 &mockVoteServiceForComments{},
318 )
319
320 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:test&cursor=invalid", nil)
321 rec := httptest.NewRecorder()
322
323 handler.HandleGetComments(rec, req)
324
325 if rec.Code != http.StatusBadRequest {
326 t.Errorf("Expected status 400, got %d", rec.Code)
327 }
328
329 var response ErrorResponse
330 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
331 t.Fatalf("Failed to decode response: %v", err)
332 }
333
334 if response.Error != "InvalidRequest" {
335 t.Errorf("Expected error 'InvalidRequest', got '%s'", response.Error)
336 }
337}
338
339func TestGetCommentsHandler_MethodNotAllowed(t *testing.T) {
340 handler := NewGetCommentsHandler(
341 &mockCommentService{},
342 &mockUserServiceForComments{},
343 &mockVoteServiceForComments{},
344 )
345
346 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.getComments", nil)
347 rec := httptest.NewRecorder()
348
349 handler.HandleGetComments(rec, req)
350
351 if rec.Code != http.StatusMethodNotAllowed {
352 t.Errorf("Expected status 405, got %d", rec.Code)
353 }
354}
355
356func TestGetCommentsHandler_HandleResolution(t *testing.T) {
357 resolvedDID := ""
358 mockComments := &mockCommentService{
359 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
360 resolvedDID = req.ActorDID
361 return &comments.GetActorCommentsResponse{Comments: []*comments.CommentView{}}, nil
362 },
363 }
364 mockUsers := &mockUserServiceForComments{
365 resolveHandleToDIDFunc: func(ctx context.Context, handle string) (string, error) {
366 if handle == "test.user" {
367 return "did:plc:resolveduser123", nil
368 }
369 return "", posts.ErrActorNotFound
370 },
371 }
372
373 handler := NewGetCommentsHandler(
374 mockComments,
375 mockUsers,
376 &mockVoteServiceForComments{},
377 )
378
379 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=test.user", nil)
380 rec := httptest.NewRecorder()
381
382 handler.HandleGetComments(rec, req)
383
384 if rec.Code != http.StatusOK {
385 t.Errorf("Expected status 200, got %d", rec.Code)
386 }
387
388 if resolvedDID != "did:plc:resolveduser123" {
389 t.Errorf("Expected resolved DID 'did:plc:resolveduser123', got '%s'", resolvedDID)
390 }
391}
392
393func TestGetCommentsHandler_DIDPassThrough(t *testing.T) {
394 receivedDID := ""
395 mockComments := &mockCommentService{
396 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
397 receivedDID = req.ActorDID
398 return &comments.GetActorCommentsResponse{Comments: []*comments.CommentView{}}, nil
399 },
400 }
401
402 handler := NewGetCommentsHandler(
403 mockComments,
404 &mockUserServiceForComments{},
405 &mockVoteServiceForComments{},
406 )
407
408 // When actor is already a DID, it should pass through without resolution
409 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:directuser", nil)
410 rec := httptest.NewRecorder()
411
412 handler.HandleGetComments(rec, req)
413
414 if rec.Code != http.StatusOK {
415 t.Errorf("Expected status 200, got %d", rec.Code)
416 }
417
418 if receivedDID != "did:plc:directuser" {
419 t.Errorf("Expected DID 'did:plc:directuser', got '%s'", receivedDID)
420 }
421}
422
423func TestGetCommentsHandler_EmptyCommentsArray(t *testing.T) {
424 mockComments := &mockCommentService{
425 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
426 return &comments.GetActorCommentsResponse{
427 Comments: []*comments.CommentView{},
428 }, nil
429 },
430 }
431
432 handler := NewGetCommentsHandler(
433 mockComments,
434 &mockUserServiceForComments{},
435 &mockVoteServiceForComments{},
436 )
437
438 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:newuser", nil)
439 rec := httptest.NewRecorder()
440
441 handler.HandleGetComments(rec, req)
442
443 if rec.Code != http.StatusOK {
444 t.Errorf("Expected status 200, got %d", rec.Code)
445 }
446
447 var response comments.GetActorCommentsResponse
448 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
449 t.Fatalf("Failed to decode response: %v", err)
450 }
451
452 if response.Comments == nil {
453 t.Error("Expected comments array to be non-nil (empty array), got nil")
454 }
455
456 if len(response.Comments) != 0 {
457 t.Errorf("Expected 0 comments for new user, got %d", len(response.Comments))
458 }
459}
460
461func TestGetCommentsHandler_WithCursor(t *testing.T) {
462 receivedCursor := ""
463 mockComments := &mockCommentService{
464 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
465 if req.Cursor != nil {
466 receivedCursor = *req.Cursor
467 }
468 nextCursor := "page2cursor"
469 return &comments.GetActorCommentsResponse{
470 Comments: []*comments.CommentView{},
471 Cursor: &nextCursor,
472 }, nil
473 },
474 }
475
476 handler := NewGetCommentsHandler(
477 mockComments,
478 &mockUserServiceForComments{},
479 &mockVoteServiceForComments{},
480 )
481
482 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:test&cursor=testcursor123", nil)
483 rec := httptest.NewRecorder()
484
485 handler.HandleGetComments(rec, req)
486
487 if rec.Code != http.StatusOK {
488 t.Errorf("Expected status 200, got %d", rec.Code)
489 }
490
491 if receivedCursor != "testcursor123" {
492 t.Errorf("Expected cursor 'testcursor123', got '%s'", receivedCursor)
493 }
494
495 var response comments.GetActorCommentsResponse
496 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
497 t.Fatalf("Failed to decode response: %v", err)
498 }
499
500 if response.Cursor == nil || *response.Cursor != "page2cursor" {
501 t.Error("Expected response to include next cursor")
502 }
503}
504
505func TestGetCommentsHandler_WithLimit(t *testing.T) {
506 receivedLimit := 0
507 mockComments := &mockCommentService{
508 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
509 receivedLimit = req.Limit
510 return &comments.GetActorCommentsResponse{
511 Comments: []*comments.CommentView{},
512 }, nil
513 },
514 }
515
516 handler := NewGetCommentsHandler(
517 mockComments,
518 &mockUserServiceForComments{},
519 &mockVoteServiceForComments{},
520 )
521
522 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:test&limit=25", nil)
523 rec := httptest.NewRecorder()
524
525 handler.HandleGetComments(rec, req)
526
527 if rec.Code != http.StatusOK {
528 t.Errorf("Expected status 200, got %d", rec.Code)
529 }
530
531 if receivedLimit != 25 {
532 t.Errorf("Expected limit 25, got %d", receivedLimit)
533 }
534}
535
536func TestGetCommentsHandler_WithCommunityFilter(t *testing.T) {
537 receivedCommunity := ""
538 mockComments := &mockCommentService{
539 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
540 receivedCommunity = req.Community
541 return &comments.GetActorCommentsResponse{
542 Comments: []*comments.CommentView{},
543 }, nil
544 },
545 }
546
547 handler := NewGetCommentsHandler(
548 mockComments,
549 &mockUserServiceForComments{},
550 &mockVoteServiceForComments{},
551 )
552
553 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:test&community=did:plc:community123", nil)
554 rec := httptest.NewRecorder()
555
556 handler.HandleGetComments(rec, req)
557
558 if rec.Code != http.StatusOK {
559 t.Errorf("Expected status 200, got %d", rec.Code)
560 }
561
562 if receivedCommunity != "did:plc:community123" {
563 t.Errorf("Expected community 'did:plc:community123', got '%s'", receivedCommunity)
564 }
565}
566
567func TestGetCommentsHandler_ServiceError_Returns500(t *testing.T) {
568 // Test that generic service errors (database failures, etc.) return 500
569 mockComments := &mockCommentService{
570 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
571 return nil, errors.New("database connection failed")
572 },
573 }
574
575 handler := NewGetCommentsHandler(
576 mockComments,
577 &mockUserServiceForComments{},
578 &mockVoteServiceForComments{},
579 )
580
581 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:test", nil)
582 rec := httptest.NewRecorder()
583
584 handler.HandleGetComments(rec, req)
585
586 if rec.Code != http.StatusInternalServerError {
587 t.Errorf("Expected status 500, got %d", rec.Code)
588 }
589
590 var response ErrorResponse
591 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
592 t.Fatalf("Failed to decode response: %v", err)
593 }
594
595 if response.Error != "InternalServerError" {
596 t.Errorf("Expected error 'InternalServerError', got '%s'", response.Error)
597 }
598
599 // Verify error message doesn't leak internal details
600 if response.Message == "database connection failed" {
601 t.Error("Error message should not leak internal error details")
602 }
603}
604
605func TestGetCommentsHandler_ResolutionFailedError_Returns500(t *testing.T) {
606 // Test that infrastructure failures during handle resolution return 500, not 400
607 mockUsers := &mockUserServiceForComments{
608 resolveHandleToDIDFunc: func(ctx context.Context, handle string) (string, error) {
609 // Simulate a database failure during resolution
610 return "", errors.New("connection refused")
611 },
612 }
613
614 handler := NewGetCommentsHandler(
615 &mockCommentService{},
616 mockUsers,
617 &mockVoteServiceForComments{},
618 )
619
620 // Use a handle (not a DID) to trigger resolution
621 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=test.user", nil)
622 rec := httptest.NewRecorder()
623
624 handler.HandleGetComments(rec, req)
625
626 // Infrastructure failures should return 500, not 400 or 404
627 if rec.Code != http.StatusInternalServerError {
628 t.Errorf("Expected status 500 for infrastructure failure, got %d", rec.Code)
629 }
630
631 var response ErrorResponse
632 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
633 t.Fatalf("Failed to decode response: %v", err)
634 }
635
636 if response.Error != "InternalServerError" {
637 t.Errorf("Expected error 'InternalServerError', got '%s'", response.Error)
638 }
639}
640
641func TestGetCommentsHandler_DeletedComment_NilRecord(t *testing.T) {
642 // Test that deleted comments are properly serialized with nil Record at the API layer.
643 // This verifies the JSON response correctly handles deleted comments where content
644 // has been removed but the comment shell remains for thread continuity.
645 createdAt := time.Now().Format(time.RFC3339)
646 indexedAt := time.Now().Format(time.RFC3339)
647 deletedAt := time.Now().Format(time.RFC3339)
648 deletionReason := "User deleted"
649
650 mockComments := &mockCommentService{
651 getActorCommentsFunc: func(ctx context.Context, req *comments.GetActorCommentsRequest) (*comments.GetActorCommentsResponse, error) {
652 return &comments.GetActorCommentsResponse{
653 Comments: []*comments.CommentView{
654 {
655 URI: "at://did:plc:testuser/social.coves.community.comment/deleted123",
656 CID: "bafydeleted",
657 Record: nil, // Deleted comments have nil Record
658 IsDeleted: true,
659 DeletedAt: &deletedAt,
660 DeletionReason: &deletionReason,
661 CreatedAt: createdAt,
662 IndexedAt: indexedAt,
663 Author: &posts.AuthorView{
664 DID: "did:plc:testuser",
665 Handle: "test.user",
666 },
667 Post: &comments.CommentRef{
668 URI: "at://did:plc:community/social.coves.community.post/parent123",
669 CID: "bafyparent",
670 },
671 Stats: &comments.CommentStats{
672 Upvotes: 0,
673 Downvotes: 0,
674 Score: 0,
675 ReplyCount: 0,
676 },
677 },
678 },
679 }, nil
680 },
681 }
682 mockUsers := &mockUserServiceForComments{}
683 mockVotes := &mockVoteServiceForComments{}
684
685 handler := NewGetCommentsHandler(mockComments, mockUsers, mockVotes)
686
687 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getComments?actor=did:plc:testuser", nil)
688 rec := httptest.NewRecorder()
689
690 handler.HandleGetComments(rec, req)
691
692 if rec.Code != http.StatusOK {
693 t.Errorf("Expected status 200, got %d", rec.Code)
694 }
695
696 var response comments.GetActorCommentsResponse
697 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
698 t.Fatalf("Failed to decode response: %v", err)
699 }
700
701 if len(response.Comments) != 1 {
702 t.Fatalf("Expected 1 comment, got %d", len(response.Comments))
703 }
704
705 deletedComment := response.Comments[0]
706
707 // Verify deleted comment fields
708 if !deletedComment.IsDeleted {
709 t.Error("Expected IsDeleted to be true for deleted comment")
710 }
711
712 if deletedComment.Record != nil {
713 t.Errorf("Expected Record to be nil for deleted comment, got %T", deletedComment.Record)
714 }
715
716 if deletedComment.DeletedAt == nil || *deletedComment.DeletedAt != deletedAt {
717 t.Errorf("Expected DeletedAt to be %s, got %v", deletedAt, deletedComment.DeletedAt)
718 }
719
720 if deletedComment.DeletionReason == nil || *deletedComment.DeletionReason != deletionReason {
721 t.Errorf("Expected DeletionReason to be %s, got %v", deletionReason, deletedComment.DeletionReason)
722 }
723
724 // Verify author info is still present (for attribution even on deleted comments)
725 if deletedComment.Author == nil || deletedComment.Author.DID != "did:plc:testuser" {
726 t.Error("Expected deleted comment to retain author information")
727 }
728}