A community based topic aggregation platform built on atproto
at main 14 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 17// mockVoteService implements votes.Service for testing 18type mockVoteService struct { 19 createFunc func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) 20 deleteFunc func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error 21} 22 23func (m *mockVoteService) CreateVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) { 24 if m.createFunc != nil { 25 return m.createFunc(ctx, session, req) 26 } 27 return &votes.CreateVoteResponse{ 28 URI: "at://did:plc:test123/social.coves.vote/abc123", 29 CID: "bafyvote123", 30 }, nil 31} 32 33func (m *mockVoteService) DeleteVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error { 34 if m.deleteFunc != nil { 35 return m.deleteFunc(ctx, session, req) 36 } 37 return nil 38} 39 40func TestCreateVoteHandler_Success(t *testing.T) { 41 mockService := &mockVoteService{} 42 handler := NewCreateVoteHandler(mockService) 43 44 // Create request body 45 reqBody := CreateVoteInput{ 46 Subject: struct { 47 URI string `json:"uri"` 48 CID string `json:"cid"` 49 }{ 50 URI: "at://did:plc:author123/social.coves.post/xyz789", 51 CID: "bafypost123", 52 }, 53 Direction: "up", 54 } 55 bodyBytes, err := json.Marshal(reqBody) 56 if err != nil { 57 t.Fatalf("Failed to marshal request: %v", err) 58 } 59 60 // Create HTTP request 61 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes)) 62 req.Header.Set("Content-Type", "application/json") 63 64 // Inject OAuth session into context (simulates auth middleware) 65 did, _ := syntax.ParseDID("did:plc:test123") 66 session := &oauthlib.ClientSessionData{ 67 AccountDID: did, 68 AccessToken: "test_token", 69 } 70 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 71 req = req.WithContext(ctx) 72 73 // Execute handler 74 w := httptest.NewRecorder() 75 handler.HandleCreateVote(w, req) 76 77 // Check status code 78 if w.Code != http.StatusOK { 79 t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) 80 } 81 82 // Check response 83 var response CreateVoteOutput 84 if err := json.NewDecoder(w.Body).Decode(&response); err != nil { 85 t.Fatalf("Failed to decode response: %v", err) 86 } 87 88 if response.URI != "at://did:plc:test123/social.coves.vote/abc123" { 89 t.Errorf("Expected URI at://did:plc:test123/social.coves.vote/abc123, got %s", response.URI) 90 } 91 if response.CID != "bafyvote123" { 92 t.Errorf("Expected CID bafyvote123, got %s", response.CID) 93 } 94} 95 96func TestCreateVoteHandler_RequiresAuth(t *testing.T) { 97 mockService := &mockVoteService{} 98 handler := NewCreateVoteHandler(mockService) 99 100 // Create request body 101 reqBody := CreateVoteInput{ 102 Subject: struct { 103 URI string `json:"uri"` 104 CID string `json:"cid"` 105 }{ 106 URI: "at://did:plc:author123/social.coves.post/xyz789", 107 CID: "bafypost123", 108 }, 109 Direction: "up", 110 } 111 bodyBytes, err := json.Marshal(reqBody) 112 if err != nil { 113 t.Fatalf("Failed to marshal request: %v", err) 114 } 115 116 // Create HTTP request without auth context 117 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes)) 118 req.Header.Set("Content-Type", "application/json") 119 // No OAuth session in context 120 121 // Execute handler 122 w := httptest.NewRecorder() 123 handler.HandleCreateVote(w, req) 124 125 // Check status code 126 if w.Code != http.StatusUnauthorized { 127 t.Errorf("Expected status 401, got %d. Body: %s", w.Code, w.Body.String()) 128 } 129 130 // Check error response 131 var errResp XRPCError 132 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 133 t.Fatalf("Failed to decode error response: %v", err) 134 } 135 if errResp.Error != "AuthRequired" { 136 t.Errorf("Expected error AuthRequired, got %s", errResp.Error) 137 } 138} 139 140func TestCreateVoteHandler_InvalidDirection(t *testing.T) { 141 mockService := &mockVoteService{} 142 handler := NewCreateVoteHandler(mockService) 143 144 tests := []struct { 145 name string 146 direction string 147 }{ 148 {"empty direction", ""}, 149 {"invalid direction", "sideways"}, 150 {"wrong case", "UP"}, 151 {"typo", "upp"}, 152 } 153 154 for _, tc := range tests { 155 t.Run(tc.name, func(t *testing.T) { 156 // Create request body 157 reqBody := CreateVoteInput{ 158 Subject: struct { 159 URI string `json:"uri"` 160 CID string `json:"cid"` 161 }{ 162 URI: "at://did:plc:author123/social.coves.post/xyz789", 163 CID: "bafypost123", 164 }, 165 Direction: tc.direction, 166 } 167 bodyBytes, err := json.Marshal(reqBody) 168 if err != nil { 169 t.Fatalf("Failed to marshal request: %v", err) 170 } 171 172 // Create HTTP request 173 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes)) 174 req.Header.Set("Content-Type", "application/json") 175 176 // Inject OAuth session 177 did, _ := syntax.ParseDID("did:plc:test123") 178 session := &oauthlib.ClientSessionData{ 179 AccountDID: did, 180 AccessToken: "test_token", 181 } 182 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 183 req = req.WithContext(ctx) 184 185 // Execute handler 186 w := httptest.NewRecorder() 187 handler.HandleCreateVote(w, req) 188 189 // Check status code 190 if w.Code != http.StatusBadRequest { 191 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String()) 192 } 193 194 // Check error response 195 var errResp XRPCError 196 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 197 t.Fatalf("Failed to decode error response: %v", err) 198 } 199 if errResp.Error != "InvalidRequest" { 200 t.Errorf("Expected error InvalidRequest, got %s", errResp.Error) 201 } 202 }) 203 } 204} 205 206func TestCreateVoteHandler_MissingFields(t *testing.T) { 207 mockService := &mockVoteService{} 208 handler := NewCreateVoteHandler(mockService) 209 210 tests := []struct { 211 name string 212 subjectURI string 213 subjectCID string 214 direction string 215 expectedError string 216 }{ 217 { 218 name: "missing subject URI", 219 subjectURI: "", 220 subjectCID: "bafypost123", 221 direction: "up", 222 expectedError: "subject.uri is required", 223 }, 224 { 225 name: "missing subject CID", 226 subjectURI: "at://did:plc:author123/social.coves.post/xyz789", 227 subjectCID: "", 228 direction: "up", 229 expectedError: "subject.cid is required", 230 }, 231 { 232 name: "missing direction", 233 subjectURI: "at://did:plc:author123/social.coves.post/xyz789", 234 subjectCID: "bafypost123", 235 direction: "", 236 expectedError: "direction is required", 237 }, 238 } 239 240 for _, tc := range tests { 241 t.Run(tc.name, func(t *testing.T) { 242 // Create request body 243 reqBody := CreateVoteInput{ 244 Subject: struct { 245 URI string `json:"uri"` 246 CID string `json:"cid"` 247 }{ 248 URI: tc.subjectURI, 249 CID: tc.subjectCID, 250 }, 251 Direction: tc.direction, 252 } 253 bodyBytes, err := json.Marshal(reqBody) 254 if err != nil { 255 t.Fatalf("Failed to marshal request: %v", err) 256 } 257 258 // Create HTTP request 259 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes)) 260 req.Header.Set("Content-Type", "application/json") 261 262 // Inject OAuth session 263 did, _ := syntax.ParseDID("did:plc:test123") 264 session := &oauthlib.ClientSessionData{ 265 AccountDID: did, 266 AccessToken: "test_token", 267 } 268 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 269 req = req.WithContext(ctx) 270 271 // Execute handler 272 w := httptest.NewRecorder() 273 handler.HandleCreateVote(w, req) 274 275 // Check status code 276 if w.Code != http.StatusBadRequest { 277 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String()) 278 } 279 280 // Check error response 281 var errResp XRPCError 282 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 283 t.Fatalf("Failed to decode error response: %v", err) 284 } 285 if errResp.Message != tc.expectedError { 286 t.Errorf("Expected message '%s', got '%s'", tc.expectedError, errResp.Message) 287 } 288 }) 289 } 290} 291 292func TestCreateVoteHandler_InvalidJSON(t *testing.T) { 293 mockService := &mockVoteService{} 294 handler := NewCreateVoteHandler(mockService) 295 296 // Create invalid JSON 297 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBufferString("{invalid json")) 298 req.Header.Set("Content-Type", "application/json") 299 300 // Inject OAuth session 301 did, _ := syntax.ParseDID("did:plc:test123") 302 session := &oauthlib.ClientSessionData{ 303 AccountDID: did, 304 AccessToken: "test_token", 305 } 306 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 307 req = req.WithContext(ctx) 308 309 // Execute handler 310 w := httptest.NewRecorder() 311 handler.HandleCreateVote(w, req) 312 313 // Check status code 314 if w.Code != http.StatusBadRequest { 315 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String()) 316 } 317 318 // Check error response 319 var errResp XRPCError 320 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 321 t.Fatalf("Failed to decode error response: %v", err) 322 } 323 if errResp.Error != "InvalidRequest" { 324 t.Errorf("Expected error InvalidRequest, got %s", errResp.Error) 325 } 326} 327 328func TestCreateVoteHandler_MethodNotAllowed(t *testing.T) { 329 mockService := &mockVoteService{} 330 handler := NewCreateVoteHandler(mockService) 331 332 // Create GET request (should only accept POST) 333 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.vote.create", nil) 334 335 // Execute handler 336 w := httptest.NewRecorder() 337 handler.HandleCreateVote(w, req) 338 339 // Check status code 340 if w.Code != http.StatusMethodNotAllowed { 341 t.Errorf("Expected status 405, got %d", w.Code) 342 } 343} 344 345func TestCreateVoteHandler_ServiceError(t *testing.T) { 346 tests := []struct { 347 serviceError error 348 name string 349 expectedError string 350 expectedStatus int 351 }{ 352 { 353 name: "invalid direction", 354 serviceError: votes.ErrInvalidDirection, 355 expectedStatus: http.StatusBadRequest, 356 expectedError: "InvalidRequest", 357 }, 358 { 359 name: "invalid subject", 360 serviceError: votes.ErrInvalidSubject, 361 expectedStatus: http.StatusBadRequest, 362 expectedError: "InvalidSubject", // Per lexicon: social.coves.feed.vote.create#InvalidSubject 363 }, 364 { 365 name: "not authorized", 366 serviceError: votes.ErrNotAuthorized, 367 expectedStatus: http.StatusForbidden, 368 expectedError: "NotAuthorized", // Per lexicon: social.coves.feed.vote.create#NotAuthorized 369 }, 370 { 371 name: "banned", 372 serviceError: votes.ErrBanned, 373 expectedStatus: http.StatusForbidden, 374 expectedError: "NotAuthorized", // Banned maps to NotAuthorized per lexicon 375 }, 376 } 377 378 for _, tc := range tests { 379 t.Run(tc.name, func(t *testing.T) { 380 mockService := &mockVoteService{ 381 createFunc: func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) { 382 return nil, tc.serviceError 383 }, 384 } 385 handler := NewCreateVoteHandler(mockService) 386 387 // Create request body 388 reqBody := CreateVoteInput{ 389 Subject: struct { 390 URI string `json:"uri"` 391 CID string `json:"cid"` 392 }{ 393 URI: "at://did:plc:author123/social.coves.post/xyz789", 394 CID: "bafypost123", 395 }, 396 Direction: "up", 397 } 398 bodyBytes, err := json.Marshal(reqBody) 399 if err != nil { 400 t.Fatalf("Failed to marshal request: %v", err) 401 } 402 403 // Create HTTP request 404 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes)) 405 req.Header.Set("Content-Type", "application/json") 406 407 // Inject OAuth session 408 did, _ := syntax.ParseDID("did:plc:test123") 409 session := &oauthlib.ClientSessionData{ 410 AccountDID: did, 411 AccessToken: "test_token", 412 } 413 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 414 req = req.WithContext(ctx) 415 416 // Execute handler 417 w := httptest.NewRecorder() 418 handler.HandleCreateVote(w, req) 419 420 // Check status code 421 if w.Code != tc.expectedStatus { 422 t.Errorf("Expected status %d, got %d. Body: %s", tc.expectedStatus, w.Code, w.Body.String()) 423 } 424 425 // Check error response 426 var errResp XRPCError 427 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 428 t.Fatalf("Failed to decode error response: %v", err) 429 } 430 if errResp.Error != tc.expectedError { 431 t.Errorf("Expected error %s, got %s", tc.expectedError, errResp.Error) 432 } 433 }) 434 } 435} 436 437func TestCreateVoteHandler_ValidDirections(t *testing.T) { 438 mockService := &mockVoteService{} 439 handler := NewCreateVoteHandler(mockService) 440 441 directions := []string{"up", "down"} 442 443 for _, direction := range directions { 444 t.Run("direction_"+direction, func(t *testing.T) { 445 // Create request body 446 reqBody := CreateVoteInput{ 447 Subject: struct { 448 URI string `json:"uri"` 449 CID string `json:"cid"` 450 }{ 451 URI: "at://did:plc:author123/social.coves.post/xyz789", 452 CID: "bafypost123", 453 }, 454 Direction: direction, 455 } 456 bodyBytes, err := json.Marshal(reqBody) 457 if err != nil { 458 t.Fatalf("Failed to marshal request: %v", err) 459 } 460 461 // Create HTTP request 462 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes)) 463 req.Header.Set("Content-Type", "application/json") 464 465 // Inject OAuth session 466 did, _ := syntax.ParseDID("did:plc:test123") 467 session := &oauthlib.ClientSessionData{ 468 AccountDID: did, 469 AccessToken: "test_token", 470 } 471 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 472 req = req.WithContext(ctx) 473 474 // Execute handler 475 w := httptest.NewRecorder() 476 handler.HandleCreateVote(w, req) 477 478 // Check status code 479 if w.Code != http.StatusOK { 480 t.Errorf("Expected status 200 for direction '%s', got %d. Body: %s", direction, w.Code, w.Body.String()) 481 } 482 }) 483 } 484}