Add SQLite persistence and write-through gRPC mutations
Database (internal/db) stores listeners, routes, and firewall rules with WAL mode, foreign keys, and idempotent migrations. First run seeds from TOML config; subsequent runs load from DB as source of truth. gRPC admin API now writes to the database before updating in-memory state (write-through cache pattern). Adds snapshot command for VACUUM INTO backups. Refactors firewall.New to accept raw rule slices instead of config struct for flexibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@ func TestLoadValid(t *testing.T) {
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
data := `
|
||||
[database]
|
||||
path = "/tmp/test.db"
|
||||
|
||||
[[listeners]]
|
||||
addr = ":443"
|
||||
|
||||
@@ -49,11 +52,37 @@ level = "info"
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNoListeners(t *testing.T) {
|
||||
func TestLoadNoDatabasePath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
data := `
|
||||
[[listeners]]
|
||||
addr = ":443"
|
||||
|
||||
[[listeners.routes]]
|
||||
hostname = "example.com"
|
||||
backend = "127.0.0.1:8443"
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing database path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNoListenersValid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
// No listeners is valid — DB may already have them.
|
||||
data := `
|
||||
[database]
|
||||
path = "/tmp/test.db"
|
||||
|
||||
[log]
|
||||
level = "info"
|
||||
`
|
||||
@@ -62,26 +91,8 @@ level = "info"
|
||||
}
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing listeners")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNoRoutes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
data := `
|
||||
[[listeners]]
|
||||
addr = ":443"
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing routes")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +101,9 @@ func TestLoadDuplicateHostnames(t *testing.T) {
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
data := `
|
||||
[database]
|
||||
path = "/tmp/test.db"
|
||||
|
||||
[[listeners]]
|
||||
addr = ":443"
|
||||
|
||||
@@ -116,6 +130,9 @@ func TestLoadGeoIPRequiredWithCountries(t *testing.T) {
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
data := `
|
||||
[database]
|
||||
path = "/tmp/test.db"
|
||||
|
||||
[[listeners]]
|
||||
addr = ":443"
|
||||
|
||||
@@ -141,6 +158,9 @@ func TestLoadMultipleListeners(t *testing.T) {
|
||||
path := filepath.Join(dir, "test.toml")
|
||||
|
||||
data := `
|
||||
[database]
|
||||
path = "/tmp/test.db"
|
||||
|
||||
[[listeners]]
|
||||
addr = ":443"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user