package middleware import ( "bytes" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" ) func TestInputGate_AllowsGETRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) req := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200 for GET request, got %d", w.Code) } } func TestInputGate_AllowsHEADRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.HEAD("/test", func(c *gin.Context) { c.Status(http.StatusOK) }) req := httptest.NewRequest(http.MethodHead, "/test", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200 for HEAD request, got %d", w.Code) } } func TestInputGate_AllowsOPTIONSRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.OPTIONS("/test", func(c *gin.Context) { c.Status(http.StatusOK) }) req := httptest.NewRequest(http.MethodOptions, "/test", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200 for OPTIONS request, got %d", w.Code) } } func TestInputGate_AllowsValidJSONRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) body := bytes.NewBufferString(`{"key": "value"}`) req := httptest.NewRequest(http.MethodPost, "/test", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Length", "16") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200 for valid JSON, got %d", w.Code) } } func TestInputGate_RejectsInvalidContentType(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() config := DefaultInputGateConfig() config.StrictContentType = true router.Use(InputGateWithConfig(config)) router.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) body := bytes.NewBufferString(`data`) req := httptest.NewRequest(http.MethodPost, "/test", body) req.Header.Set("Content-Type", "application/xml") req.Header.Set("Content-Length", "4") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusUnsupportedMediaType { t.Errorf("Expected status 415 for invalid content type, got %d", w.Code) } } func TestInputGate_AllowsEmptyContentType(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) body := bytes.NewBufferString(`data`) req := httptest.NewRequest(http.MethodPost, "/test", body) // No Content-Type header w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200 for empty content type, got %d", w.Code) } } func TestInputGate_RejectsOversizedRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() config := DefaultInputGateConfig() config.MaxBodySize = 100 // 100 bytes router.Use(InputGateWithConfig(config)) router.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // Create a body larger than 100 bytes largeBody := strings.Repeat("x", 200) body := bytes.NewBufferString(largeBody) req := httptest.NewRequest(http.MethodPost, "/test", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Length", "200") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusRequestEntityTooLarge { t.Errorf("Expected status 413 for oversized request, got %d", w.Code) } } func TestInputGate_AllowsLargeUploadPath(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() config := DefaultInputGateConfig() config.MaxBodySize = 100 // 100 bytes config.MaxFileSize = 1000 // 1000 bytes config.LargeUploadPaths = []string{"/api/v1/files/upload"} router.Use(InputGateWithConfig(config)) router.POST("/api/v1/files/upload", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // Create a body larger than MaxBodySize but smaller than MaxFileSize largeBody := strings.Repeat("x", 500) body := bytes.NewBufferString(largeBody) req := httptest.NewRequest(http.MethodPost, "/api/v1/files/upload", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Length", "500") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200 for large upload path, got %d", w.Code) } } func TestInputGate_ExcludedPaths(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() config := DefaultInputGateConfig() config.MaxBodySize = 10 // Very small config.ExcludedPaths = []string{"/health"} router.Use(InputGateWithConfig(config)) router.POST("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "healthy"}) }) // Send oversized body to excluded path largeBody := strings.Repeat("x", 100) body := bytes.NewBufferString(largeBody) req := httptest.NewRequest(http.MethodPost, "/health", body) req.Header.Set("Content-Length", "100") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should pass because path is excluded if w.Code != http.StatusOK { t.Errorf("Expected status 200 for excluded path, got %d", w.Code) } } func TestInputGate_RejectsInvalidContentLength(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) body := bytes.NewBufferString(`data`) req := httptest.NewRequest(http.MethodPost, "/test", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Length", "invalid") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for invalid content length, got %d", w.Code) } } func TestValidateFileUpload_BlockedExtension(t *testing.T) { tests := []struct { filename string contentType string blocked bool }{ {"malware.exe", "application/octet-stream", true}, {"script.bat", "application/octet-stream", true}, {"hack.cmd", "application/octet-stream", true}, {"shell.sh", "application/octet-stream", true}, {"powershell.ps1", "application/octet-stream", true}, {"document.pdf", "application/pdf", false}, {"image.jpg", "image/jpeg", false}, {"data.csv", "text/csv", false}, } for _, tt := range tests { valid, errMsg := ValidateFileUpload(tt.filename, tt.contentType, 100, nil) if tt.blocked && valid { t.Errorf("File %s should be blocked", tt.filename) } if !tt.blocked && !valid { t.Errorf("File %s should not be blocked, error: %s", tt.filename, errMsg) } } } func TestValidateFileUpload_OversizedFile(t *testing.T) { config := DefaultInputGateConfig() config.MaxFileSize = 1000 // 1KB valid, errMsg := ValidateFileUpload("test.pdf", "application/pdf", 2000, &config) if valid { t.Error("Should reject oversized file") } if !strings.Contains(errMsg, "size") { t.Errorf("Error message should mention size, got: %s", errMsg) } } func TestValidateFileUpload_ValidFile(t *testing.T) { config := DefaultInputGateConfig() valid, errMsg := ValidateFileUpload("document.pdf", "application/pdf", 1000, &config) if !valid { t.Errorf("Should accept valid file, got error: %s", errMsg) } } func TestValidateFileUpload_InvalidContentType(t *testing.T) { config := DefaultInputGateConfig() valid, errMsg := ValidateFileUpload("file.xyz", "application/x-unknown", 100, &config) if valid { t.Error("Should reject unknown file type") } if !strings.Contains(errMsg, "not allowed") { t.Errorf("Error message should mention not allowed, got: %s", errMsg) } } func TestValidateFileUpload_NilConfig(t *testing.T) { // Should use default config when nil is passed valid, _ := ValidateFileUpload("document.pdf", "application/pdf", 1000, nil) if !valid { t.Error("Should accept valid file with nil config (uses defaults)") } } func TestHasBlockedExtension(t *testing.T) { config := DefaultInputGateConfig() tests := []struct { filename string blocked bool }{ {"test.exe", true}, {"TEST.EXE", true}, // Case insensitive {"script.BAT", true}, {"app.APP", true}, {"document.pdf", false}, {"image.png", false}, {"", false}, } for _, tt := range tests { result := config.hasBlockedExtension(tt.filename) if result != tt.blocked { t.Errorf("File %s: expected blocked=%v, got %v", tt.filename, tt.blocked, result) } } } func TestValidateContentType(t *testing.T) { config := DefaultInputGateConfig() tests := []struct { contentType string valid bool }{ {"application/json", true}, {"application/json; charset=utf-8", true}, {"APPLICATION/JSON", true}, // Case insensitive {"multipart/form-data; boundary=----WebKitFormBoundary", true}, {"text/plain", true}, {"application/xml", false}, {"text/html", false}, {"", true}, // Empty is allowed } for _, tt := range tests { valid, _ := config.validateContentType(tt.contentType) if valid != tt.valid { t.Errorf("Content-Type %q: expected valid=%v, got %v", tt.contentType, tt.valid, valid) } } } func TestIsLargeUploadPath(t *testing.T) { config := DefaultInputGateConfig() config.LargeUploadPaths = []string{"/api/v1/files/upload", "/api/v1/documents"} tests := []struct { path string isLarge bool }{ {"/api/v1/files/upload", true}, {"/api/v1/files/upload/batch", true}, // Prefix match {"/api/v1/documents", true}, {"/api/v1/documents/1/attachments", true}, {"/api/v1/users", false}, {"/health", false}, } for _, tt := range tests { result := config.isLargeUploadPath(tt.path) if result != tt.isLarge { t.Errorf("Path %s: expected isLarge=%v, got %v", tt.path, tt.isLarge, result) } } } func TestGetMaxSize(t *testing.T) { config := DefaultInputGateConfig() config.MaxBodySize = 100 config.MaxFileSize = 1000 config.LargeUploadPaths = []string{"/api/v1/files/upload"} tests := []struct { path string expected int64 }{ {"/api/test", 100}, {"/api/v1/files/upload", 1000}, {"/health", 100}, } for _, tt := range tests { result := config.getMaxSize(tt.path) if result != tt.expected { t.Errorf("Path %s: expected maxSize=%d, got %d", tt.path, tt.expected, result) } } } func TestInputGate_DefaultMiddleware(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(InputGate()) router.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) body := bytes.NewBufferString(`{"key": "value"}`) req := httptest.NewRequest(http.MethodPost, "/test", body) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } }