A community based topic aggregation platform built on atproto
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}