Files
Charon/backend/internal/api/handlers/security_headers_handler_test.go
GitHub Actions 99f01608d9 fix: improve test coverage to meet 85% threshold
- Add comprehensive tests for security headers handler
- Add testdb timeout behavior tests
- Add recovery middleware edge case tests
- Add routes registration tests
- Add config initialization tests
- Fix parallel test safety issues

Coverage improved from 78.51% to 85.3%
2025-12-21 07:24:11 +00:00

844 lines
24 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupSecurityHeadersTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{})
assert.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewSecurityHeadersHandler(db, nil)
handler.RegisterRoutes(router.Group("/"))
return router, db
}
func TestListProfiles(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
// Create test profiles
profile1 := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Profile 1",
}
db.Create(&profile1)
profile2 := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Profile 2",
IsPreset: true,
}
db.Create(&profile2)
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string][]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response["profiles"], 2)
}
func TestGetProfile_ByID(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Test Profile",
}
db.Create(&profile)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Test Profile", response["profile"].Name)
}
func TestGetProfile_ByUUID(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
testUUID := uuid.New().String()
profile := models.SecurityHeaderProfile{
UUID: testUUID,
Name: "Test Profile",
}
db.Create(&profile)
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles/"+testUUID, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Test Profile", response["profile"].Name)
assert.Equal(t, testUUID, response["profile"].UUID)
}
func TestGetProfile_NotFound(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles/99999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestCreateProfile(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"name": "New Profile",
"hsts_enabled": true,
"hsts_max_age": 31536000,
"x_frame_options": "DENY",
"x_content_type_options": true,
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/profiles", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var response map[string]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "New Profile", response["profile"].Name)
assert.NotEmpty(t, response["profile"].UUID)
assert.NotZero(t, response["profile"].SecurityScore)
}
func TestCreateProfile_MissingName(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"hsts_enabled": true,
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/profiles", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestUpdateProfile(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Original Name",
}
db.Create(&profile)
updates := map[string]any{
"name": "Updated Name",
"hsts_enabled": false,
"csp_enabled": true,
"csp_directives": `{"default-src":["'self'"]}`,
}
body, _ := json.Marshal(updates)
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Updated Name", response["profile"].Name)
assert.False(t, response["profile"].HSTSEnabled)
assert.True(t, response["profile"].CSPEnabled)
}
func TestUpdateProfile_CannotModifyPreset(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
preset := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Preset",
IsPreset: true,
}
db.Create(&preset)
updates := map[string]any{
"name": "Modified Preset",
}
body, _ := json.Marshal(updates)
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/security/headers/profiles/%d", preset.ID), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestDeleteProfile(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "To Delete",
}
db.Create(&profile)
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify deleted
var count int64
db.Model(&models.SecurityHeaderProfile{}).Where("id = ?", profile.ID).Count(&count)
assert.Equal(t, int64(0), count)
}
func TestDeleteProfile_CannotDeletePreset(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
preset := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Preset",
IsPreset: true,
}
db.Create(&preset)
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/security/headers/profiles/%d", preset.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestDeleteProfile_InUse(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "In Use",
}
db.Create(&profile)
// Create proxy host using this profile
host := models.ProxyHost{
UUID: uuid.New().String(),
DomainNames: "example.com",
ForwardHost: "localhost",
ForwardPort: 8080,
SecurityHeaderProfileID: &profile.ID,
}
db.Create(&host)
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusConflict, w.Code)
}
func TestGetPresets(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodGet, "/security/headers/presets", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string][]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Len(t, response["presets"], 4)
// Verify preset types
presetTypes := make(map[string]bool)
for _, preset := range response["presets"] {
presetTypes[preset.PresetType] = true
}
assert.True(t, presetTypes["basic"])
assert.True(t, presetTypes["api-friendly"])
assert.True(t, presetTypes["strict"])
assert.True(t, presetTypes["paranoid"])
}
func TestApplyPreset(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"preset_type": "basic",
"name": "My Basic Profile",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/presets/apply", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var response map[string]models.SecurityHeaderProfile
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "My Basic Profile", response["profile"].Name)
assert.False(t, response["profile"].IsPreset) // Should not be a preset
assert.Empty(t, response["profile"].PresetType)
assert.NotEmpty(t, response["profile"].UUID)
}
func TestApplyPreset_InvalidType(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"preset_type": "nonexistent",
"name": "Test",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/presets/apply", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestCalculateScore(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"hsts_enabled": true,
"hsts_max_age": 31536000,
"hsts_include_subdomains": true,
"hsts_preload": true,
"csp_enabled": true,
"csp_directives": `{"default-src":["'self'"]}`,
"x_frame_options": "DENY",
"x_content_type_options": true,
"referrer_policy": "no-referrer",
"permissions_policy": `[{"feature":"camera","allowlist":[]}]`,
"cross_origin_opener_policy": "same-origin",
"cross_origin_resource_policy": "same-origin",
"cross_origin_embedder_policy": "require-corp",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/score", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, float64(100), response["score"])
assert.Equal(t, float64(100), response["max_score"])
assert.NotNil(t, response["breakdown"])
}
func TestValidateCSP_Valid(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"csp": `{"default-src":["'self'"],"script-src":["'self'"]}`,
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/validate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.True(t, response["valid"].(bool))
}
func TestValidateCSP_Invalid(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"csp": `not valid json`,
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/validate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["valid"].(bool))
assert.NotEmpty(t, response["errors"])
}
func TestValidateCSP_UnsafeDirectives(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"csp": `{"default-src":["'self'"],"script-src":["'self'","'unsafe-inline'","'unsafe-eval'"]}`,
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/validate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["valid"].(bool))
errors := response["errors"].([]any)
assert.NotEmpty(t, errors)
}
func TestBuildCSP(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"directives": []map[string]any{
{
"directive": "default-src",
"values": []string{"'self'"},
},
{
"directive": "script-src",
"values": []string{"'self'", "https:"},
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/build", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response["csp"])
// Verify it's valid JSON
var cspMap map[string][]string
err = json.Unmarshal([]byte(response["csp"]), &cspMap)
assert.NoError(t, err)
assert.Equal(t, []string{"'self'"}, cspMap["default-src"])
assert.Equal(t, []string{"'self'", "https:"}, cspMap["script-src"])
}
// Additional tests for missing coverage
func TestListProfiles_DBError(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
// Close DB to force error
sqlDB, _ := db.DB()
sqlDB.Close()
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestGetProfile_UUID_NotFound(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
// Use a UUID that doesn't exist
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles/non-existent-uuid-12345", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestGetProfile_ID_DBError(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
// Close DB to force error
sqlDB, _ := db.DB()
sqlDB.Close()
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestGetProfile_UUID_DBError(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
// Close DB to force error
sqlDB, _ := db.DB()
sqlDB.Close()
req := httptest.NewRequest(http.MethodGet, "/security/headers/profiles/some-uuid-format", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestCreateProfile_InvalidJSON(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/security/headers/profiles", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestCreateProfile_DBError(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
// Close DB to force error
sqlDB, _ := db.DB()
sqlDB.Close()
payload := map[string]any{
"name": "Test Profile",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/profiles", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestUpdateProfile_InvalidID(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodPut, "/security/headers/profiles/invalid", bytes.NewReader([]byte("{}")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestUpdateProfile_NotFound(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{"name": "Updated"}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPut, "/security/headers/profiles/99999", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestUpdateProfile_InvalidJSON(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Test Profile",
}
db.Create(&profile)
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestUpdateProfile_DBError(t *testing.T) {
router, db := setupSecurityHeadersTestRouter(t)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Test Profile",
}
db.Create(&profile)
// Close DB to force error on save
sqlDB, _ := db.DB()
sqlDB.Close()
payload := map[string]any{"name": "Updated"}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestUpdateProfile_LookupDBError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{})
assert.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewSecurityHeadersHandler(db, nil)
handler.RegisterRoutes(router.Group("/"))
// Close DB before making request
sqlDB, _ := db.DB()
sqlDB.Close()
payload := map[string]any{"name": "Updated"}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPut, "/security/headers/profiles/1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestDeleteProfile_InvalidID(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodDelete, "/security/headers/profiles/invalid", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestDeleteProfile_NotFound(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodDelete, "/security/headers/profiles/99999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestDeleteProfile_LookupDBError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{})
assert.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewSecurityHeadersHandler(db, nil)
handler.RegisterRoutes(router.Group("/"))
// Close DB before making request
sqlDB, _ := db.DB()
sqlDB.Close()
req := httptest.NewRequest(http.MethodDelete, "/security/headers/profiles/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestDeleteProfile_CountDBError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Only migrate SecurityHeaderProfile, NOT ProxyHost - this will cause count to fail
err = db.AutoMigrate(&models.SecurityHeaderProfile{})
assert.NoError(t, err)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Test",
}
db.Create(&profile)
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewSecurityHeadersHandler(db, nil)
handler.RegisterRoutes(router.Group("/"))
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestDeleteProfile_DeleteDBError(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&models.SecurityHeaderProfile{}, &models.ProxyHost{})
assert.NoError(t, err)
profile := models.SecurityHeaderProfile{
UUID: uuid.New().String(),
Name: "Test",
}
db.Create(&profile)
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewSecurityHeadersHandler(db, nil)
handler.RegisterRoutes(router.Group("/"))
// Close DB before delete to simulate DB error
sqlDB, _ := db.DB()
sqlDB.Close()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/security/headers/profiles/%d", profile.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Should be internal server error since DB is closed
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestApplyPreset_InvalidJSON(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/security/headers/presets/apply", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestCalculateScore_InvalidJSON(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/security/headers/score", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestValidateCSP_InvalidJSON(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/validate", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestValidateCSP_EmptyCSP(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"csp": "",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/validate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Empty CSP binding should fail since it's required
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestValidateCSP_UnknownDirective(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
payload := map[string]any{
"csp": `{"unknown-directive":["'self'"]}`,
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/validate", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["valid"].(bool))
errors := response["errors"].([]any)
assert.NotEmpty(t, errors)
}
func TestBuildCSP_InvalidJSON(t *testing.T) {
router, _ := setupSecurityHeadersTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/security/headers/csp/build", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}