chore: git cache cleanup
This commit is contained in:
240
backend/internal/database/settings_query_test.go
Normal file
240
backend/internal/database/settings_query_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user