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