A community based topic aggregation platform built on atproto
at main 728 lines 23 kB view raw
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}