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