This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

422 lines
11 KiB
Go

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)
}
}