test: comprehensive test gap closure across 24 packages

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>
This commit is contained in:
shankar0123
2026-04-09 23:09:40 -04:00
parent 5567d4b411
commit 7382e5f03b
24 changed files with 9225 additions and 4 deletions
+254
View File
@@ -0,0 +1,254 @@
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)
}
}
+104
View File
@@ -0,0 +1,104 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
)
// TestRecovery_CatchesPanic verifies that panic recovery middleware catches panics
// and returns a 500 error response.
func TestRecovery_CatchesPanic(t *testing.T) {
handler := Recovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("test panic")
}))
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
}
// Verify error response is present
if w.Body.Len() == 0 {
t.Error("expected error response body, got empty")
}
}
// TestRecovery_CatchesNilPanic verifies that recovery middleware handles nil panics.
func TestRecovery_CatchesNilPanic(t *testing.T) {
handler := Recovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This is unusual but valid in Go
panic(nil)
}))
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
}
}
// TestRecovery_NoPanicPasses verifies that non-panicking handlers pass through normally.
func TestRecovery_NoPanicPasses(t *testing.T) {
handler := Recovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Test", "success")
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)
}
if w.Header().Get("X-Test") != "success" {
t.Error("expected custom header to be set")
}
}
// TestRecovery_StringPanic verifies recovery from string panics.
func TestRecovery_StringPanic(t *testing.T) {
handler := Recovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("string panic message")
}))
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
}
}
// TestRecovery_ErrorPanic verifies recovery from error type panics.
func TestRecovery_ErrorPanic(t *testing.T) {
testErr := &customError{msg: "test error"}
handler := Recovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic(testErr)
}))
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
}
}
// customError is a simple error type for testing.
type customError struct {
msg string
}
func (e *customError) Error() string {
return e.msg
}