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