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>
806 lines
20 KiB
Go
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)
|
|
}
|
|
}
|