mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 22:21:30 +00:00
7382e5f03b
Close coverage gaps identified by dual-audit (qualitative + quantitative). New test files for config (0%→98%), router (0%→100%), handler validation, health, audit, response helpers, webhook notifier (0%→88%), email notifier, middleware (recovery, rate limiter), domain profile, service nil-safety, config helpers, issuer bootstrap, and server bootstrap wiring. Expanded existing tests for ACME (34%→42%), step-ca (42%→52%), F5, SSH, agent (43%→63%), scheduler (88%→99%), renewal service, and issuerfactory. All tests pass: go test -short, go vet, go test -race clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
7.6 KiB
Go
255 lines
7.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestRateLimiter_AllowedWithinLimit verifies that requests within the rate limit are allowed.
|
|
func TestRateLimiter_AllowedWithinLimit(t *testing.T) {
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 10, BurstSize: 10})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_ExceededReturns429 verifies that requests exceeding the rate limit get 429.
|
|
func TestRateLimiter_ExceededReturns429(t *testing.T) {
|
|
// Create a limiter with very strict limits
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 0.1, BurstSize: 1})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
// First request should succeed (within burst)
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("first request: expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
// Second request should fail (burst exhausted, no tokens refilled)
|
|
req2 := httptest.NewRequest("GET", "/test", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
if w2.Code != http.StatusTooManyRequests {
|
|
t.Errorf("second request: expected status %d, got %d", http.StatusTooManyRequests, w2.Code)
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_BurstCapacity verifies that burst allows spike in traffic.
|
|
func TestRateLimiter_BurstCapacity(t *testing.T) {
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 1, BurstSize: 5})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
// Fire 5 requests in rapid succession (burst size)
|
|
for i := 0; i < 5; i++ {
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("burst request %d: expected status %d, got %d", i, http.StatusOK, w.Code)
|
|
}
|
|
}
|
|
|
|
// 6th request should be rejected (burst exhausted)
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusTooManyRequests {
|
|
t.Errorf("request after burst: expected status %d, got %d", http.StatusTooManyRequests, w.Code)
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_TokenRefill verifies that tokens refill over time.
|
|
func TestRateLimiter_TokenRefill(t *testing.T) {
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 10, BurstSize: 1})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
// First request succeeds (within burst)
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("first request: expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
// Second request fails (burst exhausted)
|
|
req2 := httptest.NewRequest("GET", "/test", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
if w2.Code != http.StatusTooManyRequests {
|
|
t.Errorf("second request: expected status %d, got %d", http.StatusTooManyRequests, w2.Code)
|
|
}
|
|
|
|
// Wait for tokens to refill at RPS=10 (100ms per token)
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Third request should succeed (token refilled)
|
|
req3 := httptest.NewRequest("GET", "/test", nil)
|
|
w3 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w3, req3)
|
|
if w3.Code != http.StatusOK {
|
|
t.Errorf("third request after refill: expected status %d, got %d", http.StatusOK, w3.Code)
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_ConcurrentRequests verifies behavior under concurrent load.
|
|
func TestRateLimiter_ConcurrentRequests(t *testing.T) {
|
|
// Rate limit: 5 RPS, burst of 2
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 5, BurstSize: 2})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
numGoroutines := 10
|
|
results := make([]int, numGoroutines)
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
|
|
// Fire concurrent requests
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
mu.Lock()
|
|
results[idx] = w.Code
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Count successful vs rate-limited responses
|
|
successCount := 0
|
|
rateLimitedCount := 0
|
|
for _, code := range results {
|
|
if code == http.StatusOK {
|
|
successCount++
|
|
} else if code == http.StatusTooManyRequests {
|
|
rateLimitedCount++
|
|
} else {
|
|
t.Errorf("unexpected status code: %d", code)
|
|
}
|
|
}
|
|
|
|
// With burst size 2, at most 2 should succeed immediately
|
|
if successCount > 2 {
|
|
t.Errorf("expected at most 2 concurrent requests to succeed, got %d", successCount)
|
|
}
|
|
|
|
// Some should be rate limited
|
|
if rateLimitedCount == 0 {
|
|
t.Error("expected at least some requests to be rate limited")
|
|
}
|
|
|
|
if successCount+rateLimitedCount != numGoroutines {
|
|
t.Errorf("request count mismatch: %d + %d != %d", successCount, rateLimitedCount, numGoroutines)
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_RetryAfterHeader verifies that rate-limited responses include Retry-After.
|
|
func TestRateLimiter_RetryAfterHeader(t *testing.T) {
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 0.1, BurstSize: 1})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
// Exhaust burst
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
// Trigger rate limit
|
|
req2 := httptest.NewRequest("GET", "/test", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusTooManyRequests {
|
|
t.Errorf("expected 429, got %d", w2.Code)
|
|
}
|
|
|
|
// Check for Retry-After header
|
|
retryAfter := w2.Header().Get("Retry-After")
|
|
if retryAfter == "" {
|
|
t.Error("expected Retry-After header in rate-limited response")
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_ZeroRPS verifies behavior with RPS=0 (all requests blocked).
|
|
func TestRateLimiter_ZeroRPS(t *testing.T) {
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 0, BurstSize: 1})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
// First request succeeds (burst)
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("burst request: expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
// Second request blocked (no refill with RPS=0)
|
|
req2 := httptest.NewRequest("GET", "/test", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
if w2.Code != http.StatusTooManyRequests {
|
|
t.Errorf("second request: expected status %d, got %d", http.StatusTooManyRequests, w2.Code)
|
|
}
|
|
}
|
|
|
|
// TestRateLimiter_VeryHighRPS verifies behavior with very high RPS (unlimited-like).
|
|
func TestRateLimiter_VeryHighRPS(t *testing.T) {
|
|
// 1000 RPS should allow most requests through
|
|
handler := NewRateLimiter(RateLimitConfig{RPS: 1000, BurstSize: 100})(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}),
|
|
)
|
|
|
|
// Fire 50 requests — most should succeed given the high rate
|
|
successCount := 0
|
|
for i := 0; i < 50; i++ {
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code == http.StatusOK {
|
|
successCount++
|
|
}
|
|
}
|
|
|
|
// With 1000 RPS and 100 burst, most should pass
|
|
if successCount < 40 {
|
|
t.Errorf("expected at least 40 of 50 requests to succeed at 1000 RPS, got %d", successCount)
|
|
}
|
|
}
|