709 lines
21 KiB
Go
709 lines
21 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
)
|
|
|
|
func TestIsTransientSQLiteError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{name: "nil", err: nil, want: false},
|
|
{name: "locked", err: errors.New("database is locked"), want: true},
|
|
{name: "busy", err: errors.New("database is busy"), want: true},
|
|
{name: "table locked", err: errors.New("database table is locked"), want: true},
|
|
{name: "mixed case", err: errors.New("DataBase Is Locked"), want: true},
|
|
{name: "non transient", err: errors.New("constraint failed"), want: false},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
require.Equal(t, testCase.want, isTransientSQLiteError(testCase.err))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpsertSettingWithRetry_ReturnsErrorForClosedDB(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
|
|
stdDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
require.NoError(t, stdDB.Close())
|
|
|
|
setting := &models.Setting{
|
|
Key: "security.test.closed_db",
|
|
Value: "false",
|
|
Category: "security",
|
|
Type: "bool",
|
|
}
|
|
|
|
err = handler.upsertSettingWithRetry(setting)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func jsonReader(data interface{}) io.Reader {
|
|
b, _ := json.Marshal(data)
|
|
return bytes.NewReader(b)
|
|
}
|
|
|
|
func setupEmergencyTestDB(t *testing.T) *gorm.DB {
|
|
dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
err = db.AutoMigrate(
|
|
&models.Setting{},
|
|
&models.SecurityConfig{},
|
|
&models.SecurityAudit{},
|
|
&models.SecurityDecision{},
|
|
&models.EmergencyToken{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return db
|
|
}
|
|
|
|
func setupEmergencyRouter(handler *EmergencyHandler) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
_ = router.SetTrustedProxies(nil)
|
|
router.POST("/api/v1/emergency/security-reset", handler.SecurityReset)
|
|
return router
|
|
}
|
|
|
|
type mockCaddyManager struct {
|
|
calls int
|
|
}
|
|
|
|
func (m *mockCaddyManager) ApplyConfig(_ context.Context) error {
|
|
m.calls++
|
|
return nil
|
|
}
|
|
|
|
type mockCacheInvalidator struct {
|
|
calls int
|
|
}
|
|
|
|
func (m *mockCacheInvalidator) InvalidateCache() {
|
|
m.calls++
|
|
}
|
|
|
|
func TestEmergencySecurityReset_Success(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
// Configure valid token
|
|
validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum"
|
|
_ = os.Setenv(EmergencyTokenEnvVar, validToken)
|
|
defer func() { _ = os.Unsetenv(EmergencyTokenEnvVar) }()
|
|
|
|
// Create initial security config to verify it gets disabled
|
|
secConfig := models.SecurityConfig{
|
|
Name: "default",
|
|
Enabled: true,
|
|
WAFMode: "enabled",
|
|
RateLimitMode: "enabled",
|
|
RateLimitEnable: true,
|
|
CrowdSecMode: "local",
|
|
}
|
|
require.NoError(t, db.Create(&secConfig).Error)
|
|
|
|
// Make request with valid token
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, validToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert response
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, response["success"].(bool))
|
|
assert.NotNil(t, response["disabled_modules"])
|
|
disabledModules := response["disabled_modules"].([]interface{})
|
|
assert.GreaterOrEqual(t, len(disabledModules), 5)
|
|
|
|
// Verify settings were updated
|
|
// Note: feature.cerberus.enabled is intentionally NOT disabled
|
|
// The emergency reset only disables individual security modules (ACL, WAF, etc)
|
|
// while keeping the Cerberus framework enabled for break glass testing
|
|
|
|
// Verify ACL module is disabled
|
|
var aclSetting models.Setting
|
|
err = db.Where("key = ?", "security.acl.enabled").First(&aclSetting).Error
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "false", aclSetting.Value)
|
|
|
|
// Verify CrowdSec mode is disabled
|
|
var crowdsecMode models.Setting
|
|
err = db.Where("key = ?", "security.crowdsec.mode").First(&crowdsecMode).Error
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "disabled", crowdsecMode.Value)
|
|
|
|
// Verify admin whitelist is cleared
|
|
var adminWhitelist models.Setting
|
|
err = db.Where("key = ?", "security.admin_whitelist").First(&adminWhitelist).Error
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "", adminWhitelist.Value)
|
|
|
|
// Verify SecurityConfig was updated
|
|
var updatedConfig models.SecurityConfig
|
|
err = db.Where("name = ?", "default").First(&updatedConfig).Error
|
|
require.NoError(t, err)
|
|
assert.False(t, updatedConfig.Enabled)
|
|
assert.Equal(t, "disabled", updatedConfig.WAFMode)
|
|
assert.Equal(t, "", updatedConfig.AdminWhitelist)
|
|
|
|
// Note: Audit logging is async via SecurityService channel, tested separately
|
|
}
|
|
|
|
func TestEmergencySecurityReset_InvalidToken(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
// Configure valid token
|
|
validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum"
|
|
_ = os.Setenv(EmergencyTokenEnvVar, validToken)
|
|
defer func() { _ = os.Unsetenv(EmergencyTokenEnvVar) }()
|
|
|
|
// Make request with invalid token
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, "wrong-token")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert response
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "unauthorized", response["error"])
|
|
|
|
// Note: Audit logging is async via SecurityService channel, tested separately
|
|
}
|
|
|
|
func TestEmergencySecurityReset_MissingToken(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
// Configure valid token
|
|
validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum"
|
|
_ = os.Setenv(EmergencyTokenEnvVar, validToken)
|
|
defer func() { _ = os.Unsetenv(EmergencyTokenEnvVar) }()
|
|
|
|
// Make request without token header
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert response
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "unauthorized", response["error"])
|
|
assert.Contains(t, response["message"], "required")
|
|
|
|
// Note: Audit logging is async via SecurityService channel, tested separately
|
|
}
|
|
|
|
func TestEmergencySecurityReset_NotConfigured(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
// Ensure token is not configured
|
|
_ = os.Unsetenv(EmergencyTokenEnvVar)
|
|
|
|
// Make request
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, "any-token")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert response
|
|
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "not configured", response["error"])
|
|
assert.Contains(t, response["message"], "CHARON_EMERGENCY_TOKEN")
|
|
|
|
// Note: Audit logging is async via SecurityService channel, tested separately
|
|
}
|
|
|
|
func TestEmergencySecurityReset_TokenTooShort(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
// Configure token that is too short
|
|
shortToken := "too-short"
|
|
require.NoError(t, os.Setenv(EmergencyTokenEnvVar, shortToken))
|
|
defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }()
|
|
|
|
// Make request
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, shortToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Assert response
|
|
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "not configured", response["error"])
|
|
assert.Contains(t, response["message"], "minimum length")
|
|
}
|
|
|
|
func TestEmergencySecurityReset_NoRateLimit(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum"
|
|
require.NoError(t, os.Setenv(EmergencyTokenEnvVar, validToken))
|
|
defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }()
|
|
|
|
wrongToken := "wrong-token-for-no-rate-limit-test-32chars"
|
|
|
|
// Make rapid requests with invalid token; all should be unauthorized
|
|
for i := 0; i < 10; i++ {
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, wrongToken)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code, "Request %d should be unauthorized", i+1)
|
|
|
|
var response map[string]interface{}
|
|
err := json.NewDecoder(w.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "unauthorized", response["error"])
|
|
}
|
|
}
|
|
|
|
func TestEmergencySecurityReset_TriggersReloadAndCacheInvalidate(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
mockCaddy := &mockCaddyManager{}
|
|
mockCache := &mockCacheInvalidator{}
|
|
handler := NewEmergencyHandlerWithDeps(db, mockCaddy, mockCache)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum"
|
|
require.NoError(t, os.Setenv(EmergencyTokenEnvVar, validToken))
|
|
defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }()
|
|
|
|
// Make request with valid token
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, validToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, 1, mockCaddy.calls)
|
|
assert.Equal(t, 1, mockCache.calls)
|
|
}
|
|
|
|
func TestEmergencySecurityReset_ClearsBlockDecisions(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
router := setupEmergencyRouter(handler)
|
|
|
|
validToken := "this-is-a-valid-emergency-token-with-32-chars-minimum"
|
|
require.NoError(t, os.Setenv(EmergencyTokenEnvVar, validToken))
|
|
defer func() { require.NoError(t, os.Unsetenv(EmergencyTokenEnvVar)) }()
|
|
|
|
require.NoError(t, db.Create(&models.SecurityDecision{UUID: "dec-1", Source: "manual", Action: "block", IP: "127.0.0.1", CreatedAt: time.Now()}).Error)
|
|
require.NoError(t, db.Create(&models.SecurityDecision{UUID: "dec-2", Source: "manual", Action: "allow", IP: "127.0.0.2", CreatedAt: time.Now()}).Error)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
req.Header.Set(EmergencyTokenHeader, validToken)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var remaining []models.SecurityDecision
|
|
require.NoError(t, db.Find(&remaining).Error)
|
|
require.Len(t, remaining, 1)
|
|
assert.Equal(t, "allow", remaining[0].Action)
|
|
}
|
|
|
|
func TestEmergencySecurityReset_MiddlewarePrevalidatedBypass(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) {
|
|
c.Set("emergency_bypass", true)
|
|
handler.SecurityReset(c)
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestEmergencySecurityReset_MiddlewareBypass_ResetFailure(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
|
|
stdDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
require.NoError(t, stdDB.Close())
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) {
|
|
c.Set("emergency_bypass", true)
|
|
handler.SecurityReset(c)
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, w.Code)
|
|
}
|
|
|
|
func TestLogEnhancedAudit(t *testing.T) {
|
|
// Setup
|
|
db := setupEmergencyTestDB(t)
|
|
handler := NewEmergencyHandler(db)
|
|
defer handler.Close() // Flush async audit events
|
|
|
|
// Test enhanced audit logging
|
|
clientIP := "192.168.1.100"
|
|
action := "emergency_reset_test"
|
|
details := "Test audit log"
|
|
duration := 150 * time.Millisecond
|
|
|
|
handler.logEnhancedAudit(clientIP, action, details, true, duration)
|
|
|
|
// Close to flush async events before querying DB
|
|
handler.Close()
|
|
|
|
// Verify audit log was created
|
|
var audit models.SecurityAudit
|
|
err := db.Where("actor = ?", clientIP).First(&audit).Error
|
|
require.NoError(t, err, "Audit log should be created")
|
|
|
|
assert.Equal(t, clientIP, audit.Actor)
|
|
assert.Equal(t, action, audit.Action)
|
|
assert.Contains(t, audit.Details, "result=success")
|
|
assert.Contains(t, audit.Details, "duration=")
|
|
assert.Contains(t, audit.Details, "timestamp=")
|
|
}
|
|
|
|
func TestNewEmergencyTokenHandler(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
|
|
// Create token service
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
|
|
// Create handler using the token handler constructor
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
|
|
// Verify handler was created correctly
|
|
require.NotNil(t, handler)
|
|
require.NotNil(t, handler.db)
|
|
require.NotNil(t, handler.tokenService)
|
|
require.Nil(t, handler.securityService) // Token handler doesn't need security service
|
|
|
|
// Cleanup
|
|
handler.Close()
|
|
}
|
|
|
|
func TestGenerateToken_Success(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.POST("/api/v1/emergency/token", func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
c.Set("userID", uint(1))
|
|
handler.GenerateToken(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/token",
|
|
jsonReader(map[string]interface{}{"expiration_days": 30}))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resp["token"])
|
|
assert.Equal(t, "30_days", resp["expiration_policy"])
|
|
}
|
|
|
|
func TestGenerateToken_AdminRequired(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.POST("/api/v1/emergency/token", func(c *gin.Context) {
|
|
// No role set - simulating non-admin user
|
|
handler.GenerateToken(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/token",
|
|
jsonReader(map[string]interface{}{"expiration_days": 30}))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestGenerateToken_InvalidExpirationDays(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.POST("/api/v1/emergency/token", func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
handler.GenerateToken(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/token",
|
|
jsonReader(map[string]interface{}{"expiration_days": 500}))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Expiration days must be between 0 and 365")
|
|
}
|
|
|
|
func TestGetTokenStatus_Success(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
// Generate a token first
|
|
_, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30})
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.GET("/api/v1/emergency/token/status", func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
handler.GetTokenStatus(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/emergency/token/status", nil)
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
// Check key fields exist
|
|
assert.True(t, resp["configured"].(bool))
|
|
assert.Equal(t, "30_days", resp["expiration_policy"])
|
|
}
|
|
|
|
func TestGetTokenStatus_AdminRequired(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.GET("/api/v1/emergency/token/status", handler.GetTokenStatus)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/emergency/token/status", nil)
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestRevokeToken_Success(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
// Generate a token first
|
|
_, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30})
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.DELETE("/api/v1/emergency/token", func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
handler.RevokeToken(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/emergency/token", nil)
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Emergency token revoked")
|
|
}
|
|
|
|
func TestRevokeToken_AdminRequired(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.DELETE("/api/v1/emergency/token", handler.RevokeToken)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/emergency/token", nil)
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestUpdateTokenExpiration_Success(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
// Generate a token first
|
|
_, _ = tokenService.Generate(services.GenerateRequest{ExpirationDays: 30})
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.PATCH("/api/v1/emergency/token/expiration", func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
handler.UpdateTokenExpiration(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/emergency/token/expiration",
|
|
jsonReader(map[string]interface{}{"expiration_days": 60}))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "new_expires_at")
|
|
}
|
|
|
|
func TestUpdateTokenExpiration_AdminRequired(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.PATCH("/api/v1/emergency/token/expiration", handler.UpdateTokenExpiration)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/emergency/token/expiration",
|
|
jsonReader(map[string]interface{}{"expiration_days": 60}))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestUpdateTokenExpiration_InvalidDays(t *testing.T) {
|
|
db := setupEmergencyTestDB(t)
|
|
tokenService := services.NewEmergencyTokenService(db)
|
|
handler := NewEmergencyTokenHandler(tokenService)
|
|
defer handler.Close()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.PATCH("/api/v1/emergency/token/expiration", func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
handler.UpdateTokenExpiration(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/v1/emergency/token/expiration",
|
|
jsonReader(map[string]interface{}{"expiration_days": 400}))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Expiration days must be between 0 and 365")
|
|
}
|