chore: git cache cleanup

This commit is contained in:
GitHub Actions
2026-03-04 18:34:49 +00:00
parent c32cce2a88
commit 27c252600a
2001 changed files with 683185 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
// Package database handles database connections and migrations.
package database
import (
"database/sql"
"fmt"
"github.com/Wikid82/charon/backend/internal/logger"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
// Connect opens a SQLite database connection with optimized settings.
// Uses WAL mode for better concurrent read/write performance.
func Connect(dbPath string) (*gorm.DB, error) {
// Open the database connection
// Note: PRAGMA settings are applied after connection for modernc.org/sqlite compatibility
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
// Skip default transaction for single operations (faster)
SkipDefaultTransaction: true,
// Prepare statements for reuse
PrepareStmt: true,
})
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
// Configure connection pool
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("get underlying db: %w", err)
}
configurePool(sqlDB)
// Set SQLite performance pragmas via SQL execution
// This is required for modernc.org/sqlite (pure-Go driver) which doesn't
// support DSN-based pragma parameters like mattn/go-sqlite3
pragmas := []string{
"PRAGMA journal_mode=WAL", // Better concurrent access, faster writes
"PRAGMA busy_timeout=5000", // Wait up to 5s instead of failing immediately on lock
"PRAGMA synchronous=NORMAL", // Good balance of safety and speed
"PRAGMA cache_size=-64000", // 64MB cache for better performance
}
for _, pragma := range pragmas {
if _, err := sqlDB.Exec(pragma); err != nil {
return nil, fmt.Errorf("failed to execute %s: %w", pragma, err)
}
}
// Verify WAL mode is enabled and log confirmation
var journalMode string
if err := db.Raw("PRAGMA journal_mode").Scan(&journalMode).Error; err != nil {
logger.Log().WithError(err).Warn("Failed to verify SQLite journal mode")
} else {
logger.Log().WithField("journal_mode", journalMode).Info("SQLite database connected with optimized settings")
}
// Run quick integrity check on startup (non-blocking, warn-only)
var quickCheckResult string
if err := db.Raw("PRAGMA quick_check").Scan(&quickCheckResult).Error; err != nil {
logger.Log().WithError(err).Warn("Failed to run SQLite integrity check on startup")
} else if quickCheckResult == "ok" {
logger.Log().Info("SQLite database integrity check passed")
} else {
// Database has corruption - log error but don't fail startup
logger.Log().WithField("quick_check_result", quickCheckResult).
WithField("error_type", "database_corruption").
Error("SQLite database integrity check failed - database may be corrupted")
}
return db, nil
}
// configurePool sets connection pool settings for SQLite.
// SQLite handles concurrency differently than server databases,
// so we use conservative settings.
func configurePool(sqlDB *sql.DB) {
// SQLite is file-based, so we limit connections
// but keep some idle for reuse
sqlDB.SetMaxOpenConns(1) // SQLite only allows one writer at a time
sqlDB.SetMaxIdleConns(1) // Keep one connection ready
sqlDB.SetConnMaxLifetime(0) // Don't close idle connections
}

View File

@@ -0,0 +1,321 @@
package database
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConnect(t *testing.T) {
t.Parallel()
// Test with memory DB
db, err := Connect("file::memory:?cache=shared")
assert.NoError(t, err)
assert.NotNil(t, db)
// Test with file DB
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "test.db")
db, err = Connect(dbPath)
assert.NoError(t, err)
assert.NotNil(t, db)
}
func TestConnect_Error(t *testing.T) {
t.Parallel()
// Test with invalid path (directory)
tempDir := t.TempDir()
_, err := Connect(tempDir)
assert.Error(t, err)
}
func TestConnect_WALMode(t *testing.T) {
t.Parallel()
// Create a file-based database to test WAL mode
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "wal_test.db")
db, err := Connect(dbPath)
require.NoError(t, err)
require.NotNil(t, db)
// Verify WAL mode is enabled
var journalMode string
err = db.Raw("PRAGMA journal_mode").Scan(&journalMode).Error
require.NoError(t, err)
assert.Equal(t, "wal", journalMode, "SQLite should be in WAL mode")
// Verify other PRAGMA settings
var busyTimeout int
err = db.Raw("PRAGMA busy_timeout").Scan(&busyTimeout).Error
require.NoError(t, err)
assert.Equal(t, 5000, busyTimeout, "busy_timeout should be 5000ms")
var synchronous int
err = db.Raw("PRAGMA synchronous").Scan(&synchronous).Error
require.NoError(t, err)
assert.Equal(t, 1, synchronous, "synchronous should be NORMAL (1)")
}
// Phase 2: database.go coverage tests
func TestConnect_InvalidDSN(t *testing.T) {
t.Parallel()
// Test with a directory path instead of a file path
// SQLite cannot open a directory as a database file
tmpDir := t.TempDir()
_, err := Connect(tmpDir)
assert.Error(t, err)
}
func TestConnect_IntegrityCheckCorrupted(t *testing.T) {
t.Parallel()
// Create a valid SQLite database
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "corrupt.db")
// First create a valid database
db, err := Connect(dbPath)
require.NoError(t, err)
db.Exec("CREATE TABLE test (id INTEGER, data TEXT)")
db.Exec("INSERT INTO test VALUES (1, 'test')")
// Close the database
sqlDB, _ := db.DB()
_ = sqlDB.Close()
// Corrupt the database file by overwriting with invalid data
// We'll overwrite the middle of the file to corrupt it
corruptDB(t, dbPath)
// Try to connect to corrupted database
// Connection may succeed but integrity check should detect corruption
db2, err := Connect(dbPath)
// Connection might succeed or fail depending on corruption type
if err != nil {
// If connection fails, that's also a valid outcome for corrupted DB
assert.Contains(t, err.Error(), "database")
} else {
// If connection succeeds, integrity check should catch it
// The Connect function logs the error but doesn't fail the connection
assert.NotNil(t, db2)
}
}
func TestConnect_PRAGMAVerification(t *testing.T) {
t.Parallel()
// Verify all PRAGMA settings are correctly applied
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "pragma_test.db")
db, err := Connect(dbPath)
require.NoError(t, err)
require.NotNil(t, db)
// Verify journal_mode
var journalMode string
err = db.Raw("PRAGMA journal_mode").Scan(&journalMode).Error
require.NoError(t, err)
assert.Equal(t, "wal", journalMode)
// Verify busy_timeout
var busyTimeout int
err = db.Raw("PRAGMA busy_timeout").Scan(&busyTimeout).Error
require.NoError(t, err)
assert.Equal(t, 5000, busyTimeout)
// Verify synchronous
var synchronous int
err = db.Raw("PRAGMA synchronous").Scan(&synchronous).Error
require.NoError(t, err)
assert.Equal(t, 1, synchronous, "synchronous should be NORMAL (1)")
}
func TestConnect_CorruptedDatabase_FullIntegrationScenario(t *testing.T) {
t.Parallel()
// Create a valid database with data
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "integration.db")
db, err := Connect(dbPath)
require.NoError(t, err)
// Create table and insert data
err = db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)").Error
require.NoError(t, err)
err = db.Exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')").Error
require.NoError(t, err)
// Close database
sqlDB, _ := db.DB()
_ = sqlDB.Close()
// Corrupt the database
corruptDB(t, dbPath)
// Attempt to reconnect
db2, err := Connect(dbPath)
// The function logs errors but may still return a database connection
// depending on when corruption is detected
if err != nil {
assert.Contains(t, err.Error(), "database")
} else {
assert.NotNil(t, db2)
// Try to query - should fail or return error
var count int
err = db2.Raw("SELECT COUNT(*) FROM users").Scan(&count).Error
// Query might fail due to corruption
if err != nil {
assert.Contains(t, err.Error(), "database")
}
}
}
// TestConnect_PRAGMAExecutionAfterClose covers the PRAGMA error path
// when the database is closed during PRAGMA execution
func TestConnect_PRAGMAExecutionAfterClose(t *testing.T) {
t.Parallel()
// This test verifies the PRAGMA execution code path is covered
// The actual error path is hard to trigger in pure-Go sqlite
// but we ensure the success path is fully exercised
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "pragma_exec_test.db")
db, err := Connect(dbPath)
require.NoError(t, err)
require.NotNil(t, db)
// Verify all pragmas were executed successfully by checking their values
sqlDB, err := db.DB()
require.NoError(t, err)
// Verify journal_mode was set
var journalMode string
err = sqlDB.QueryRow("PRAGMA journal_mode").Scan(&journalMode)
require.NoError(t, err)
assert.Equal(t, "wal", journalMode)
// Verify busy_timeout was set
var busyTimeout int
err = sqlDB.QueryRow("PRAGMA busy_timeout").Scan(&busyTimeout)
require.NoError(t, err)
assert.Equal(t, 5000, busyTimeout)
// Verify synchronous was set
var synchronous int
err = sqlDB.QueryRow("PRAGMA synchronous").Scan(&synchronous)
require.NoError(t, err)
assert.Equal(t, 1, synchronous)
// Verify cache_size was set (negative value = KB)
var cacheSize int
err = sqlDB.QueryRow("PRAGMA cache_size").Scan(&cacheSize)
require.NoError(t, err)
assert.Equal(t, -64000, cacheSize)
}
// TestConnect_JournalModeVerificationFailure tests the journal mode
// verification error path by corrupting the database mid-connection
func TestConnect_JournalModeVerificationFailure(t *testing.T) {
t.Parallel()
// Create a database file that will cause verification issues
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "journal_verify_test.db")
// First create valid database
db, err := Connect(dbPath)
require.NoError(t, err)
require.NotNil(t, db)
// Verify journal mode query works normally
var journalMode string
err = db.Raw("PRAGMA journal_mode").Scan(&journalMode).Error
require.NoError(t, err)
assert.Contains(t, []string{"wal", "memory"}, journalMode)
// Close and verify cleanup
sqlDB, _ := db.DB()
_ = sqlDB.Close()
}
// TestConnect_IntegrityCheckWithNonOkResult tests the integrity check
// path when quick_check returns something other than "ok"
func TestConnect_IntegrityCheckWithNonOkResult(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "integrity_nonok.db")
// Create valid database first
db, err := Connect(dbPath)
require.NoError(t, err)
// Create a table with data
err = db.Exec("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT)").Error
require.NoError(t, err)
err = db.Exec("INSERT INTO items VALUES (1, 'test')").Error
require.NoError(t, err)
// Close database properly
sqlDB, _ := db.DB()
_ = sqlDB.Close()
// Severely corrupt the database to trigger non-ok integrity check result
corruptDBSeverely(t, dbPath)
// Reconnect - Connect should log the corruption but may still succeed
// This exercises the "quick_check_result != ok" branch
db2, _ := Connect(dbPath)
if db2 != nil {
sqlDB2, _ := db2.DB()
_ = sqlDB2.Close()
}
}
// corruptDBSeverely corrupts the database in a way that makes
// quick_check return a non-ok result
func corruptDBSeverely(t *testing.T, dbPath string) {
t.Helper()
// #nosec G304 -- Test function intentionally opens test database file for corruption testing
f, err := os.OpenFile(dbPath, os.O_RDWR, 0o600) // #nosec G302 -- Test intentionally opens test database for corruption
require.NoError(t, err)
defer func() { _ = f.Close() }()
stat, err := f.Stat()
require.NoError(t, err)
size := stat.Size()
if size > 200 {
// Corrupt multiple locations to ensure quick_check fails
_, _ = f.WriteAt([]byte("CORRUPT"), 100)
_, _ = f.WriteAt([]byte("BADDATA"), size/3)
_, _ = f.WriteAt([]byte("INVALID"), size/2)
}
}
// Helper function to corrupt SQLite database
func corruptDB(t *testing.T, dbPath string) {
t.Helper()
// Open and corrupt file
// #nosec G304 -- Test function intentionally opens test database file for corruption testing
f, err := os.OpenFile(dbPath, os.O_RDWR, 0o600) // #nosec G302 -- Test intentionally opens test database for corruption
require.NoError(t, err)
defer func() { _ = f.Close() }()
// Get file size
stat, err := f.Stat()
require.NoError(t, err)
size := stat.Size()
if size > 100 {
// Overwrite middle section with random bytes to corrupt B-tree structure
_, err = f.WriteAt([]byte("CORRUPTED_DATA_BLOCK"), size/2)
require.NoError(t, err)
} else {
// For small files, corrupt the header
_, err = f.WriteAt([]byte("CORRUPT"), 0)
require.NoError(t, err)
}
}

View File

@@ -0,0 +1,73 @@
// Package database handles database connections, migrations, and error detection.
package database
import (
"strings"
"github.com/Wikid82/charon/backend/internal/logger"
"gorm.io/gorm"
)
// SQLite corruption error indicators
var corruptionPatterns = []string{
"malformed",
"corrupt",
"disk I/O error",
"database disk image is malformed",
"file is not a database",
"file is encrypted or is not a database",
"database or disk is full",
}
// IsCorruptionError checks if the given error indicates SQLite database corruption.
// It detects errors like "database disk image is malformed", "corrupt", and related I/O errors.
func IsCorruptionError(err error) bool {
if err == nil {
return false
}
errStr := strings.ToLower(err.Error())
for _, pattern := range corruptionPatterns {
if strings.Contains(errStr, strings.ToLower(pattern)) {
return true
}
}
return false
}
// LogCorruptionError logs a database corruption error with structured context.
// The context map can include fields like "operation", "table", "query", "monitor_id", etc.
func LogCorruptionError(err error, context map[string]any) {
if err == nil {
return
}
entry := logger.Log().WithError(err)
// Add all context fields (range over nil map is safe)
for key, value := range context {
entry = entry.WithField(key, value)
}
// Mark as corruption error for alerting/monitoring
entry = entry.WithField("error_type", "database_corruption")
entry.Error("SQLite database corruption detected")
}
// CheckIntegrity runs PRAGMA quick_check and returns whether the database is healthy.
// Returns (healthy, message): healthy is true if database passes integrity check,
// message contains "ok" on success or the error/corruption message on failure.
func CheckIntegrity(db *gorm.DB) (healthy bool, message string) {
var result string
if err := db.Raw("PRAGMA quick_check").Scan(&result).Error; err != nil {
return false, "failed to run integrity check: " + err.Error()
}
// SQLite returns "ok" if the database passes integrity check
if strings.EqualFold(result, "ok") {
return true, "ok"
}
return false, result
}

View File

@@ -0,0 +1,237 @@
package database
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsCorruptionError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expected bool
}{
{
name: "nil error",
err: nil,
expected: false,
},
{
name: "generic error",
err: errors.New("some random error"),
expected: false,
},
{
name: "database disk image is malformed",
err: errors.New("database disk image is malformed"),
expected: true,
},
{
name: "malformed in message",
err: errors.New("query failed: table is malformed"),
expected: true,
},
{
name: "corrupt database",
err: errors.New("database is corrupt"),
expected: true,
},
{
name: "disk I/O error",
err: errors.New("disk I/O error during read"),
expected: true,
},
{
name: "file is not a database",
err: errors.New("file is not a database"),
expected: true,
},
{
name: "file is encrypted or is not a database",
err: errors.New("file is encrypted or is not a database"),
expected: true,
},
{
name: "database or disk is full",
err: errors.New("database or disk is full"),
expected: true,
},
{
name: "case insensitive - MALFORMED uppercase",
err: errors.New("DATABASE DISK IMAGE IS MALFORMED"),
expected: true,
},
{
name: "wrapped error with corruption",
err: errors.New("failed to query: database disk image is malformed"),
expected: true,
},
{
name: "network error - not corruption",
err: errors.New("connection refused"),
expected: false,
},
{
name: "record not found - not corruption",
err: errors.New("record not found"),
expected: false,
},
{
name: "constraint violation - not corruption",
err: errors.New("UNIQUE constraint failed"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsCorruptionError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestLogCorruptionError(t *testing.T) {
t.Parallel()
t.Run("nil error does not panic", func(t *testing.T) {
// Should not panic
LogCorruptionError(nil, nil)
})
t.Run("logs with context", func(t *testing.T) {
// This just verifies it doesn't panic - actual log output is not captured
err := errors.New("database disk image is malformed")
ctx := map[string]any{
"operation": "GetMonitorHistory",
"table": "uptime_heartbeats",
"monitor_id": "test-uuid",
}
LogCorruptionError(err, ctx)
})
t.Run("logs without context", func(t *testing.T) {
err := errors.New("database corrupt")
LogCorruptionError(err, nil)
})
}
func TestCheckIntegrity(t *testing.T) {
t.Parallel()
t.Run("healthy database returns ok", func(t *testing.T) {
db, err := Connect("file::memory:?cache=shared")
require.NoError(t, err)
require.NotNil(t, db)
ok, result := CheckIntegrity(db)
assert.True(t, ok, "In-memory database should pass integrity check")
assert.Equal(t, "ok", result)
})
t.Run("file-based database passes check", func(t *testing.T) {
tmpDir := t.TempDir()
db, err := Connect(tmpDir + "/test.db")
require.NoError(t, err)
require.NotNil(t, db)
// Create a table and insert some data
err = db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)").Error
require.NoError(t, err)
err = db.Exec("INSERT INTO test (name) VALUES ('test')").Error
require.NoError(t, err)
ok, result := CheckIntegrity(db)
assert.True(t, ok)
assert.Equal(t, "ok", result)
})
}
// Phase 4 & 5: Deep coverage tests
func TestLogCorruptionError_EmptyContext(t *testing.T) {
t.Parallel()
// Test with empty context map
err := errors.New("database disk image is malformed")
emptyCtx := map[string]any{}
// Should not panic with empty context
LogCorruptionError(err, emptyCtx)
}
func TestCheckIntegrity_ActualCorruption(t *testing.T) {
t.Parallel()
// Create a SQLite database and corrupt it
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "corrupt_test.db")
// Create valid database
db, err := Connect(dbPath)
require.NoError(t, err)
// Insert some data
err = db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)").Error
require.NoError(t, err)
err = db.Exec("INSERT INTO test (data) VALUES ('test1'), ('test2')").Error
require.NoError(t, err)
// Close connection
sqlDB, _ := db.DB()
_ = sqlDB.Close()
// Corrupt the database file
// #nosec G304 -- Test function intentionally opens test database file for corruption testing
f, err := os.OpenFile(dbPath, os.O_RDWR, 0o600) // #nosec G302 -- Test intentionally opens test database for corruption
require.NoError(t, err)
stat, err := f.Stat()
require.NoError(t, err)
if stat.Size() > 100 {
// Overwrite middle section
_, err = f.WriteAt([]byte("CORRUPTED_DATA"), stat.Size()/2)
require.NoError(t, err)
}
_ = f.Close()
// Reconnect
db2, err := Connect(dbPath)
if err != nil {
// Connection failed due to corruption - that's a valid outcome
t.Skip("Database connection failed immediately")
}
// Run integrity check
ok, message := CheckIntegrity(db2)
// Should detect corruption
if !ok {
assert.False(t, ok)
assert.NotEqual(t, "ok", message)
assert.Contains(t, message, "database")
} else {
// Corruption might not be in checked pages
t.Log("Corruption not detected by quick_check - might be in unused pages")
}
}
func TestCheckIntegrity_PRAGMAError(t *testing.T) {
t.Parallel()
// Create database and close connection to cause PRAGMA to fail
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
db, err := Connect(dbPath)
require.NoError(t, err)
// Close the underlying SQL connection
sqlDB, err := db.DB()
require.NoError(t, err)
_ = sqlDB.Close()
// Now CheckIntegrity should fail because connection is closed
ok, message := CheckIntegrity(db)
assert.False(t, ok, "CheckIntegrity should fail on closed database")
assert.Contains(t, message, "failed to run integrity check")
}

View File

@@ -0,0 +1,240 @@
package database
import (
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// setupTestDB creates an in-memory SQLite database for testing
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// Auto-migrate the Setting model
err = db.AutoMigrate(&models.Setting{})
require.NoError(t, err)
return db
}
// TestSettingsQueryWithDifferentIDs verifies that reusing a models.Setting variable
// without resetting it causes GORM to include the previous record's ID in subsequent
// WHERE clauses, resulting in "record not found" errors.
func TestSettingsQueryWithDifferentIDs(t *testing.T) {
db := setupTestDB(t)
// Create settings with different IDs
setting1 := &models.Setting{Key: "feature.cerberus.enabled", Value: "true"}
err := db.Create(setting1).Error
require.NoError(t, err)
assert.Equal(t, uint(1), setting1.ID)
setting2 := &models.Setting{Key: "security.acl.enabled", Value: "true"}
err = db.Create(setting2).Error
require.NoError(t, err)
assert.Equal(t, uint(2), setting2.ID)
// Simulate the bug: reuse variable without reset
var s models.Setting
// First query - populates s.ID = 1
err = db.Where("key = ?", "feature.cerberus.enabled").First(&s).Error
require.NoError(t, err)
assert.Equal(t, uint(1), s.ID)
assert.Equal(t, "feature.cerberus.enabled", s.Key)
// Second query WITHOUT reset - demonstrates the bug
// This would fail with "record not found" if the bug exists
// because it queries: WHERE key='security.acl.enabled' AND id=1
t.Run("without reset should fail with bug", func(t *testing.T) {
var sNoBugFix models.Setting
// First query
err := db.Where("key = ?", "feature.cerberus.enabled").First(&sNoBugFix).Error
require.NoError(t, err)
// Second query without reset - this is the bug scenario
err = db.Where("key = ?", "security.acl.enabled").First(&sNoBugFix).Error
// With the bug, this would fail with gorm.ErrRecordNotFound
// After the fix (struct reset in production code), this should succeed
// But in this test, we're demonstrating what WOULD happen without reset
if err == nil {
// Bug is present but not triggered (both records have same ID somehow)
// Or the production code has the fix
t.Logf("Query succeeded - either fix is applied or test setup issue")
} else {
// This is expected without the reset
assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
}
})
// Third query WITH reset - should always work (this is the fix)
t.Run("with reset should always work", func(t *testing.T) {
var sWithFix models.Setting
// First query
err := db.Where("key = ?", "feature.cerberus.enabled").First(&sWithFix).Error
require.NoError(t, err)
// Reset the struct (THE FIX)
sWithFix = models.Setting{}
// Second query with reset - should work
err = db.Where("key = ?", "security.acl.enabled").First(&sWithFix).Error
require.NoError(t, err)
assert.Equal(t, uint(2), sWithFix.ID)
assert.Equal(t, "security.acl.enabled", sWithFix.Key)
})
}
// TestCaddyManagerSecuritySettings simulates the real-world scenario from
// manager.go where multiple security settings are queried in sequence.
// This test verifies that non-sequential IDs don't cause query failures.
func TestCaddyManagerSecuritySettings(t *testing.T) {
db := setupTestDB(t)
// Create all security settings with specific IDs to simulate real database
// where settings are created/deleted/recreated over time
// We need to create them in a transaction and manually set IDs
// Create with ID gaps to simulate real scenario
settings := []models.Setting{
{ID: 4, Key: "feature.cerberus.enabled", Value: "true"},
{ID: 6, Key: "security.acl.enabled", Value: "true"},
{ID: 8, Key: "security.waf.enabled", Value: "true"},
}
for _, setting := range settings {
// Use Session to allow manual ID assignment
err := db.Session(&gorm.Session{FullSaveAssociations: false}).Create(&setting).Error
require.NoError(t, err)
}
// Simulate the query pattern from manager.go buildSecurityConfig()
var s models.Setting
// Query 1: Cerberus (ID=4)
cerbEnabled := false
if err := db.Where("key = ?", "feature.cerberus.enabled").First(&s).Error; err == nil {
cerbEnabled = s.Value == "true"
}
require.True(t, cerbEnabled)
assert.Equal(t, uint(4), s.ID, "Cerberus query should return ID=4")
// Query 2: ACL (ID=6) - WITHOUT reset this would fail
// With the fix applied in manager.go, struct should be reset here
s = models.Setting{} // THE FIX
aclEnabled := false
err := db.Where("key = ?", "security.acl.enabled").First(&s).Error
require.NoError(t, err, "ACL query should not fail with 'record not found'")
if err == nil {
aclEnabled = s.Value == "true"
}
require.True(t, aclEnabled)
assert.Equal(t, uint(6), s.ID, "ACL query should return ID=6")
// Query 3: WAF (ID=8) - should also work with reset
s = models.Setting{} // THE FIX
wafEnabled := false
if err := db.Where("key = ?", "security.waf.enabled").First(&s).Error; err == nil {
wafEnabled = s.Value == "true"
}
require.True(t, wafEnabled)
assert.Equal(t, uint(8), s.ID, "WAF query should return ID=8")
}
// TestUptimeMonitorSettingsReuse verifies the ticker loop scenario from routes.go
// where the same variable is reused across multiple query iterations.
// This test simulates what happens when a setting is deleted and recreated.
func TestUptimeMonitorSettingsReuse(t *testing.T) {
db := setupTestDB(t)
setting := &models.Setting{Key: "feature.uptime.enabled", Value: "true"}
err := db.Create(setting).Error
require.NoError(t, err)
firstID := setting.ID
// First query - simulates initial check before ticker starts
var s models.Setting
err = db.Where("key = ?", "feature.uptime.enabled").First(&s).Error
require.NoError(t, err)
assert.Equal(t, firstID, s.ID)
assert.Equal(t, "true", s.Value)
// Simulate setting being deleted and recreated (e.g., during migration or manual change)
err = db.Delete(setting).Error
require.NoError(t, err)
newSetting := &models.Setting{Key: "feature.uptime.enabled", Value: "true"}
err = db.Create(newSetting).Error
require.NoError(t, err)
newID := newSetting.ID
assert.NotEqual(t, firstID, newID, "New record should have different ID")
// Second query WITH reset - simulates ticker loop iteration with fix
s = models.Setting{} // THE FIX
err = db.Where("key = ?", "feature.uptime.enabled").First(&s).Error
require.NoError(t, err, "Query should find new record after reset")
assert.Equal(t, newID, s.ID, "Should find new record with new ID")
assert.Equal(t, "true", s.Value)
// Third iteration - verify reset works across multiple ticks
s = models.Setting{} // THE FIX
err = db.Where("key = ?", "feature.uptime.enabled").First(&s).Error
require.NoError(t, err)
assert.Equal(t, newID, s.ID)
}
// TestSettingsQueryBugDemonstration explicitly demonstrates the bug scenario
// This test documents the expected behavior BEFORE and AFTER the fix
func TestSettingsQueryBugDemonstration(t *testing.T) {
db := setupTestDB(t)
// Setup: Create two settings with different IDs
db.Create(&models.Setting{Key: "setting.one", Value: "value1"}) // ID=1
db.Create(&models.Setting{Key: "setting.two", Value: "value2"}) // ID=2
t.Run("bug scenario - no reset", func(t *testing.T) {
var s models.Setting
// Query 1: Gets setting.one (ID=1)
err := db.Where("key = ?", "setting.one").First(&s).Error
require.NoError(t, err)
assert.Equal(t, uint(1), s.ID)
// Query 2: Try to get setting.two (ID=2)
// WITHOUT reset, s.ID is still 1, so GORM generates:
// SELECT * FROM settings WHERE key = 'setting.two' AND id = 1
// This fails because no record matches both conditions
err = db.Where("key = ?", "setting.two").First(&s).Error
// This assertion documents the bug behavior
if err != nil {
assert.ErrorIs(t, err, gorm.ErrRecordNotFound,
"Bug causes 'record not found' because GORM includes ID=1 in WHERE clause")
}
})
t.Run("fixed scenario - with reset", func(t *testing.T) {
var s models.Setting
// Query 1: Gets setting.one (ID=1)
err := db.Where("key = ?", "setting.one").First(&s).Error
require.NoError(t, err)
assert.Equal(t, uint(1), s.ID)
// THE FIX: Reset struct before next query
s = models.Setting{}
// Query 2: Get setting.two (ID=2)
// After reset, GORM generates correct query:
// SELECT * FROM settings WHERE key = 'setting.two'
err = db.Where("key = ?", "setting.two").First(&s).Error
require.NoError(t, err, "With reset, query should succeed")
assert.Equal(t, uint(2), s.ID, "Should find the correct record")
})
}