A community based topic aggregation platform built on atproto
1package e2e 2 3import ( 4 "Coves/internal/api/middleware" 5 "bytes" 6 "encoding/json" 7 "net/http" 8 "net/http/httptest" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13) 14 15// TestRateLimiting_E2E_GeneralEndpoints tests the global rate limiter (100 req/min) 16// This tests the middleware applied to all endpoints in main.go 17func TestRateLimiting_E2E_GeneralEndpoints(t *testing.T) { 18 // Create rate limiter with same config as main.go: 100 requests per minute 19 rateLimiter := middleware.NewRateLimiter(100, 1*time.Minute) 20 21 // Simple test handler that just returns 200 OK 22 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 w.WriteHeader(http.StatusOK) 24 _, _ = w.Write([]byte("OK")) 25 }) 26 27 // Wrap handler with rate limiter 28 handler := rateLimiter.Middleware(testHandler) 29 30 t.Run("Allows requests under limit", func(t *testing.T) { 31 // Make 50 requests (well under 100 limit) 32 for i := 0; i < 50; i++ { 33 req := httptest.NewRequest("GET", "/test", nil) 34 req.RemoteAddr = "192.168.1.100:12345" // Consistent IP 35 rr := httptest.NewRecorder() 36 37 handler.ServeHTTP(rr, req) 38 39 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 40 } 41 }) 42 43 t.Run("Blocks requests at limit", func(t *testing.T) { 44 // Create fresh rate limiter for this test 45 limiter := middleware.NewRateLimiter(10, 1*time.Minute) 46 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 w.WriteHeader(http.StatusOK) 48 }) 49 handler := limiter.Middleware(testHandler) 50 51 clientIP := "192.168.1.101:12345" 52 53 // Make exactly 10 requests (at limit) 54 for i := 0; i < 10; i++ { 55 req := httptest.NewRequest("GET", "/test", nil) 56 req.RemoteAddr = clientIP 57 rr := httptest.NewRecorder() 58 59 handler.ServeHTTP(rr, req) 60 61 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 62 } 63 64 // 11th request should be rate limited 65 req := httptest.NewRequest("GET", "/test", nil) 66 req.RemoteAddr = clientIP 67 rr := httptest.NewRecorder() 68 69 handler.ServeHTTP(rr, req) 70 71 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Request 11 should be rate limited") 72 assert.Contains(t, rr.Body.String(), "Rate limit exceeded", "Should have rate limit error message") 73 }) 74 75 t.Run("Returns proper 429 status code", func(t *testing.T) { 76 // Create very strict rate limiter (1 req/min) 77 limiter := middleware.NewRateLimiter(1, 1*time.Minute) 78 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 w.WriteHeader(http.StatusOK) 80 }) 81 handler := limiter.Middleware(testHandler) 82 83 clientIP := "192.168.1.102:12345" 84 85 // First request succeeds 86 req := httptest.NewRequest("GET", "/test", nil) 87 req.RemoteAddr = clientIP 88 rr := httptest.NewRecorder() 89 handler.ServeHTTP(rr, req) 90 assert.Equal(t, http.StatusOK, rr.Code) 91 92 // Second request gets 429 93 req = httptest.NewRequest("GET", "/test", nil) 94 req.RemoteAddr = clientIP 95 rr = httptest.NewRecorder() 96 handler.ServeHTTP(rr, req) 97 98 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Should return 429 Too Many Requests") 99 assert.Equal(t, "text/plain; charset=utf-8", rr.Header().Get("Content-Type")) 100 }) 101 102 t.Run("Rate limits are per-client (IP isolation)", func(t *testing.T) { 103 // Create strict rate limiter 104 limiter := middleware.NewRateLimiter(2, 1*time.Minute) 105 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 106 w.WriteHeader(http.StatusOK) 107 }) 108 handler := limiter.Middleware(testHandler) 109 110 // Client 1 makes 2 requests (exhausts limit) 111 client1IP := "192.168.1.103:12345" 112 for i := 0; i < 2; i++ { 113 req := httptest.NewRequest("GET", "/test", nil) 114 req.RemoteAddr = client1IP 115 rr := httptest.NewRecorder() 116 handler.ServeHTTP(rr, req) 117 assert.Equal(t, http.StatusOK, rr.Code) 118 } 119 120 // Client 1's 3rd request is blocked 121 req := httptest.NewRequest("GET", "/test", nil) 122 req.RemoteAddr = client1IP 123 rr := httptest.NewRecorder() 124 handler.ServeHTTP(rr, req) 125 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Client 1 should be rate limited") 126 127 // Client 2 can still make requests (different IP) 128 client2IP := "192.168.1.104:12345" 129 req = httptest.NewRequest("GET", "/test", nil) 130 req.RemoteAddr = client2IP 131 rr = httptest.NewRecorder() 132 handler.ServeHTTP(rr, req) 133 assert.Equal(t, http.StatusOK, rr.Code, "Client 2 should not be affected by Client 1's rate limit") 134 }) 135 136 t.Run("Respects X-Forwarded-For header", func(t *testing.T) { 137 limiter := middleware.NewRateLimiter(1, 1*time.Minute) 138 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 139 w.WriteHeader(http.StatusOK) 140 }) 141 handler := limiter.Middleware(testHandler) 142 143 // First request with X-Forwarded-For 144 req := httptest.NewRequest("GET", "/test", nil) 145 req.Header.Set("X-Forwarded-For", "203.0.113.1") 146 rr := httptest.NewRecorder() 147 handler.ServeHTTP(rr, req) 148 assert.Equal(t, http.StatusOK, rr.Code) 149 150 // Second request with same X-Forwarded-For should be rate limited 151 req = httptest.NewRequest("GET", "/test", nil) 152 req.Header.Set("X-Forwarded-For", "203.0.113.1") 153 rr = httptest.NewRecorder() 154 handler.ServeHTTP(rr, req) 155 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Should rate limit based on X-Forwarded-For") 156 }) 157 158 t.Run("Respects X-Real-IP header", func(t *testing.T) { 159 limiter := middleware.NewRateLimiter(1, 1*time.Minute) 160 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 161 w.WriteHeader(http.StatusOK) 162 }) 163 handler := limiter.Middleware(testHandler) 164 165 // First request with X-Real-IP 166 req := httptest.NewRequest("GET", "/test", nil) 167 req.Header.Set("X-Real-IP", "203.0.113.2") 168 rr := httptest.NewRecorder() 169 handler.ServeHTTP(rr, req) 170 assert.Equal(t, http.StatusOK, rr.Code) 171 172 // Second request with same X-Real-IP should be rate limited 173 req = httptest.NewRequest("GET", "/test", nil) 174 req.Header.Set("X-Real-IP", "203.0.113.2") 175 rr = httptest.NewRecorder() 176 handler.ServeHTTP(rr, req) 177 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Should rate limit based on X-Real-IP") 178 }) 179} 180 181// TestRateLimiting_E2E_CommentEndpoints tests comment-specific rate limiting (20 req/min) 182// This tests the stricter rate limit applied to expensive nested comment queries 183func TestRateLimiting_E2E_CommentEndpoints(t *testing.T) { 184 // Create rate limiter with comment config from main.go: 20 requests per minute 185 commentRateLimiter := middleware.NewRateLimiter(20, 1*time.Minute) 186 187 // Mock comment handler 188 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 189 // Simulate comment response 190 response := map[string]interface{}{ 191 "comments": []map[string]interface{}{}, 192 } 193 w.Header().Set("Content-Type", "application/json") 194 w.WriteHeader(http.StatusOK) 195 _ = json.NewEncoder(w).Encode(response) 196 }) 197 198 // Wrap with comment rate limiter 199 handler := commentRateLimiter.Middleware(testHandler) 200 201 t.Run("Allows requests under comment limit", func(t *testing.T) { 202 clientIP := "192.168.1.110:12345" 203 204 // Make 15 requests (under 20 limit) 205 for i := 0; i < 15; i++ { 206 req := httptest.NewRequest("GET", "/xrpc/social.coves.community.comment.getComments?post=at://test", nil) 207 req.RemoteAddr = clientIP 208 rr := httptest.NewRecorder() 209 210 handler.ServeHTTP(rr, req) 211 212 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 213 } 214 }) 215 216 t.Run("Blocks requests at comment limit", func(t *testing.T) { 217 clientIP := "192.168.1.111:12345" 218 219 // Make exactly 20 requests (at limit) 220 for i := 0; i < 20; i++ { 221 req := httptest.NewRequest("GET", "/xrpc/social.coves.community.comment.getComments?post=at://test", nil) 222 req.RemoteAddr = clientIP 223 rr := httptest.NewRecorder() 224 225 handler.ServeHTTP(rr, req) 226 227 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 228 } 229 230 // 21st request should be rate limited 231 req := httptest.NewRequest("GET", "/xrpc/social.coves.community.comment.getComments?post=at://test", nil) 232 req.RemoteAddr = clientIP 233 rr := httptest.NewRecorder() 234 235 handler.ServeHTTP(rr, req) 236 237 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Request 21 should be rate limited") 238 assert.Contains(t, rr.Body.String(), "Rate limit exceeded") 239 }) 240 241 t.Run("Comment limit is stricter than general limit", func(t *testing.T) { 242 // Verify that 20 req/min < 100 req/min 243 assert.Less(t, 20, 100, "Comment rate limit should be stricter than general rate limit") 244 }) 245} 246 247// TestRateLimiting_E2E_AggregatorPosts tests aggregator post rate limiting (10 posts/hour) 248// This is already tested in aggregator_e2e_test.go but we verify it here for completeness 249func TestRateLimiting_E2E_AggregatorPosts(t *testing.T) { 250 t.Run("Aggregator rate limit enforced", func(t *testing.T) { 251 // This test is comprehensive in tests/integration/aggregator_e2e_test.go 252 // Part 4: Rate Limiting - Enforces 10 posts/hour limit 253 // We verify the constants match here 254 const RateLimitWindow = 1 * time.Hour 255 const RateLimitMaxPosts = 10 256 257 assert.Equal(t, 1*time.Hour, RateLimitWindow, "Aggregator rate limit window should be 1 hour") 258 assert.Equal(t, 10, RateLimitMaxPosts, "Aggregator rate limit should be 10 posts/hour") 259 }) 260} 261 262// TestRateLimiting_E2E_RateLimitHeaders tests that rate limit information is included in responses 263func TestRateLimiting_E2E_RateLimitHeaders(t *testing.T) { 264 t.Run("Current implementation does not include rate limit headers", func(t *testing.T) { 265 // CURRENT STATE: The middleware does not set rate limit headers 266 // FUTURE ENHANCEMENT: Add headers like: 267 // - X-RateLimit-Limit: Maximum requests allowed 268 // - X-RateLimit-Remaining: Requests remaining in window 269 // - X-RateLimit-Reset: Time when limit resets (Unix timestamp) 270 // - Retry-After: Seconds until limit resets (on 429 responses) 271 272 limiter := middleware.NewRateLimiter(5, 1*time.Minute) 273 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 274 w.WriteHeader(http.StatusOK) 275 }) 276 handler := limiter.Middleware(testHandler) 277 278 req := httptest.NewRequest("GET", "/test", nil) 279 req.RemoteAddr = "192.168.1.120:12345" 280 rr := httptest.NewRecorder() 281 282 handler.ServeHTTP(rr, req) 283 284 // Document current behavior: no rate limit headers 285 assert.Equal(t, "", rr.Header().Get("X-RateLimit-Limit"), "Currently no rate limit headers") 286 assert.Equal(t, "", rr.Header().Get("X-RateLimit-Remaining"), "Currently no rate limit headers") 287 assert.Equal(t, "", rr.Header().Get("X-RateLimit-Reset"), "Currently no rate limit headers") 288 assert.Equal(t, "", rr.Header().Get("Retry-After"), "Currently no Retry-After header") 289 290 t.Log("NOTE: Rate limit headers are not implemented yet. This is acceptable for Alpha.") 291 t.Log("Consider adding rate limit headers in a future enhancement.") 292 }) 293 294 t.Run("429 response includes error message", func(t *testing.T) { 295 limiter := middleware.NewRateLimiter(1, 1*time.Minute) 296 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 297 w.WriteHeader(http.StatusOK) 298 }) 299 handler := limiter.Middleware(testHandler) 300 301 clientIP := "192.168.1.121:12345" 302 303 // First request 304 req := httptest.NewRequest("GET", "/test", nil) 305 req.RemoteAddr = clientIP 306 rr := httptest.NewRecorder() 307 handler.ServeHTTP(rr, req) 308 assert.Equal(t, http.StatusOK, rr.Code) 309 310 // Second request gets 429 with message 311 req = httptest.NewRequest("GET", "/test", nil) 312 req.RemoteAddr = clientIP 313 rr = httptest.NewRecorder() 314 handler.ServeHTTP(rr, req) 315 316 assert.Equal(t, http.StatusTooManyRequests, rr.Code) 317 assert.Contains(t, rr.Body.String(), "Rate limit exceeded") 318 assert.Contains(t, rr.Body.String(), "Please try again later") 319 }) 320} 321 322// TestRateLimiting_E2E_ResetBehavior tests rate limit window reset behavior 323func TestRateLimiting_E2E_ResetBehavior(t *testing.T) { 324 t.Run("Rate limit resets after window expires", func(t *testing.T) { 325 // Use very short window for testing (100ms) 326 limiter := middleware.NewRateLimiter(2, 100*time.Millisecond) 327 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 328 w.WriteHeader(http.StatusOK) 329 }) 330 handler := limiter.Middleware(testHandler) 331 332 clientIP := "192.168.1.130:12345" 333 334 // Make 2 requests (exhaust limit) 335 for i := 0; i < 2; i++ { 336 req := httptest.NewRequest("GET", "/test", nil) 337 req.RemoteAddr = clientIP 338 rr := httptest.NewRecorder() 339 handler.ServeHTTP(rr, req) 340 assert.Equal(t, http.StatusOK, rr.Code) 341 } 342 343 // 3rd request is blocked 344 req := httptest.NewRequest("GET", "/test", nil) 345 req.RemoteAddr = clientIP 346 rr := httptest.NewRecorder() 347 handler.ServeHTTP(rr, req) 348 assert.Equal(t, http.StatusTooManyRequests, rr.Code) 349 350 // Wait for window to expire 351 time.Sleep(150 * time.Millisecond) 352 353 // Request should now succeed (window reset) 354 req = httptest.NewRequest("GET", "/test", nil) 355 req.RemoteAddr = clientIP 356 rr = httptest.NewRecorder() 357 handler.ServeHTTP(rr, req) 358 assert.Equal(t, http.StatusOK, rr.Code, "Request should succeed after window reset") 359 }) 360 361 t.Run("Rolling window behavior", func(t *testing.T) { 362 // Use 200ms window for testing 363 limiter := middleware.NewRateLimiter(3, 200*time.Millisecond) 364 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 365 w.WriteHeader(http.StatusOK) 366 }) 367 handler := limiter.Middleware(testHandler) 368 369 clientIP := "192.168.1.131:12345" 370 371 // Make 3 requests over time 372 for i := 0; i < 3; i++ { 373 req := httptest.NewRequest("GET", "/test", nil) 374 req.RemoteAddr = clientIP 375 rr := httptest.NewRecorder() 376 handler.ServeHTTP(rr, req) 377 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 378 time.Sleep(50 * time.Millisecond) // Space out requests 379 } 380 381 // 4th request immediately after should be blocked (still in window) 382 req := httptest.NewRequest("GET", "/test", nil) 383 req.RemoteAddr = clientIP 384 rr := httptest.NewRecorder() 385 handler.ServeHTTP(rr, req) 386 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "4th request should be blocked") 387 388 // Wait for first request's window to expire (200ms + buffer) 389 time.Sleep(100 * time.Millisecond) 390 391 // Now request should succeed (window has rolled forward) 392 req = httptest.NewRequest("GET", "/test", nil) 393 req.RemoteAddr = clientIP 394 rr = httptest.NewRecorder() 395 handler.ServeHTTP(rr, req) 396 assert.Equal(t, http.StatusOK, rr.Code, "Request should succeed after window rolls") 397 }) 398} 399 400// TestRateLimiting_E2E_ConcurrentRequests tests rate limiting with concurrent requests 401func TestRateLimiting_E2E_ConcurrentRequests(t *testing.T) { 402 t.Run("Rate limiting is thread-safe", func(t *testing.T) { 403 limiter := middleware.NewRateLimiter(10, 1*time.Minute) 404 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 405 w.WriteHeader(http.StatusOK) 406 }) 407 handler := limiter.Middleware(testHandler) 408 409 clientIP := "192.168.1.140:12345" 410 successCount := 0 411 rateLimitedCount := 0 412 413 // Make 20 concurrent requests from same IP 414 results := make(chan int, 20) 415 for i := 0; i < 20; i++ { 416 go func() { 417 req := httptest.NewRequest("GET", "/test", nil) 418 req.RemoteAddr = clientIP 419 rr := httptest.NewRecorder() 420 handler.ServeHTTP(rr, req) 421 results <- rr.Code 422 }() 423 } 424 425 // Collect results 426 for i := 0; i < 20; i++ { 427 code := <-results 428 if code == http.StatusOK { 429 successCount++ 430 } else if code == http.StatusTooManyRequests { 431 rateLimitedCount++ 432 } 433 } 434 435 // Should have exactly 10 successes and 10 rate limited 436 assert.Equal(t, 10, successCount, "Should allow exactly 10 requests") 437 assert.Equal(t, 10, rateLimitedCount, "Should rate limit exactly 10 requests") 438 }) 439} 440 441// TestRateLimiting_E2E_DifferentMethods tests that rate limiting applies across HTTP methods 442func TestRateLimiting_E2E_DifferentMethods(t *testing.T) { 443 t.Run("Rate limiting applies to all HTTP methods", func(t *testing.T) { 444 limiter := middleware.NewRateLimiter(3, 1*time.Minute) 445 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 446 w.WriteHeader(http.StatusOK) 447 }) 448 handler := limiter.Middleware(testHandler) 449 450 clientIP := "192.168.1.150:12345" 451 452 // Make GET request 453 req := httptest.NewRequest("GET", "/test", nil) 454 req.RemoteAddr = clientIP 455 rr := httptest.NewRecorder() 456 handler.ServeHTTP(rr, req) 457 assert.Equal(t, http.StatusOK, rr.Code) 458 459 // Make POST request 460 req = httptest.NewRequest("POST", "/test", bytes.NewBufferString("{}")) 461 req.RemoteAddr = clientIP 462 rr = httptest.NewRecorder() 463 handler.ServeHTTP(rr, req) 464 assert.Equal(t, http.StatusOK, rr.Code) 465 466 // Make PUT request 467 req = httptest.NewRequest("PUT", "/test", bytes.NewBufferString("{}")) 468 req.RemoteAddr = clientIP 469 rr = httptest.NewRecorder() 470 handler.ServeHTTP(rr, req) 471 assert.Equal(t, http.StatusOK, rr.Code) 472 473 // 4th request (DELETE) should be rate limited 474 req = httptest.NewRequest("DELETE", "/test", nil) 475 req.RemoteAddr = clientIP 476 rr = httptest.NewRecorder() 477 handler.ServeHTTP(rr, req) 478 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Rate limit should apply across methods") 479 }) 480} 481 482// Rate Limiting Configuration Documentation 483// ========================================== 484// This test file validates the following rate limits: 485// 486// 1. General Endpoints (Global Middleware) 487// - Limit: 100 requests per minute per IP 488// - Applied to: All XRPC endpoints 489// - Implementation: cmd/server/main.go:98-99 490// 491// 2. Comment Endpoints (Endpoint-Specific) 492// - Limit: 20 requests per minute per IP 493// - Applied to: social.coves.community.comment.getComments 494// - Reason: Expensive nested queries 495// - Implementation: cmd/server/main.go:448-456 496// 497// 3. Aggregator Posts (Business Logic) 498// - Limit: 10 posts per hour per aggregator per community 499// - Applied to: Aggregator post creation 500// - Implementation: internal/core/aggregators/service.go 501// - Tests: tests/integration/aggregator_e2e_test.go (Part 4) 502// 503// Rate Limit Response Behavior: 504// - Status Code: 429 Too Many Requests 505// - Error Message: 'Rate limit exceeded. Please try again later.' 506// - Headers: Not implemented (acceptable for Alpha) 507// 508// Client Identification (priority order): 509// 1. X-Forwarded-For header 510// 2. X-Real-IP header 511// 3. RemoteAddr 512// 513// Implementation Details: 514// - Type: In-memory, per-instance 515// - Thread-safe: Yes (mutex-protected) 516// - Cleanup: Background goroutine 517// - Future: Consider Redis for distributed rate limiting