A community based topic aggregation platform built on atproto
at main 11 kB view raw
1package vote 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/core/votes" 6 "bytes" 7 "context" 8 "encoding/json" 9 "net/http" 10 "net/http/httptest" 11 "testing" 12 13 oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15) 16 17func TestDeleteVoteHandler_Success(t *testing.T) { 18 mockService := &mockVoteService{} 19 handler := NewDeleteVoteHandler(mockService) 20 21 // Create request body 22 reqBody := DeleteVoteInput{ 23 Subject: struct { 24 URI string `json:"uri"` 25 CID string `json:"cid"` 26 }{ 27 URI: "at://did:plc:author123/social.coves.post/xyz789", 28 CID: "bafypost123", 29 }, 30 } 31 bodyBytes, err := json.Marshal(reqBody) 32 if err != nil { 33 t.Fatalf("Failed to marshal request: %v", err) 34 } 35 36 // Create HTTP request 37 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes)) 38 req.Header.Set("Content-Type", "application/json") 39 40 // Inject OAuth session into context (simulates auth middleware) 41 did, _ := syntax.ParseDID("did:plc:test123") 42 session := &oauthlib.ClientSessionData{ 43 AccountDID: did, 44 AccessToken: "test_token", 45 } 46 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 47 req = req.WithContext(ctx) 48 49 // Execute handler 50 w := httptest.NewRecorder() 51 handler.HandleDeleteVote(w, req) 52 53 // Check status code 54 if w.Code != http.StatusOK { 55 t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) 56 } 57 58 // Check response is empty object per lexicon 59 var response map[string]interface{} 60 if err := json.NewDecoder(w.Body).Decode(&response); err != nil { 61 t.Fatalf("Failed to decode response: %v", err) 62 } 63 64 if len(response) != 0 { 65 t.Errorf("Expected empty object per lexicon, got %v", response) 66 } 67} 68 69func TestDeleteVoteHandler_RequiresAuth(t *testing.T) { 70 mockService := &mockVoteService{} 71 handler := NewDeleteVoteHandler(mockService) 72 73 // Create request body 74 reqBody := DeleteVoteInput{ 75 Subject: struct { 76 URI string `json:"uri"` 77 CID string `json:"cid"` 78 }{ 79 URI: "at://did:plc:author123/social.coves.post/xyz789", 80 CID: "bafypost123", 81 }, 82 } 83 bodyBytes, err := json.Marshal(reqBody) 84 if err != nil { 85 t.Fatalf("Failed to marshal request: %v", err) 86 } 87 88 // Create HTTP request without auth context 89 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes)) 90 req.Header.Set("Content-Type", "application/json") 91 // No OAuth session in context 92 93 // Execute handler 94 w := httptest.NewRecorder() 95 handler.HandleDeleteVote(w, req) 96 97 // Check status code 98 if w.Code != http.StatusUnauthorized { 99 t.Errorf("Expected status 401, got %d. Body: %s", w.Code, w.Body.String()) 100 } 101 102 // Check error response 103 var errResp XRPCError 104 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 105 t.Fatalf("Failed to decode error response: %v", err) 106 } 107 if errResp.Error != "AuthRequired" { 108 t.Errorf("Expected error AuthRequired, got %s", errResp.Error) 109 } 110} 111 112func TestDeleteVoteHandler_MissingFields(t *testing.T) { 113 mockService := &mockVoteService{} 114 handler := NewDeleteVoteHandler(mockService) 115 116 tests := []struct { 117 name string 118 subjectURI string 119 subjectCID string 120 expectedError string 121 }{ 122 { 123 name: "missing subject URI", 124 subjectURI: "", 125 subjectCID: "bafypost123", 126 expectedError: "subject.uri is required", 127 }, 128 { 129 name: "missing subject CID", 130 subjectURI: "at://did:plc:author123/social.coves.post/xyz789", 131 subjectCID: "", 132 expectedError: "subject.cid is required", 133 }, 134 } 135 136 for _, tc := range tests { 137 t.Run(tc.name, func(t *testing.T) { 138 // Create request body 139 reqBody := DeleteVoteInput{ 140 Subject: struct { 141 URI string `json:"uri"` 142 CID string `json:"cid"` 143 }{ 144 URI: tc.subjectURI, 145 CID: tc.subjectCID, 146 }, 147 } 148 bodyBytes, err := json.Marshal(reqBody) 149 if err != nil { 150 t.Fatalf("Failed to marshal request: %v", err) 151 } 152 153 // Create HTTP request 154 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes)) 155 req.Header.Set("Content-Type", "application/json") 156 157 // Inject OAuth session 158 did, _ := syntax.ParseDID("did:plc:test123") 159 session := &oauthlib.ClientSessionData{ 160 AccountDID: did, 161 AccessToken: "test_token", 162 } 163 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 164 req = req.WithContext(ctx) 165 166 // Execute handler 167 w := httptest.NewRecorder() 168 handler.HandleDeleteVote(w, req) 169 170 // Check status code 171 if w.Code != http.StatusBadRequest { 172 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String()) 173 } 174 175 // Check error response 176 var errResp XRPCError 177 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 178 t.Fatalf("Failed to decode error response: %v", err) 179 } 180 if errResp.Message != tc.expectedError { 181 t.Errorf("Expected message '%s', got '%s'", tc.expectedError, errResp.Message) 182 } 183 }) 184 } 185} 186 187func TestDeleteVoteHandler_InvalidJSON(t *testing.T) { 188 mockService := &mockVoteService{} 189 handler := NewDeleteVoteHandler(mockService) 190 191 // Create invalid JSON 192 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBufferString("{invalid json")) 193 req.Header.Set("Content-Type", "application/json") 194 195 // Inject OAuth session 196 did, _ := syntax.ParseDID("did:plc:test123") 197 session := &oauthlib.ClientSessionData{ 198 AccountDID: did, 199 AccessToken: "test_token", 200 } 201 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 202 req = req.WithContext(ctx) 203 204 // Execute handler 205 w := httptest.NewRecorder() 206 handler.HandleDeleteVote(w, req) 207 208 // Check status code 209 if w.Code != http.StatusBadRequest { 210 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String()) 211 } 212 213 // Check error response 214 var errResp XRPCError 215 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 216 t.Fatalf("Failed to decode error response: %v", err) 217 } 218 if errResp.Error != "InvalidRequest" { 219 t.Errorf("Expected error InvalidRequest, got %s", errResp.Error) 220 } 221} 222 223func TestDeleteVoteHandler_MethodNotAllowed(t *testing.T) { 224 mockService := &mockVoteService{} 225 handler := NewDeleteVoteHandler(mockService) 226 227 // Create GET request (should only accept POST) 228 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.vote.delete", nil) 229 230 // Execute handler 231 w := httptest.NewRecorder() 232 handler.HandleDeleteVote(w, req) 233 234 // Check status code 235 if w.Code != http.StatusMethodNotAllowed { 236 t.Errorf("Expected status 405, got %d", w.Code) 237 } 238} 239 240func TestDeleteVoteHandler_ServiceError(t *testing.T) { 241 tests := []struct { 242 serviceError error 243 name string 244 expectedError string 245 expectedStatus int 246 }{ 247 { 248 name: "vote not found", 249 serviceError: votes.ErrVoteNotFound, 250 expectedStatus: http.StatusNotFound, 251 expectedError: "VoteNotFound", // Per lexicon: social.coves.feed.vote.delete#VoteNotFound 252 }, 253 { 254 name: "invalid subject", 255 serviceError: votes.ErrInvalidSubject, 256 expectedStatus: http.StatusBadRequest, 257 expectedError: "InvalidSubject", // Per lexicon: social.coves.feed.vote.create#InvalidSubject 258 }, 259 { 260 name: "not authorized", 261 serviceError: votes.ErrNotAuthorized, 262 expectedStatus: http.StatusForbidden, 263 expectedError: "NotAuthorized", // Per lexicon: social.coves.feed.vote.delete#NotAuthorized 264 }, 265 { 266 name: "banned", 267 serviceError: votes.ErrBanned, 268 expectedStatus: http.StatusForbidden, 269 expectedError: "NotAuthorized", // Banned maps to NotAuthorized per lexicon 270 }, 271 } 272 273 for _, tc := range tests { 274 t.Run(tc.name, func(t *testing.T) { 275 mockService := &mockVoteService{ 276 deleteFunc: func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error { 277 return tc.serviceError 278 }, 279 } 280 handler := NewDeleteVoteHandler(mockService) 281 282 // Create request body 283 reqBody := DeleteVoteInput{ 284 Subject: struct { 285 URI string `json:"uri"` 286 CID string `json:"cid"` 287 }{ 288 URI: "at://did:plc:author123/social.coves.post/xyz789", 289 CID: "bafypost123", 290 }, 291 } 292 bodyBytes, err := json.Marshal(reqBody) 293 if err != nil { 294 t.Fatalf("Failed to marshal request: %v", err) 295 } 296 297 // Create HTTP request 298 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes)) 299 req.Header.Set("Content-Type", "application/json") 300 301 // Inject OAuth session 302 did, _ := syntax.ParseDID("did:plc:test123") 303 session := &oauthlib.ClientSessionData{ 304 AccountDID: did, 305 AccessToken: "test_token", 306 } 307 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 308 req = req.WithContext(ctx) 309 310 // Execute handler 311 w := httptest.NewRecorder() 312 handler.HandleDeleteVote(w, req) 313 314 // Check status code 315 if w.Code != tc.expectedStatus { 316 t.Errorf("Expected status %d, got %d. Body: %s", tc.expectedStatus, w.Code, w.Body.String()) 317 } 318 319 // Check error response 320 var errResp XRPCError 321 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 322 t.Fatalf("Failed to decode error response: %v", err) 323 } 324 if errResp.Error != tc.expectedError { 325 t.Errorf("Expected error %s, got %s", tc.expectedError, errResp.Error) 326 } 327 }) 328 } 329} 330 331func TestDeleteVoteHandler_MultipleSubjects(t *testing.T) { 332 mockService := &mockVoteService{} 333 handler := NewDeleteVoteHandler(mockService) 334 335 subjects := []struct { 336 uri string 337 cid string 338 }{ 339 {"at://did:plc:author1/social.coves.post/post1", "bafypost1"}, 340 {"at://did:plc:author2/social.coves.post/post2", "bafypost2"}, 341 {"at://did:plc:author3/social.coves.comment/comment1", "bafycomment1"}, 342 } 343 344 for _, subject := range subjects { 345 t.Run("subject_"+subject.uri, func(t *testing.T) { 346 // Create request body 347 reqBody := DeleteVoteInput{ 348 Subject: struct { 349 URI string `json:"uri"` 350 CID string `json:"cid"` 351 }{ 352 URI: subject.uri, 353 CID: subject.cid, 354 }, 355 } 356 bodyBytes, err := json.Marshal(reqBody) 357 if err != nil { 358 t.Fatalf("Failed to marshal request: %v", err) 359 } 360 361 // Create HTTP request 362 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes)) 363 req.Header.Set("Content-Type", "application/json") 364 365 // Inject OAuth session 366 did, _ := syntax.ParseDID("did:plc:test123") 367 session := &oauthlib.ClientSessionData{ 368 AccountDID: did, 369 AccessToken: "test_token", 370 } 371 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 372 req = req.WithContext(ctx) 373 374 // Execute handler 375 w := httptest.NewRecorder() 376 handler.HandleDeleteVote(w, req) 377 378 // Check status code 379 if w.Code != http.StatusOK { 380 t.Errorf("Expected status 200 for subject '%s', got %d. Body: %s", subject.uri, w.Code, w.Body.String()) 381 } 382 383 // Check response is empty object per lexicon 384 var response map[string]interface{} 385 if err := json.NewDecoder(w.Body).Decode(&response); err != nil { 386 t.Fatalf("Failed to decode response: %v", err) 387 } 388 389 if len(response) != 0 { 390 t.Errorf("Expected empty object per lexicon, got %v", response) 391 } 392 }) 393 } 394}