A community based topic aggregation platform built on atproto
at main 515 lines 16 kB view raw
1package adminreport 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/api/xrpc" 6 "Coves/internal/core/adminreports" 7 "bytes" 8 "context" 9 "encoding/json" 10 "errors" 11 "net/http" 12 "net/http/httptest" 13 "strings" 14 "testing" 15) 16 17// mockService implements adminreports.Service for testing 18type mockService struct { 19 submitReportFunc func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) 20} 21 22func (m *mockService) SubmitReport(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 23 if m.submitReportFunc != nil { 24 return m.submitReportFunc(ctx, req) 25 } 26 return &adminreports.SubmitReportResult{ReportID: 1}, nil 27} 28 29func TestHandleSubmit_Success(t *testing.T) { 30 svc := &mockService{ 31 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 32 if req.ReporterDID != "did:plc:testuser123" { 33 t.Errorf("expected ReporterDID %q, got %q", "did:plc:testuser123", req.ReporterDID) 34 } 35 if req.TargetURI != "at://did:plc:author123/social.coves.post/abc123" { 36 t.Errorf("expected TargetURI %q, got %q", "at://did:plc:author123/social.coves.post/abc123", req.TargetURI) 37 } 38 if req.Reason != "spam" { 39 t.Errorf("expected Reason %q, got %q", "spam", req.Reason) 40 } 41 if req.Explanation != "This is spam" { 42 t.Errorf("expected Explanation %q, got %q", "This is spam", req.Explanation) 43 } 44 return &adminreports.SubmitReportResult{ReportID: 42}, nil 45 }, 46 } 47 handler := NewSubmitHandler(svc) 48 49 input := SubmitReportInput{ 50 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 51 Reason: "spam", 52 Explanation: "This is spam", 53 } 54 body, _ := json.Marshal(input) 55 56 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 57 req.Header.Set("Content-Type", "application/json") 58 req = setTestUserDID(req, "did:plc:testuser123") 59 60 w := httptest.NewRecorder() 61 handler.HandleSubmit(w, req) 62 63 if w.Code != http.StatusOK { 64 t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) 65 } 66 67 var output SubmitReportOutput 68 if err := json.Unmarshal(w.Body.Bytes(), &output); err != nil { 69 t.Fatalf("failed to unmarshal response: %v", err) 70 } 71 72 if !output.Success { 73 t.Error("expected Success to be true") 74 } 75 if output.ReportID != 42 { 76 t.Errorf("expected ReportID 42, got %d", output.ReportID) 77 } 78} 79 80func TestHandleSubmit_MethodNotAllowed(t *testing.T) { 81 handler := NewSubmitHandler(&mockService{}) 82 83 methods := []string{http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodPatch} 84 for _, method := range methods { 85 t.Run(method, func(t *testing.T) { 86 req := httptest.NewRequest(method, "/xrpc/social.coves.admin.submitReport", nil) 87 req = setTestUserDID(req, "did:plc:testuser123") 88 89 w := httptest.NewRecorder() 90 handler.HandleSubmit(w, req) 91 92 if w.Code != http.StatusMethodNotAllowed { 93 t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, w.Code) 94 } 95 }) 96 } 97} 98 99func TestHandleSubmit_Unauthenticated(t *testing.T) { 100 handler := NewSubmitHandler(&mockService{}) 101 102 input := SubmitReportInput{ 103 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 104 Reason: "spam", 105 Explanation: "This is spam", 106 } 107 body, _ := json.Marshal(input) 108 109 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 110 req.Header.Set("Content-Type", "application/json") 111 // No auth context - simulates unauthenticated request 112 113 w := httptest.NewRecorder() 114 handler.HandleSubmit(w, req) 115 116 if w.Code != http.StatusUnauthorized { 117 t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code) 118 } 119 120 if !strings.Contains(w.Body.String(), "AuthRequired") { 121 t.Errorf("expected AuthRequired error, got %s", w.Body.String()) 122 } 123} 124 125func TestHandleSubmit_InvalidJSON(t *testing.T) { 126 handler := NewSubmitHandler(&mockService{}) 127 128 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", strings.NewReader("not valid json")) 129 req.Header.Set("Content-Type", "application/json") 130 req = setTestUserDID(req, "did:plc:testuser123") 131 132 w := httptest.NewRecorder() 133 handler.HandleSubmit(w, req) 134 135 if w.Code != http.StatusBadRequest { 136 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 137 } 138 139 if !strings.Contains(w.Body.String(), "InvalidRequest") { 140 t.Errorf("expected InvalidRequest error, got %s", w.Body.String()) 141 } 142} 143 144func TestHandleSubmit_InvalidReason(t *testing.T) { 145 svc := &mockService{ 146 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 147 return nil, adminreports.ErrInvalidReason 148 }, 149 } 150 handler := NewSubmitHandler(svc) 151 152 input := SubmitReportInput{ 153 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 154 Reason: "invalid_reason", 155 } 156 body, _ := json.Marshal(input) 157 158 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 159 req.Header.Set("Content-Type", "application/json") 160 req = setTestUserDID(req, "did:plc:testuser123") 161 162 w := httptest.NewRecorder() 163 handler.HandleSubmit(w, req) 164 165 if w.Code != http.StatusBadRequest { 166 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 167 } 168 169 if !strings.Contains(w.Body.String(), "InvalidReason") { 170 t.Errorf("expected InvalidReason error, got %s", w.Body.String()) 171 } 172} 173 174func TestHandleSubmit_InvalidTarget(t *testing.T) { 175 svc := &mockService{ 176 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 177 return nil, adminreports.ErrInvalidTarget 178 }, 179 } 180 handler := NewSubmitHandler(svc) 181 182 input := SubmitReportInput{ 183 TargetURI: "https://example.com", 184 Reason: "spam", 185 } 186 body, _ := json.Marshal(input) 187 188 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 189 req.Header.Set("Content-Type", "application/json") 190 req = setTestUserDID(req, "did:plc:testuser123") 191 192 w := httptest.NewRecorder() 193 handler.HandleSubmit(w, req) 194 195 if w.Code != http.StatusBadRequest { 196 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 197 } 198 199 if !strings.Contains(w.Body.String(), "InvalidTarget") { 200 t.Errorf("expected InvalidTarget error, got %s", w.Body.String()) 201 } 202} 203 204func TestHandleSubmit_ExplanationTooLong(t *testing.T) { 205 svc := &mockService{ 206 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 207 return nil, adminreports.ErrExplanationTooLong 208 }, 209 } 210 handler := NewSubmitHandler(svc) 211 212 input := SubmitReportInput{ 213 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 214 Reason: "spam", 215 Explanation: strings.Repeat("a", 1001), 216 } 217 body, _ := json.Marshal(input) 218 219 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 220 req.Header.Set("Content-Type", "application/json") 221 req = setTestUserDID(req, "did:plc:testuser123") 222 223 w := httptest.NewRecorder() 224 handler.HandleSubmit(w, req) 225 226 if w.Code != http.StatusBadRequest { 227 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 228 } 229 230 if !strings.Contains(w.Body.String(), "ExplanationTooLong") { 231 t.Errorf("expected ExplanationTooLong error, got %s", w.Body.String()) 232 } 233} 234 235func TestHandleSubmit_InvalidStatus(t *testing.T) { 236 svc := &mockService{ 237 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 238 return nil, adminreports.ErrInvalidStatus 239 }, 240 } 241 handler := NewSubmitHandler(svc) 242 243 input := SubmitReportInput{ 244 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 245 Reason: "spam", 246 } 247 body, _ := json.Marshal(input) 248 249 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 250 req.Header.Set("Content-Type", "application/json") 251 req = setTestUserDID(req, "did:plc:testuser123") 252 253 w := httptest.NewRecorder() 254 handler.HandleSubmit(w, req) 255 256 if w.Code != http.StatusBadRequest { 257 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 258 } 259 260 if !strings.Contains(w.Body.String(), "InvalidStatus") { 261 t.Errorf("expected InvalidStatus error, got %s", w.Body.String()) 262 } 263} 264 265func TestHandleSubmit_InvalidTargetType(t *testing.T) { 266 svc := &mockService{ 267 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 268 return nil, adminreports.ErrInvalidTargetType 269 }, 270 } 271 handler := NewSubmitHandler(svc) 272 273 input := SubmitReportInput{ 274 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 275 Reason: "spam", 276 } 277 body, _ := json.Marshal(input) 278 279 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 280 req.Header.Set("Content-Type", "application/json") 281 req = setTestUserDID(req, "did:plc:testuser123") 282 283 w := httptest.NewRecorder() 284 handler.HandleSubmit(w, req) 285 286 if w.Code != http.StatusBadRequest { 287 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 288 } 289 290 if !strings.Contains(w.Body.String(), "InvalidTargetType") { 291 t.Errorf("expected InvalidTargetType error, got %s", w.Body.String()) 292 } 293} 294 295func TestHandleSubmit_NotFound(t *testing.T) { 296 svc := &mockService{ 297 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 298 return nil, adminreports.ErrReportNotFound 299 }, 300 } 301 handler := NewSubmitHandler(svc) 302 303 input := SubmitReportInput{ 304 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 305 Reason: "spam", 306 } 307 body, _ := json.Marshal(input) 308 309 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 310 req.Header.Set("Content-Type", "application/json") 311 req = setTestUserDID(req, "did:plc:testuser123") 312 313 w := httptest.NewRecorder() 314 handler.HandleSubmit(w, req) 315 316 if w.Code != http.StatusNotFound { 317 t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code) 318 } 319 320 if !strings.Contains(w.Body.String(), "NotFound") { 321 t.Errorf("expected NotFound error, got %s", w.Body.String()) 322 } 323} 324 325func TestHandleSubmit_InternalError(t *testing.T) { 326 svc := &mockService{ 327 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 328 return nil, errors.New("database connection failed") 329 }, 330 } 331 handler := NewSubmitHandler(svc) 332 333 input := SubmitReportInput{ 334 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 335 Reason: "spam", 336 } 337 body, _ := json.Marshal(input) 338 339 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 340 req.Header.Set("Content-Type", "application/json") 341 req = setTestUserDID(req, "did:plc:testuser123") 342 343 w := httptest.NewRecorder() 344 handler.HandleSubmit(w, req) 345 346 if w.Code != http.StatusInternalServerError { 347 t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code) 348 } 349 350 if !strings.Contains(w.Body.String(), "InternalServerError") { 351 t.Errorf("expected InternalServerError error, got %s", w.Body.String()) 352 } 353 354 // SECURITY: Verify that the actual error message is not leaked 355 if strings.Contains(w.Body.String(), "database") { 356 t.Error("internal error details should not be exposed to client") 357 } 358} 359 360func TestHandleSubmit_EmptyExplanation(t *testing.T) { 361 svc := &mockService{ 362 submitReportFunc: func(ctx context.Context, req adminreports.SubmitReportRequest) (*adminreports.SubmitReportResult, error) { 363 if req.Explanation != "" { 364 t.Errorf("expected empty Explanation, got %q", req.Explanation) 365 } 366 return &adminreports.SubmitReportResult{ReportID: 1}, nil 367 }, 368 } 369 handler := NewSubmitHandler(svc) 370 371 input := SubmitReportInput{ 372 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 373 Reason: "spam", 374 Explanation: "", 375 } 376 body, _ := json.Marshal(input) 377 378 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 379 req.Header.Set("Content-Type", "application/json") 380 req = setTestUserDID(req, "did:plc:testuser123") 381 382 w := httptest.NewRecorder() 383 handler.HandleSubmit(w, req) 384 385 if w.Code != http.StatusOK { 386 t.Errorf("expected status %d, got %d", http.StatusOK, w.Code) 387 } 388} 389 390func TestHandleSubmit_ContentTypeHeader(t *testing.T) { 391 handler := NewSubmitHandler(&mockService{}) 392 393 input := SubmitReportInput{ 394 TargetURI: "at://did:plc:author123/social.coves.post/abc123", 395 Reason: "spam", 396 } 397 body, _ := json.Marshal(input) 398 399 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.admin.submitReport", bytes.NewReader(body)) 400 req.Header.Set("Content-Type", "application/json") 401 req = setTestUserDID(req, "did:plc:testuser123") 402 403 w := httptest.NewRecorder() 404 handler.HandleSubmit(w, req) 405 406 contentType := w.Header().Get("Content-Type") 407 if contentType != "application/json" { 408 t.Errorf("expected Content-Type %q, got %q", "application/json", contentType) 409 } 410} 411 412func TestWriteError(t *testing.T) { 413 w := httptest.NewRecorder() 414 writeError(w, http.StatusBadRequest, "TestError", "Test message") 415 416 if w.Code != http.StatusBadRequest { 417 t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code) 418 } 419 420 var resp xrpc.Error 421 if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { 422 t.Fatalf("failed to unmarshal error response: %v", err) 423 } 424 425 if resp.Error != "TestError" { 426 t.Errorf("expected error %q, got %q", "TestError", resp.Error) 427 } 428 if resp.Message != "Test message" { 429 t.Errorf("expected message %q, got %q", "Test message", resp.Message) 430 } 431} 432 433func TestHandleServiceError_AllValidationErrors(t *testing.T) { 434 tests := []struct { 435 name string 436 err error 437 expectedStatus int 438 expectedError string 439 }{ 440 { 441 name: "ErrInvalidReason", 442 err: adminreports.ErrInvalidReason, 443 expectedStatus: http.StatusBadRequest, 444 expectedError: "InvalidReason", 445 }, 446 { 447 name: "ErrInvalidStatus", 448 err: adminreports.ErrInvalidStatus, 449 expectedStatus: http.StatusBadRequest, 450 expectedError: "InvalidStatus", 451 }, 452 { 453 name: "ErrInvalidTarget", 454 err: adminreports.ErrInvalidTarget, 455 expectedStatus: http.StatusBadRequest, 456 expectedError: "InvalidTarget", 457 }, 458 { 459 name: "ErrExplanationTooLong", 460 err: adminreports.ErrExplanationTooLong, 461 expectedStatus: http.StatusBadRequest, 462 expectedError: "ExplanationTooLong", 463 }, 464 { 465 name: "ErrReporterRequired", 466 err: adminreports.ErrReporterRequired, 467 expectedStatus: http.StatusBadRequest, 468 expectedError: "ReporterRequired", 469 }, 470 { 471 name: "ErrInvalidTargetType", 472 err: adminreports.ErrInvalidTargetType, 473 expectedStatus: http.StatusBadRequest, 474 expectedError: "InvalidTargetType", 475 }, 476 { 477 name: "ErrReportNotFound", 478 err: adminreports.ErrReportNotFound, 479 expectedStatus: http.StatusNotFound, 480 expectedError: "NotFound", 481 }, 482 { 483 name: "internal error", 484 err: errors.New("some internal error"), 485 expectedStatus: http.StatusInternalServerError, 486 expectedError: "InternalServerError", 487 }, 488 } 489 490 for _, tt := range tests { 491 t.Run(tt.name, func(t *testing.T) { 492 w := httptest.NewRecorder() 493 handleServiceError(w, tt.err) 494 495 if w.Code != tt.expectedStatus { 496 t.Errorf("expected status %d, got %d", tt.expectedStatus, w.Code) 497 } 498 499 var resp xrpc.Error 500 if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { 501 t.Fatalf("failed to unmarshal error response: %v", err) 502 } 503 504 if resp.Error != tt.expectedError { 505 t.Errorf("expected error %q, got %q", tt.expectedError, resp.Error) 506 } 507 }) 508 } 509} 510 511// setTestUserDID sets the user DID in the context for testing 512func setTestUserDID(req *http.Request, userDID string) *http.Request { 513 ctx := middleware.SetTestUserDID(req.Context(), userDID) 514 return req.WithContext(ctx) 515}