package httpx import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) func init() { gin.SetMode(gin.TestMode) } func TestSecurityHeaders(t *testing.T) { r := gin.New() r.Use(SecurityHeaders()) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(w, req) want := map[string]string{ "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Referrer-Policy": "no-referrer", "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", "Strict-Transport-Security": "max-age=63072000; includeSubDomains", } for header, expected := range want { if got := w.Header().Get(header); got != expected { t.Errorf("header %s = %q, want %q", header, got, expected) } } } func TestCORS_AllowedOrigin(t *testing.T) { r := gin.New() r.Use(CORS([]string{"https://omnex.example"})) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Origin", "https://omnex.example") r.ServeHTTP(w, req) if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://omnex.example" { t.Errorf("Access-Control-Allow-Origin = %q, want %q", got, "https://omnex.example") } if got := w.Header().Get("Vary"); got != "Origin" { t.Errorf("Vary = %q, want %q", got, "Origin") } } func TestCORS_DisallowedOrigin(t *testing.T) { r := gin.New() r.Use(CORS([]string{"https://omnex.example"})) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Origin", "https://evil.example") r.ServeHTTP(w, req) if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("Access-Control-Allow-Origin = %q, want empty for disallowed origin", got) } } func TestCORS_PreflightOptions(t *testing.T) { r := gin.New() r.Use(CORS([]string{"https://omnex.example"})) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodOptions, "/", nil) req.Header.Set("Origin", "https://omnex.example") r.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Errorf("OPTIONS status = %d, want %d", w.Code, http.StatusNoContent) } } func TestRateLimit_AllowsWithinBurst(t *testing.T) { r := gin.New() r.Use(RateLimit(1, 3)) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) for i := 0; i < 3; i++ { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.1:1234" r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("request %d: status = %d, want %d", i, w.Code, http.StatusOK) } } } func TestRateLimit_BlocksOverBurst(t *testing.T) { r := gin.New() r.Use(RateLimit(1, 2)) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) for i := 0; i < 2; i++ { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.2:1234" r.ServeHTTP(w, req) } w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.2:1234" r.ServeHTTP(w, req) if w.Code != http.StatusTooManyRequests { t.Errorf("status = %d, want %d", w.Code, http.StatusTooManyRequests) } } func TestRateLimit_PerIPIsolation(t *testing.T) { r := gin.New() r.Use(RateLimit(1, 1)) r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) }) // Épuise le burst pour l'IP A. w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.3:1234" r.ServeHTTP(w, req) // L'IP B doit rester indépendante. w2 := httptest.NewRecorder() req2 := httptest.NewRequest(http.MethodGet, "/", nil) req2.RemoteAddr = "10.0.0.4:1234" r.ServeHTTP(w2, req2) if w2.Code != http.StatusOK { t.Errorf("IP B status = %d, want %d (isolation from IP A)", w2.Code, http.StatusOK) } }