chore: clean .gitignore cache
This commit is contained in:
@@ -1,957 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupSecurityTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityDecision{}, &models.SecurityAudit{}, &models.SecurityRuleSet{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestSecurityService_Upsert_ValidateAdminWhitelist(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Invalid CIDR in admin whitelist should fail
|
||||
cfg := &models.SecurityConfig{Name: "default", Enabled: true, AdminWhitelist: "invalid-cidr"}
|
||||
err := svc.Upsert(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrInvalidAdminCIDR, err)
|
||||
|
||||
// Valid CIDR should succeed
|
||||
cfg.AdminWhitelist = "192.168.1.0/24, 10.0.0.1"
|
||||
err = svc.Upsert(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify stored
|
||||
got, err := svc.Get()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.Contains(got.AdminWhitelist, "192.168.1.0/24"))
|
||||
}
|
||||
|
||||
func TestSecurityService_BreakGlassTokenLifecycle(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create record
|
||||
cfg := &models.SecurityConfig{Name: "default", Enabled: false}
|
||||
err := svc.Upsert(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token, err := svc.GenerateBreakGlassToken("default")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
// Verify valid token returns true
|
||||
ok, err := svc.VerifyBreakGlassToken("default", token)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Invalid token fails
|
||||
ok, err = svc.VerifyBreakGlassToken("default", "wrongtoken")
|
||||
assert.Error(t, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestSecurityService_LogDecisionAndList(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
dec := &models.SecurityDecision{Source: "manual", Action: "block", IP: "1.2.3.4", Host: "example.com", RuleID: "manual-1", Details: "test manual block"}
|
||||
err := svc.LogDecision(dec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
list, err := svc.ListDecisions(10)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(list), 1)
|
||||
assert.Equal(t, "manual", list[0].Source)
|
||||
}
|
||||
|
||||
func TestSecurityService_UpsertRuleSet(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Test creating new ruleset
|
||||
rs := &models.SecurityRuleSet{Name: "owasp-crs", SourceURL: "https://example.com/owasp.rules", Mode: "owasp", Content: "rule: 1"}
|
||||
err := svc.UpsertRuleSet(rs)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, rs.UUID)
|
||||
assert.False(t, rs.LastUpdated.IsZero())
|
||||
|
||||
// Test updating existing ruleset
|
||||
rs.Content = "rule: 2"
|
||||
rs.Mode = "updated"
|
||||
err = svc.UpsertRuleSet(rs)
|
||||
assert.NoError(t, err)
|
||||
|
||||
list, err := svc.ListRuleSets()
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(list), 1)
|
||||
assert.Equal(t, "owasp-crs", list[0].Name)
|
||||
assert.Equal(t, "rule: 2", list[0].Content)
|
||||
assert.Equal(t, "updated", list[0].Mode)
|
||||
|
||||
// Test nil ruleset
|
||||
err = svc.UpsertRuleSet(nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test ruleset without name
|
||||
invalidRuleset := &models.SecurityRuleSet{Content: "test"}
|
||||
err = svc.UpsertRuleSet(invalidRuleset)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "name required")
|
||||
}
|
||||
|
||||
func TestSecurityService_UpsertRuleSet_ContentTooLarge(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create a string slightly larger than 2MB
|
||||
large := strings.Repeat("x", 2*1024*1024+1)
|
||||
rs := &models.SecurityRuleSet{Name: "big-crs", Content: large}
|
||||
err := svc.UpsertRuleSet(rs)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSecurityService_DeleteRuleSet(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
rs := &models.SecurityRuleSet{Name: "owasp-crs", Content: "rule: 1"}
|
||||
err := svc.UpsertRuleSet(rs)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get list and pick ID
|
||||
list, err := svc.ListRuleSets()
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(list), 1)
|
||||
|
||||
id := list[0].ID
|
||||
// Delete
|
||||
err = svc.DeleteRuleSet(id)
|
||||
assert.NoError(t, err)
|
||||
// Ensure no rulesets left
|
||||
list, err = svc.ListRuleSets()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(list))
|
||||
}
|
||||
|
||||
func TestSecurityService_Upsert_RejectExternalMode(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// External mode should be rejected by validation
|
||||
cfg := &models.SecurityConfig{Name: "default", Enabled: true, CrowdSecMode: "external"}
|
||||
err := svc.Upsert(cfg)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Unknown mode should also be rejected
|
||||
cfg.CrowdSecMode = "unknown"
|
||||
err = svc.Upsert(cfg)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Local mode should be accepted
|
||||
cfg.CrowdSecMode = "local"
|
||||
err = svc.Upsert(cfg)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSecurityService_GenerateBreakGlassToken_NewConfig(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Generate token for non-existent config (should create it)
|
||||
token, err := svc.GenerateBreakGlassToken("newconfig")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
assert.Greater(t, len(token), 20) // Should be hex-encoded 24 bytes = 48 chars
|
||||
|
||||
// Verify the token works
|
||||
ok, err := svc.VerifyBreakGlassToken("newconfig", token)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Verify config was created with hash
|
||||
var cfg models.SecurityConfig
|
||||
err = db.Where("name = ?", "newconfig").First(&cfg).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, cfg.BreakGlassHash)
|
||||
}
|
||||
|
||||
func TestSecurityService_GenerateBreakGlassToken_UpdateExisting(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create initial config
|
||||
cfg := &models.SecurityConfig{Name: "default", Enabled: true}
|
||||
err := svc.Upsert(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Generate first token
|
||||
token1, err := svc.GenerateBreakGlassToken("default")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Generate second token (should replace first)
|
||||
token2, err := svc.GenerateBreakGlassToken("default")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, token1, token2)
|
||||
|
||||
// First token should no longer work
|
||||
ok, err := svc.VerifyBreakGlassToken("default", token1)
|
||||
assert.Error(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
// Second token should work
|
||||
ok, err = svc.VerifyBreakGlassToken("default", token2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestSecurityService_VerifyBreakGlassToken_NoConfig(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Verify against non-existent config
|
||||
ok, err := svc.VerifyBreakGlassToken("nonexistent", "anytoken")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrSecurityConfigNotFound, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestSecurityService_VerifyBreakGlassToken_NoHash(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create config without break-glass hash
|
||||
cfg := &models.SecurityConfig{Name: "default", Enabled: true, BreakGlassHash: ""}
|
||||
err := svc.Upsert(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify should fail with no hash
|
||||
ok, err := svc.VerifyBreakGlassToken("default", "anytoken")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrBreakGlassInvalid, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestSecurityService_VerifyBreakGlassToken_WrongToken(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Generate valid token
|
||||
token, err := svc.GenerateBreakGlassToken("default")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try various wrong tokens
|
||||
testCases := []string{
|
||||
"",
|
||||
"wrongtoken",
|
||||
"x" + token,
|
||||
token[:len(token)-1],
|
||||
strings.ToUpper(token),
|
||||
}
|
||||
|
||||
for _, wrongToken := range testCases {
|
||||
ok, err := svc.VerifyBreakGlassToken("default", wrongToken)
|
||||
assert.Error(t, err, "Token should fail: %s", wrongToken)
|
||||
assert.Equal(t, ErrBreakGlassInvalid, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityService_Get_NotFound(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Get from empty database
|
||||
cfg, err := svc.Get()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrSecurityConfigNotFound, err)
|
||||
assert.Nil(t, cfg)
|
||||
}
|
||||
|
||||
func TestSecurityService_Upsert_PreserveBreakGlassHash(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Generate token
|
||||
token, err := svc.GenerateBreakGlassToken("default")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get the hash
|
||||
var cfg models.SecurityConfig
|
||||
err = db.Where("name = ?", "default").First(&cfg).Error
|
||||
assert.NoError(t, err)
|
||||
originalHash := cfg.BreakGlassHash
|
||||
|
||||
// Update other fields
|
||||
cfg.Enabled = true
|
||||
cfg.AdminWhitelist = "10.0.0.0/8"
|
||||
err = svc.Upsert(&cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify hash is preserved
|
||||
var updated models.SecurityConfig
|
||||
err = db.Where("name = ?", "default").First(&updated).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, originalHash, updated.BreakGlassHash)
|
||||
|
||||
// Original token should still work
|
||||
ok, err := svc.VerifyBreakGlassToken("default", token)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestSecurityService_Get_PrefersDefaultConfig(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
defer svc.Close()
|
||||
|
||||
// Create a non-default config first to simulate environments with multiple rows.
|
||||
// Must provide unique UUIDs since the model has uniqueIndex on UUID field.
|
||||
other := &models.SecurityConfig{UUID: "test-other-uuid", Name: "other", Enabled: true}
|
||||
assert.NoError(t, db.Create(other).Error)
|
||||
|
||||
def := &models.SecurityConfig{UUID: "test-default-uuid", Name: "default", Enabled: false}
|
||||
assert.NoError(t, db.Create(def).Error)
|
||||
|
||||
cfg, err := svc.Get()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
assert.Equal(t, "default", cfg.Name)
|
||||
assert.False(t, cfg.Enabled)
|
||||
}
|
||||
|
||||
func TestSecurityService_Upsert_RateLimitFieldsPersist(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// 1. Create initial config with rate limit settings
|
||||
initialCfg := &models.SecurityConfig{
|
||||
Name: "default",
|
||||
Enabled: true,
|
||||
RateLimitEnable: true,
|
||||
RateLimitBurst: 10,
|
||||
RateLimitRequests: 100,
|
||||
RateLimitWindowSec: 60,
|
||||
WAFLearning: false,
|
||||
CrowdSecAPIURL: "http://localhost:8080",
|
||||
WAFRulesSource: "owasp-crs",
|
||||
}
|
||||
err := svc.Upsert(initialCfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify initial values
|
||||
got, err := svc.Get()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, got.RateLimitRequests)
|
||||
assert.Equal(t, 60, got.RateLimitWindowSec)
|
||||
assert.Equal(t, 10, got.RateLimitBurst)
|
||||
assert.False(t, got.WAFLearning)
|
||||
assert.Equal(t, "http://localhost:8080", got.CrowdSecAPIURL)
|
||||
assert.Equal(t, "owasp-crs", got.WAFRulesSource)
|
||||
|
||||
// 2. Update rate limit settings via Upsert
|
||||
updatedCfg := &models.SecurityConfig{
|
||||
Name: "default",
|
||||
Enabled: true,
|
||||
RateLimitEnable: true,
|
||||
RateLimitBurst: 50,
|
||||
RateLimitRequests: 500,
|
||||
RateLimitWindowSec: 120,
|
||||
WAFLearning: true,
|
||||
CrowdSecAPIURL: "http://crowdsec:8080",
|
||||
WAFRulesSource: "custom-rules",
|
||||
}
|
||||
err = svc.Upsert(updatedCfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 3. Verify all fields persisted correctly via Get()
|
||||
got, err = svc.Get()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 500, got.RateLimitRequests, "RateLimitRequests should be updated")
|
||||
assert.Equal(t, 120, got.RateLimitWindowSec, "RateLimitWindowSec should be updated")
|
||||
assert.Equal(t, 50, got.RateLimitBurst, "RateLimitBurst should be updated")
|
||||
assert.True(t, got.WAFLearning, "WAFLearning should be updated")
|
||||
assert.Equal(t, "http://crowdsec:8080", got.CrowdSecAPIURL, "CrowdSecAPIURL should be updated")
|
||||
assert.Equal(t, "custom-rules", got.WAFRulesSource, "WAFRulesSource should be updated")
|
||||
}
|
||||
|
||||
func TestSecurityService_LogAudit(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Test logging valid audit entry
|
||||
audit := &models.SecurityAudit{
|
||||
Action: "login_success",
|
||||
Actor: "admin",
|
||||
Details: "User admin logged in from 192.168.1.100",
|
||||
}
|
||||
err := svc.LogAudit(audit)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, audit.UUID)
|
||||
assert.False(t, audit.CreatedAt.IsZero())
|
||||
|
||||
// Give time for async audit logging to process
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Verify audit was stored
|
||||
var stored models.SecurityAudit
|
||||
err = db.Where("uuid = ?", audit.UUID).First(&stored).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "login_success", stored.Action)
|
||||
assert.Equal(t, "admin", stored.Actor)
|
||||
|
||||
// Test logging nil audit (should not error)
|
||||
err = svc.LogAudit(nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test audit with pre-filled UUID
|
||||
audit2 := &models.SecurityAudit{
|
||||
UUID: "custom-uuid-123",
|
||||
Action: "config_change",
|
||||
Actor: "admin",
|
||||
Details: "Security settings updated",
|
||||
}
|
||||
err = svc.LogAudit(audit2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "custom-uuid-123", audit2.UUID)
|
||||
}
|
||||
|
||||
func TestSecurityService_DeleteRuleSet_NotFound(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Try to delete non-existent ruleset
|
||||
err := svc.DeleteRuleSet(9999)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSecurityService_ListDecisions_UnlimitedAndLimited(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create multiple decisions
|
||||
for i := 0; i < 5; i++ {
|
||||
dec := &models.SecurityDecision{
|
||||
Source: "test",
|
||||
Action: "block",
|
||||
IP: "1.2.3." + string(rune('0'+i)),
|
||||
Host: "example.com",
|
||||
RuleID: "test-rule",
|
||||
Details: "test decision",
|
||||
}
|
||||
err := svc.LogDecision(dec)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test unlimited (limit = 0)
|
||||
all, err := svc.ListDecisions(0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 5)
|
||||
|
||||
// Test limited
|
||||
limited, err := svc.ListDecisions(2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, limited, 2)
|
||||
}
|
||||
|
||||
func TestSecurityService_LogDecision_Nil(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Nil decision should not error
|
||||
err := svc.LogDecision(nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSecurityService_LogDecision_PrefilledUUID(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
dec := &models.SecurityDecision{
|
||||
UUID: "custom-decision-uuid",
|
||||
Source: "manual",
|
||||
Action: "allow",
|
||||
IP: "10.0.0.1",
|
||||
Host: "internal.example.com",
|
||||
RuleID: "whitelist-1",
|
||||
Details: "whitelisted",
|
||||
}
|
||||
err := svc.LogDecision(dec)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "custom-decision-uuid", dec.UUID)
|
||||
|
||||
// Verify it was stored with custom UUID
|
||||
var stored models.SecurityDecision
|
||||
err = db.Where("uuid = ?", "custom-decision-uuid").First(&stored).Error
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSecurityService_ListRuleSets_Empty(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Empty database should return empty slice, not error
|
||||
list, err := svc.ListRuleSets()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, list)
|
||||
assert.Len(t, list, 0)
|
||||
}
|
||||
|
||||
func TestSecurityService_Upsert_InvalidCrowdSecMode(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Test various invalid modes
|
||||
invalidModes := []string{"", "invalid", "External", "LOCAL", "disabled123"}
|
||||
for _, mode := range invalidModes {
|
||||
cfg := &models.SecurityConfig{Name: "default", CrowdSecMode: mode}
|
||||
err := svc.Upsert(cfg)
|
||||
if mode == "" {
|
||||
// Empty mode is valid (defaults to disabled)
|
||||
continue
|
||||
}
|
||||
// Non-empty invalid modes should error
|
||||
if mode != "local" && mode != "disabled" && mode != "" {
|
||||
assert.Error(t, err, "Mode %q should be invalid", mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityService_ListAuditLogs(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create test audit logs
|
||||
testAudits := []models.SecurityAudit{
|
||||
{
|
||||
UUID: "audit-1",
|
||||
Actor: "user-1",
|
||||
Action: "dns_provider_create",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceUUID: "provider-1",
|
||||
Details: `{"name":"Provider 1"}`,
|
||||
},
|
||||
{
|
||||
UUID: "audit-2",
|
||||
Actor: "user-2",
|
||||
Action: "dns_provider_update",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceUUID: "provider-2",
|
||||
Details: `{"changed_fields":{"name":true}}`,
|
||||
},
|
||||
{
|
||||
UUID: "audit-3",
|
||||
Actor: "user-1",
|
||||
Action: "dns_provider_delete",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceUUID: "provider-3",
|
||||
Details: `{"name":"Provider 3"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, audit := range testAudits {
|
||||
err := db.Create(&audit).Error
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test listing all audits
|
||||
audits, total, err := svc.ListAuditLogs(AuditLogFilter{}, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, audits, 3)
|
||||
|
||||
// Test filter by actor
|
||||
audits, total, err = svc.ListAuditLogs(AuditLogFilter{Actor: "user-1"}, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, audits, 2)
|
||||
|
||||
// Test filter by action
|
||||
audits, total, err = svc.ListAuditLogs(AuditLogFilter{Action: "dns_provider_create"}, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, audits, 1)
|
||||
|
||||
// Test filter by event category
|
||||
audits, total, err = svc.ListAuditLogs(AuditLogFilter{EventCategory: "dns_provider"}, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, audits, 3)
|
||||
|
||||
// Test pagination
|
||||
audits, total, err = svc.ListAuditLogs(AuditLogFilter{}, 1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, audits, 2)
|
||||
|
||||
// Second page
|
||||
audits, total, err = svc.ListAuditLogs(AuditLogFilter{}, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, audits, 1)
|
||||
}
|
||||
|
||||
func TestSecurityService_GetAuditLogByUUID(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Create test audit log
|
||||
testAudit := models.SecurityAudit{
|
||||
UUID: "audit-test-uuid",
|
||||
Actor: "user-1",
|
||||
Action: "dns_provider_create",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceUUID: "provider-1",
|
||||
Details: `{"name":"Test Provider"}`,
|
||||
}
|
||||
err := db.Create(&testAudit).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test retrieving existing audit log
|
||||
audit, err := svc.GetAuditLogByUUID("audit-test-uuid")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, audit)
|
||||
assert.Equal(t, "audit-test-uuid", audit.UUID)
|
||||
assert.Equal(t, "user-1", audit.Actor)
|
||||
|
||||
// Test retrieving non-existent audit log
|
||||
audit, err = svc.GetAuditLogByUUID("non-existent-uuid")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, audit)
|
||||
assert.Equal(t, "audit log not found", err.Error())
|
||||
}
|
||||
|
||||
func TestSecurityService_ListAuditLogsByProvider(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
providerID := uint(123)
|
||||
otherProviderID := uint(456)
|
||||
|
||||
// Create test audit logs
|
||||
testAudits := []models.SecurityAudit{
|
||||
{
|
||||
UUID: "audit-provider-1",
|
||||
Actor: "user-1",
|
||||
Action: "dns_provider_create",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceID: &providerID,
|
||||
ResourceUUID: "provider-uuid-1",
|
||||
Details: `{"name":"Provider 1"}`,
|
||||
},
|
||||
{
|
||||
UUID: "audit-provider-2",
|
||||
Actor: "user-1",
|
||||
Action: "dns_provider_update",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceID: &providerID,
|
||||
ResourceUUID: "provider-uuid-1",
|
||||
Details: `{"changed_fields":{"name":true}}`,
|
||||
},
|
||||
{
|
||||
UUID: "audit-other-provider",
|
||||
Actor: "user-2",
|
||||
Action: "dns_provider_create",
|
||||
EventCategory: "dns_provider",
|
||||
ResourceID: &otherProviderID,
|
||||
ResourceUUID: "provider-uuid-2",
|
||||
Details: `{"name":"Other Provider"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, audit := range testAudits {
|
||||
err := db.Create(&audit).Error
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test listing audits for specific provider
|
||||
audits, total, err := svc.ListAuditLogsByProvider(providerID, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, audits, 2)
|
||||
|
||||
// Test listing audits for other provider
|
||||
audits, total, err = svc.ListAuditLogsByProvider(otherProviderID, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, audits, 1)
|
||||
|
||||
// Test pagination
|
||||
audits, total, err = svc.ListAuditLogsByProvider(providerID, 1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, audits, 1)
|
||||
}
|
||||
|
||||
func TestSecurityService_AsyncAuditLogging(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
// Log audit asynchronously
|
||||
audit := &models.SecurityAudit{
|
||||
Actor: "user-1",
|
||||
Action: "test_action",
|
||||
EventCategory: "test_category",
|
||||
Details: "test details",
|
||||
}
|
||||
err := svc.LogAudit(audit)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, audit.UUID)
|
||||
|
||||
// Give some time for async processing
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify audit was stored
|
||||
var stored models.SecurityAudit
|
||||
err = db.Where("uuid = ?", audit.UUID).First(&stored).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test_action", stored.Action)
|
||||
}
|
||||
|
||||
// TestSecurityService_ListAuditLogs_EdgeCases tests edge cases for audit log listing.
|
||||
func TestSecurityService_ListAuditLogs_EdgeCases(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
t.Run("list audits with no data returns empty", func(t *testing.T) {
|
||||
audits, total, err := svc.ListAuditLogs(AuditLogFilter{}, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), total)
|
||||
assert.Empty(t, audits)
|
||||
})
|
||||
|
||||
t.Run("list audits with time range filter", func(t *testing.T) {
|
||||
// Create audits with specific timestamps
|
||||
now := time.Now()
|
||||
oldAudit := models.SecurityAudit{
|
||||
UUID: "old-audit",
|
||||
Actor: "user-1",
|
||||
Action: "old_action",
|
||||
CreatedAt: now.Add(-48 * time.Hour),
|
||||
}
|
||||
newAudit := models.SecurityAudit{
|
||||
UUID: "new-audit",
|
||||
Actor: "user-1",
|
||||
Action: "new_action",
|
||||
CreatedAt: now.Add(-1 * time.Hour),
|
||||
}
|
||||
assert.NoError(t, db.Create(&oldAudit).Error)
|
||||
assert.NoError(t, db.Create(&newAudit).Error)
|
||||
|
||||
// Filter by time range - last 24 hours
|
||||
startDate := now.Add(-24 * time.Hour)
|
||||
endDate := now
|
||||
filter := AuditLogFilter{
|
||||
StartDate: &startDate,
|
||||
EndDate: &endDate,
|
||||
}
|
||||
|
||||
audits, total, err := svc.ListAuditLogs(filter, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, audits, 1)
|
||||
assert.Equal(t, "new-audit", audits[0].UUID)
|
||||
})
|
||||
|
||||
t.Run("list audits with combined filters", func(t *testing.T) {
|
||||
// Create diverse audits
|
||||
audits := []models.SecurityAudit{
|
||||
{UUID: "audit-a", Actor: "user-1", Action: "create", EventCategory: "provider"},
|
||||
{UUID: "audit-b", Actor: "user-2", Action: "update", EventCategory: "provider"},
|
||||
{UUID: "audit-c", Actor: "user-1", Action: "delete", EventCategory: "host"},
|
||||
}
|
||||
for _, a := range audits {
|
||||
assert.NoError(t, db.Create(&a).Error)
|
||||
}
|
||||
|
||||
// Filter by actor AND event category
|
||||
filter := AuditLogFilter{
|
||||
Actor: "user-1",
|
||||
EventCategory: "provider",
|
||||
}
|
||||
|
||||
results, total, err := svc.ListAuditLogs(filter, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, "audit-a", results[0].UUID)
|
||||
})
|
||||
|
||||
t.Run("list audits handles zero page", func(t *testing.T) {
|
||||
audit := models.SecurityAudit{UUID: "test", Actor: "user", Action: "test"}
|
||||
assert.NoError(t, db.Create(&audit).Error)
|
||||
|
||||
// Page 0 should default to page 1
|
||||
audits, total, err := svc.ListAuditLogs(AuditLogFilter{}, 0, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, total, int64(0))
|
||||
assert.NotEmpty(t, audits)
|
||||
})
|
||||
|
||||
t.Run("list audits with very large limit", func(t *testing.T) {
|
||||
// Should handle large limits gracefully
|
||||
audits, total, err := svc.ListAuditLogs(AuditLogFilter{}, 1, 10000)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(0))
|
||||
_ = audits
|
||||
})
|
||||
}
|
||||
|
||||
// TestSecurityService_ListAuditLogsByProvider_EdgeCases tests edge cases for provider audit logs.
|
||||
func TestSecurityService_ListAuditLogsByProvider_EdgeCases(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
defer svc.Close()
|
||||
|
||||
t.Run("list audits for non-existent provider returns empty", func(t *testing.T) {
|
||||
audits, total, err := svc.ListAuditLogsByProvider(99999, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), total)
|
||||
assert.Empty(t, audits)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSecurityService_GenerateBreakGlassToken_EdgeCases tests token generation edge cases.
|
||||
func TestSecurityService_GenerateBreakGlassToken_EdgeCases(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
defer svc.Close()
|
||||
|
||||
t.Run("generated tokens are different on regeneration", func(t *testing.T) {
|
||||
token1, err := svc.GenerateBreakGlassToken("test-config")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token1)
|
||||
|
||||
// Sleep a moment to ensure different token generated
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
token2, err := svc.GenerateBreakGlassToken("test-config")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token2)
|
||||
|
||||
// The tokens themselves should be different (even though they update the same config)
|
||||
assert.NotEqual(t, token1, token2, "Regenerated tokens should be different")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSecurityService_Flush_EdgeCases tests flush functionality.
|
||||
func TestSecurityService_Flush_EdgeCases(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
t.Run("flush with empty channel completes quickly", func(t *testing.T) {
|
||||
start := time.Now()
|
||||
svc.Flush()
|
||||
duration := time.Since(start)
|
||||
|
||||
// Should complete in less than 100ms when channel is empty
|
||||
assert.Less(t, duration, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("flush waits for pending audits", func(t *testing.T) {
|
||||
// Log multiple audits
|
||||
for i := 0; i < 5; i++ {
|
||||
audit := &models.SecurityAudit{
|
||||
Actor: "user-1",
|
||||
Action: "test_action",
|
||||
}
|
||||
_ = svc.LogAudit(audit)
|
||||
}
|
||||
|
||||
// Flush should wait for them
|
||||
svc.Flush()
|
||||
|
||||
// Verify all were processed
|
||||
var count int64
|
||||
db.Model(&models.SecurityAudit{}).Count(&count)
|
||||
assert.Equal(t, int64(5), count)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSecurityService_Get_Singleton tests Get behavior with multiple configs.
|
||||
func TestSecurityService_Get_Singleton(t *testing.T) {
|
||||
t.Run("get returns error when no config exists", func(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
defer svc.Close()
|
||||
|
||||
_, err := svc.Get()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrSecurityConfigNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("get returns first config when no default", func(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
defer svc.Close()
|
||||
|
||||
// Create only non-default config
|
||||
cfg := &models.SecurityConfig{Name: "custom", Enabled: false}
|
||||
assert.NoError(t, svc.Upsert(cfg))
|
||||
|
||||
// Should fall back to first config
|
||||
got, err := svc.Get()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "custom", got.Name)
|
||||
})
|
||||
|
||||
t.Run("get returns default config when exists", func(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
defer svc.Close()
|
||||
|
||||
// Create default config
|
||||
defaultCfg := &models.SecurityConfig{Name: "default", Enabled: true}
|
||||
assert.NoError(t, svc.Upsert(defaultCfg))
|
||||
|
||||
// Get should return "default" config
|
||||
got, err := svc.Get()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "default", got.Name)
|
||||
assert.True(t, got.Enabled)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSecurityService_ListRuleSets_EdgeCases tests rule set listing edge cases.
|
||||
func TestSecurityService_ListRuleSets_EdgeCases(t *testing.T) {
|
||||
db := setupSecurityTestDB(t)
|
||||
svc := NewSecurityService(db)
|
||||
|
||||
t.Run("list rulesets with no data returns empty", func(t *testing.T) {
|
||||
rulesets, err := svc.ListRuleSets()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, rulesets)
|
||||
})
|
||||
|
||||
t.Run("list rulesets handles pagination", func(t *testing.T) {
|
||||
// Create multiple rulesets
|
||||
for i := 0; i < 5; i++ {
|
||||
rs := &models.SecurityRuleSet{
|
||||
Name: fmt.Sprintf("ruleset-%d", i),
|
||||
Content: "rule",
|
||||
}
|
||||
assert.NoError(t, svc.UpsertRuleSet(rs))
|
||||
}
|
||||
|
||||
// List should return all
|
||||
rulesets, err := svc.ListRuleSets()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, rulesets, 5)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user