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