Files
breakpilot-core/consent-service/internal/handlers/handlers_test.go
Benjamin Boenisch ad111d5e69 Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services:
- PostgreSQL (PostGIS), Valkey, MinIO, Qdrant
- Vault (PKI/TLS), Nginx (Reverse Proxy)
- Backend Core API, Consent Service, Billing Service
- RAG Service, Embedding Service
- Gitea, Woodpecker CI/CD
- Night Scheduler, Health Aggregator
- Jitsi (Web/XMPP/JVB/Jicofo), Mailpit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:13 +01:00

806 lines
20 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func init() {
gin.SetMode(gin.TestMode)
}
// setupTestRouter creates a test router with handlers
// Note: For full integration tests, use a test database
func setupTestRouter() *gin.Engine {
router := gin.New()
return router
}
// TestHealthEndpoint tests the health check endpoint
func TestHealthEndpoint(t *testing.T) {
router := setupTestRouter()
// Add health endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "consent-service",
"version": "1.0.0",
})
})
req, _ := http.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
}
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
if response["status"] != "healthy" {
t.Errorf("Expected status 'healthy', got %v", response["status"])
}
}
// TestUnauthorizedAccess tests that protected endpoints require auth
func TestUnauthorizedAccess(t *testing.T) {
router := setupTestRouter()
// Add a protected endpoint
router.GET("/api/v1/consent/my", func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if auth == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization required"})
return
}
c.JSON(http.StatusOK, gin.H{"consents": []interface{}{}})
})
tests := []struct {
name string
authorization string
expectedStatus int
}{
{"no auth header", "", http.StatusUnauthorized},
{"empty bearer", "Bearer ", http.StatusOK}, // Would be invalid in real middleware
{"valid format", "Bearer test-token", http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/v1/consent/my", nil)
if tt.authorization != "" {
req.Header.Set("Authorization", tt.authorization)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
})
}
}
// TestCreateConsentRequest tests consent creation request validation
func TestCreateConsentRequest(t *testing.T) {
type ConsentRequest struct {
DocumentType string `json:"document_type"`
VersionID string `json:"version_id"`
Consented bool `json:"consented"`
}
tests := []struct {
name string
request ConsentRequest
expectValid bool
}{
{
name: "valid consent",
request: ConsentRequest{
DocumentType: "terms",
VersionID: "123e4567-e89b-12d3-a456-426614174000",
Consented: true,
},
expectValid: true,
},
{
name: "missing document type",
request: ConsentRequest{
VersionID: "123e4567-e89b-12d3-a456-426614174000",
Consented: true,
},
expectValid: false,
},
{
name: "missing version ID",
request: ConsentRequest{
DocumentType: "terms",
Consented: true,
},
expectValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.request.DocumentType != "" && tt.request.VersionID != ""
if isValid != tt.expectValid {
t.Errorf("Expected valid=%v, got %v", tt.expectValid, isValid)
}
})
}
}
// TestDocumentTypeValidation tests valid document types
func TestDocumentTypeValidation(t *testing.T) {
validTypes := map[string]bool{
"terms": true,
"privacy": true,
"cookies": true,
"community_guidelines": true,
"imprint": true,
}
tests := []struct {
docType string
expected bool
}{
{"terms", true},
{"privacy", true},
{"cookies", true},
{"community_guidelines", true},
{"imprint", true},
{"invalid", false},
{"", false},
{"Terms", false}, // case sensitive
}
for _, tt := range tests {
t.Run(tt.docType, func(t *testing.T) {
_, isValid := validTypes[tt.docType]
if isValid != tt.expected {
t.Errorf("Expected %s valid=%v, got %v", tt.docType, tt.expected, isValid)
}
})
}
}
// TestVersionStatusTransitions tests valid status transitions
func TestVersionStatusTransitions(t *testing.T) {
validTransitions := map[string][]string{
"draft": {"review"},
"review": {"approved", "rejected"},
"approved": {"scheduled", "published"},
"scheduled": {"published"},
"published": {"archived"},
"rejected": {"draft"},
"archived": {}, // terminal state
}
tests := []struct {
fromStatus string
toStatus string
expected bool
}{
{"draft", "review", true},
{"draft", "published", false},
{"review", "approved", true},
{"review", "rejected", true},
{"review", "published", false},
{"approved", "published", true},
{"approved", "scheduled", true},
{"published", "archived", true},
{"published", "draft", false},
{"archived", "draft", false},
}
for _, tt := range tests {
t.Run(tt.fromStatus+"->"+tt.toStatus, func(t *testing.T) {
allowed := false
if transitions, ok := validTransitions[tt.fromStatus]; ok {
for _, t := range transitions {
if t == tt.toStatus {
allowed = true
break
}
}
}
if allowed != tt.expected {
t.Errorf("Transition %s->%s: expected %v, got %v",
tt.fromStatus, tt.toStatus, tt.expected, allowed)
}
})
}
}
// TestRolePermissions tests role-based access control
func TestRolePermissions(t *testing.T) {
permissions := map[string]map[string]bool{
"user": {
"view_documents": true,
"give_consent": true,
"view_own_data": true,
"request_deletion": true,
"create_document": false,
"publish_version": false,
"approve_version": false,
},
"admin": {
"view_documents": true,
"give_consent": true,
"view_own_data": true,
"create_document": true,
"edit_version": true,
"publish_version": true,
"approve_version": false, // Only DSB
},
"data_protection_officer": {
"view_documents": true,
"create_document": true,
"edit_version": true,
"approve_version": true,
"publish_version": true,
"view_audit_log": true,
},
}
tests := []struct {
role string
action string
shouldHave bool
}{
{"user", "view_documents", true},
{"user", "create_document", false},
{"admin", "create_document", true},
{"admin", "approve_version", false},
{"data_protection_officer", "approve_version", true},
}
for _, tt := range tests {
t.Run(tt.role+":"+tt.action, func(t *testing.T) {
rolePerms, ok := permissions[tt.role]
if !ok {
t.Fatalf("Unknown role: %s", tt.role)
}
hasPermission := rolePerms[tt.action]
if hasPermission != tt.shouldHave {
t.Errorf("Role %s action %s: expected %v, got %v",
tt.role, tt.action, tt.shouldHave, hasPermission)
}
})
}
}
// TestJSONResponseFormat tests that responses have correct format
func TestJSONResponseFormat(t *testing.T) {
router := setupTestRouter()
router.GET("/api/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"id": "123",
"name": "Test",
},
})
})
req, _ := http.NewRequest("GET", "/api/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
contentType := w.Header().Get("Content-Type")
if contentType != "application/json; charset=utf-8" {
t.Errorf("Expected Content-Type 'application/json; charset=utf-8', got %s", contentType)
}
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
if err != nil {
t.Fatalf("Response should be valid JSON: %v", err)
}
}
// TestErrorResponseFormat tests error response format
func TestErrorResponseFormat(t *testing.T) {
router := setupTestRouter()
router.GET("/api/error", func(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid input",
})
})
req, _ := http.NewRequest("GET", "/api/error", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
}
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
if response["error"] == nil {
t.Error("Error response should contain 'error' field")
}
}
// TestCookieCategoryValidation tests cookie category validation
func TestCookieCategoryValidation(t *testing.T) {
mandatoryCategories := []string{"necessary"}
optionalCategories := []string{"functional", "analytics", "marketing"}
// Necessary should always be consented
for _, cat := range mandatoryCategories {
t.Run("mandatory_"+cat, func(t *testing.T) {
// Business rule: mandatory categories cannot be declined
isMandatory := true
if !isMandatory {
t.Errorf("Category %s should be mandatory", cat)
}
})
}
// Optional categories can be toggled
for _, cat := range optionalCategories {
t.Run("optional_"+cat, func(t *testing.T) {
isMandatory := false
if isMandatory {
t.Errorf("Category %s should not be mandatory", cat)
}
})
}
}
// TestPaginationParams tests pagination parameter handling
func TestPaginationParams(t *testing.T) {
tests := []struct {
name string
page int
perPage int
expPage int
expLimit int
}{
{"defaults", 0, 0, 1, 50},
{"page 1", 1, 10, 1, 10},
{"page 5", 5, 20, 5, 20},
{"negative page", -1, 10, 1, 10}, // should default
{"too large per_page", 1, 500, 1, 100}, // should cap
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
page := tt.page
perPage := tt.perPage
// Apply defaults and limits
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 50
}
if perPage > 100 {
perPage = 100
}
if page != tt.expPage {
t.Errorf("Expected page %d, got %d", tt.expPage, page)
}
if perPage != tt.expLimit {
t.Errorf("Expected perPage %d, got %d", tt.expLimit, perPage)
}
})
}
}
// TestIPAddressExtraction tests IP address extraction from requests
func TestIPAddressExtraction(t *testing.T) {
tests := []struct {
name string
xForwarded string
remoteAddr string
expected string
}{
{"direct connection", "", "192.168.1.1:1234", "192.168.1.1"},
{"behind proxy", "10.0.0.1", "192.168.1.1:1234", "10.0.0.1"},
{"multiple proxies", "10.0.0.1, 10.0.0.2", "192.168.1.1:1234", "10.0.0.1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router := setupTestRouter()
var extractedIP string
router.GET("/test", func(c *gin.Context) {
if xf := c.GetHeader("X-Forwarded-For"); xf != "" {
// Take first IP from list
for i, ch := range xf {
if ch == ',' {
extractedIP = xf[:i]
break
}
}
if extractedIP == "" {
extractedIP = xf
}
} else {
// Extract IP from RemoteAddr
addr := c.Request.RemoteAddr
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
extractedIP = addr[:i]
break
}
}
}
c.JSON(http.StatusOK, gin.H{"ip": extractedIP})
})
req, _ := http.NewRequest("GET", "/test", nil)
req.RemoteAddr = tt.remoteAddr
if tt.xForwarded != "" {
req.Header.Set("X-Forwarded-For", tt.xForwarded)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if extractedIP != tt.expected {
t.Errorf("Expected IP %s, got %s", tt.expected, extractedIP)
}
})
}
}
// TestRequestBodySizeLimit tests that large requests are rejected
func TestRequestBodySizeLimit(t *testing.T) {
router := setupTestRouter()
// Simulate a body size limit check
maxBodySize := int64(1024 * 1024) // 1MB
router.POST("/api/upload", func(c *gin.Context) {
if c.Request.ContentLength > maxBodySize {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{
"error": "Request body too large",
})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
})
tests := []struct {
name string
contentLength int64
expectedStatus int
}{
{"small body", 1000, http.StatusOK},
{"medium body", 500000, http.StatusOK},
{"exactly at limit", maxBodySize, http.StatusOK},
{"over limit", maxBodySize + 1, http.StatusRequestEntityTooLarge},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := bytes.NewReader(make([]byte, 0))
req, _ := http.NewRequest("POST", "/api/upload", body)
req.ContentLength = tt.contentLength
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
})
}
}
// ========================================
// EXTENDED HANDLER TESTS
// ========================================
// TestAuthHandlers tests authentication endpoints
func TestAuthHandlers(t *testing.T) {
router := setupTestRouter()
// Register endpoint
router.POST("/api/v1/auth/register", func(c *gin.Context) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "User registered"})
})
// Login endpoint
router.POST("/api/v1/auth/login", func(c *gin.Context) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
c.JSON(http.StatusOK, gin.H{"access_token": "token123"})
})
tests := []struct {
name string
endpoint string
method string
body interface{}
expectedStatus int
}{
{
name: "register - valid",
endpoint: "/api/v1/auth/register",
method: "POST",
body: map[string]string{"email": "test@example.com", "password": "password123"},
expectedStatus: http.StatusCreated,
},
{
name: "login - valid",
endpoint: "/api/v1/auth/login",
method: "POST",
body: map[string]string{"email": "test@example.com", "password": "password123"},
expectedStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
jsonBody, _ := json.Marshal(tt.body)
req, _ := http.NewRequest(tt.method, tt.endpoint, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
})
}
}
// TestDocumentHandlers tests document endpoints
func TestDocumentHandlers(t *testing.T) {
router := setupTestRouter()
// GET documents
router.GET("/api/v1/documents", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"documents": []interface{}{}})
})
// GET document by type
router.GET("/api/v1/documents/:type", func(c *gin.Context) {
docType := c.Param("type")
if docType == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid type"})
return
}
c.JSON(http.StatusOK, gin.H{"id": "123", "type": docType})
})
tests := []struct {
name string
endpoint string
expectedStatus int
}{
{"get all documents", "/api/v1/documents", http.StatusOK},
{"get terms", "/api/v1/documents/terms", http.StatusOK},
{"get privacy", "/api/v1/documents/privacy", http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", tt.endpoint, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
})
}
}
// TestConsentHandlers tests consent endpoints
func TestConsentHandlers(t *testing.T) {
router := setupTestRouter()
// Create consent
router.POST("/api/v1/consent", func(c *gin.Context) {
var req struct {
VersionID string `json:"version_id"`
Consented bool `json:"consented"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "Consent saved"})
})
// Check consent
router.GET("/api/v1/consent/check/:type", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"has_consent": true, "needs_update": false})
})
tests := []struct {
name string
endpoint string
method string
body interface{}
expectedStatus int
}{
{
name: "create consent",
endpoint: "/api/v1/consent",
method: "POST",
body: map[string]interface{}{"version_id": "123", "consented": true},
expectedStatus: http.StatusCreated,
},
{
name: "check consent",
endpoint: "/api/v1/consent/check/terms",
method: "GET",
expectedStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var req *http.Request
if tt.body != nil {
jsonBody, _ := json.Marshal(tt.body)
req, _ = http.NewRequest(tt.method, tt.endpoint, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
} else {
req, _ = http.NewRequest(tt.method, tt.endpoint, nil)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
})
}
}
// TestAdminHandlers tests admin endpoints
func TestAdminHandlers(t *testing.T) {
router := setupTestRouter()
// Create document (admin only)
router.POST("/api/v1/admin/documents", func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if auth != "Bearer admin-token" {
c.JSON(http.StatusForbidden, gin.H{"error": "Admin only"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "Document created"})
})
tests := []struct {
name string
token string
expectedStatus int
}{
{"admin token", "Bearer admin-token", http.StatusCreated},
{"user token", "Bearer user-token", http.StatusForbidden},
{"no token", "", http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := map[string]string{"type": "terms", "name": "Test"}
jsonBody, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "/api/v1/admin/documents", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
if tt.token != "" {
req.Header.Set("Authorization", tt.token)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
}
})
}
}
// TestCORSHeaders tests CORS headers
func TestCORSHeaders(t *testing.T) {
router := setupTestRouter()
router.Use(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Next()
})
router.GET("/api/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "test"})
})
req, _ := http.NewRequest("GET", "/api/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Error("CORS headers not set correctly")
}
}
// TestRateLimiting tests rate limiting logic
func TestRateLimiting(t *testing.T) {
requests := 0
limit := 5
for i := 0; i < 10; i++ {
requests++
if requests > limit {
// Would return 429 Too Many Requests
if requests <= limit {
t.Error("Rate limit not enforced")
}
}
}
}
// TestEmailTemplateHandlers tests email template endpoints
func TestEmailTemplateHandlers(t *testing.T) {
router := setupTestRouter()
router.GET("/api/v1/admin/email-templates", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"templates": []interface{}{}})
})
router.POST("/api/v1/admin/email-templates/test", func(c *gin.Context) {
var req struct {
Recipient string `json:"recipient"`
VersionID string `json:"version_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Test email sent"})
})
req, _ := http.NewRequest("GET", "/api/v1/admin/email-templates", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
}
}