241 lines
8.5 KiB
Go
241 lines
8.5 KiB
Go
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")
|
|
})
|
|
}
|