snapshot_apply_test.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"database/sql"
"testing"
"netgoat.xyz/agent/internal/database"
"netgoat.xyz/agent/internal/streaming"
)
func TestApplySnapshotReconcilesRoutesAndRulesAtomically(t *testing.T) {
db, err := database.Init(":memory:")
if err != nil {
t.Fatalf("database.Init: %v", err)
}
db.SetMaxOpenConns(1)
defer db.Close()
first := &streaming.ConfigSnapshot{
RoutesConfigured: true,
Routes: map[string]streaming.RouteData{
"old.example.test": {Type: "domain", Target: "http://127.0.0.1:9001"},
},
WAFRulesConfigured: true,
WAFRules: map[string]streaming.WAFRuleData{
"old": {Name: "old", Expression: `Path == "/old"`, Action: "BLOCK"},
},
}
if err := applySnapshotToDB(db, first); err != nil {
t.Fatalf("apply first snapshot: %v", err)
}
disabled := &streaming.ConfigSnapshot{
RoutesConfigured: true,
Routes: map[string]streaming.RouteData{
"new.example.test": {Type: "domain", Target: "http://127.0.0.1:9002"},
},
WAFRulesConfigured: true,
WAFRules: map[string]streaming.WAFRuleData{
"new": {Name: "new", Expression: `Path == "/new"`, Action: "BLOCK"},
},
ZeroTrustConfigured: true,
ZeroTrustEnabled: false,
}
if err := applySnapshotToDB(db, disabled); err != nil {
t.Fatalf("apply replacement snapshot: %v", err)
}
if _, err := database.GetRouteTargets(db, "old.example.test", "/"); err != sql.ErrNoRows {
t.Fatalf("stale route lookup error = %v, want sql.ErrNoRows", err)
}
if _, err := database.GetRouteTargets(db, "new.example.test", "/"); err != nil {
t.Fatalf("new route lookup: %v", err)
}
var oldRules, newRules int
_ = db.QueryRow(`SELECT COUNT(*) FROM waf_rules WHERE name = 'old'`).Scan(&oldRules)
_ = db.QueryRow(`SELECT COUNT(*) FROM waf_rules WHERE name = 'new'`).Scan(&newRules)
if oldRules != 0 || newRules != 1 {
t.Fatalf("WAF rule counts old/new = %d/%d", oldRules, newRules)
}
if database.IsZeroTrustEnabled(db) {
t.Fatal("explicit zero-trust false was not applied")
}
}
func TestApplySnapshotRollsBackInvalidReplacement(t *testing.T) {
db, err := database.Init(":memory:")
if err != nil {
t.Fatalf("database.Init: %v", err)
}
db.SetMaxOpenConns(1)
defer db.Close()
valid := &streaming.ConfigSnapshot{
RoutesConfigured: true,
Routes: map[string]streaming.RouteData{
"stable.example.test": {Type: "domain", Target: "http://127.0.0.1:9001"},
},
}
if err := applySnapshotToDB(db, valid); err != nil {
t.Fatalf("apply valid snapshot: %v", err)
}
invalid := &streaming.ConfigSnapshot{
RoutesConfigured: true,
Routes: map[string]streaming.RouteData{
"broken.example.test": {Type: "domain", Target: "file:///etc/passwd"},
},
}
if err := applySnapshotToDB(db, invalid); err == nil {
t.Fatal("invalid replacement should fail")
}
if _, err := database.GetRouteTargets(db, "stable.example.test", "/"); err != nil {
t.Fatalf("rollback did not preserve stable route: %v", err)
}
if _, err := database.GetRouteTargets(db, "broken.example.test", "/"); err != sql.ErrNoRows {
t.Fatalf("invalid route lookup error = %v, want sql.ErrNoRows", err)
}
}
func TestApplySnapshotReconcilesOnlyAuthoritativeUsersAndDomains(t *testing.T) {
db, err := database.Init(":memory:")
if err != nil {
t.Fatalf("database.Init: %v", err)
}
db.SetMaxOpenConns(1)
defer db.Close()
initial := &streaming.ConfigSnapshot{
UsersConfigured: true,
Users: []streaming.UserData{
{Username: "alice", PasswordHash: "alice-v1"},
{Username: "stale", PasswordHash: "stale-v1"},
},
UserDomainsConfigured: true,
UserDomains: []streaming.UserDomainData{
{Username: "alice", Domain: "alice.example.test", TargetURL: "http://alice", Active: true},
{Username: "stale", Domain: "stale.example.test", TargetURL: "http://stale", Active: true},
},
}
if err := applySnapshotToDB(db, initial); err != nil {
t.Fatalf("apply initial user snapshot: %v", err)
}
replacement := &streaming.ConfigSnapshot{
UsersConfigured: true,
Users: []streaming.UserData{
{Username: "alice", PasswordHash: "alice-v2", Email: "alice@example.test"},
},
UserDomainsConfigured: true,
UserDomains: []streaming.UserDomainData{
{Username: "alice", Domain: "new.example.test", TargetURL: "http://new", Active: true},
},
}
if err := applySnapshotToDB(db, replacement); err != nil {
t.Fatalf("apply replacement user snapshot: %v", err)
}
var users, domains int
if err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&users); err != nil {
t.Fatalf("count users: %v", err)
}
if err := db.QueryRow(`SELECT COUNT(*) FROM user_proxy_records`).Scan(&domains); err != nil {
t.Fatalf("count user domains: %v", err)
}
if users != 1 || domains != 1 {
t.Fatalf("authoritative replacement kept stale data: users=%d domains=%d", users, domains)
}
if _, err := database.GetUserID(db, "stale"); err != sql.ErrNoRows {
t.Fatalf("stale user lookup = %v, want sql.ErrNoRows", err)
}
var passwordHash, email string
if err := db.QueryRow(`SELECT password_hash, COALESCE(email, '') FROM users WHERE username = 'alice'`).Scan(&passwordHash, &email); err != nil {
t.Fatalf("load retained user: %v", err)
}
if passwordHash != "alice-v2" || email != "alice@example.test" {
t.Fatalf("retained user was not refreshed: hash=%q email=%q", passwordHash, email)
}
}
func TestApplySnapshotDoesNotReconcileUsersWithoutConfiguredSignal(t *testing.T) {
db, err := database.Init(":memory:")
if err != nil {
t.Fatalf("database.Init: %v", err)
}
db.SetMaxOpenConns(1)
defer db.Close()
seed := &streaming.ConfigSnapshot{
UsersConfigured: true,
Users: []streaming.UserData{{Username: "local", PasswordHash: "local-hash"}},
UserDomainsConfigured: true,
UserDomains: []streaming.UserDomainData{{
Username: "local", Domain: "local.example.test", TargetURL: "http://local", Active: true,
}},
}
if err := applySnapshotToDB(db, seed); err != nil {
t.Fatalf("seed user snapshot: %v", err)
}
// This mirrors an older control plane: the lists are absent/empty, but it
// never claims ownership of either collection.
if err := applySnapshotToDB(db, &streaming.ConfigSnapshot{}); err != nil {
t.Fatalf("apply non-authoritative snapshot: %v", err)
}
var users, domains int
_ = db.QueryRow(`SELECT COUNT(*) FROM users WHERE username = 'local'`).Scan(&users)
_ = db.QueryRow(`SELECT COUNT(*) FROM user_proxy_records WHERE domain = 'local.example.test'`).Scan(&domains)
if users != 1 || domains != 1 {
t.Fatalf("non-authoritative snapshot removed local data: users=%d domains=%d", users, domains)
}
}
func TestApplySnapshotRollsBackConfiguredUserReconciliation(t *testing.T) {
db, err := database.Init(":memory:")
if err != nil {
t.Fatalf("database.Init: %v", err)
}
db.SetMaxOpenConns(1)
defer db.Close()
if err := applySnapshotToDB(db, &streaming.ConfigSnapshot{
UsersConfigured: true,
Users: []streaming.UserData{{Username: "stable", PasswordHash: "hash"}},
}); err != nil {
t.Fatalf("seed stable user: %v", err)
}
err = applySnapshotToDB(db, &streaming.ConfigSnapshot{
UsersConfigured: true,
UserDomainsConfigured: true,
UserDomains: []streaming.UserDomainData{{
Username: "missing", Domain: "broken.example.test", TargetURL: "http://broken", Active: true,
}},
})
if err == nil {
t.Fatal("invalid configured user-domain snapshot should fail")
}
if _, err := database.GetUserID(db, "stable"); err != nil {
t.Fatalf("rollback did not preserve stable user: %v", err)
}
}
func TestMergeConfigSnapshotsKeepsLocalFallbacks(t *testing.T) {
local := &streaming.ConfigSnapshot{RoutesConfigured: true, Routes: map[string]streaming.RouteData{
"local.example.test": {Type: "domain", Target: "http://local"},
"shared.example.test": {Type: "domain", Target: "http://local-shared"},
}}
remote := &streaming.ConfigSnapshot{RoutesConfigured: true, Routes: map[string]streaming.RouteData{
"remote.example.test": {Type: "domain", Target: "http://remote"},
"shared.example.test": {Type: "domain", Target: "http://remote-shared"},
}}
merged := mergeConfigSnapshots(local, remote)
if len(merged.Routes) != 3 {
t.Fatalf("merged routes = %d, want 3", len(merged.Routes))
}
if got := merged.Routes["shared.example.test"].Target; got != "http://remote-shared" {
t.Fatalf("remote route did not override local fallback: %q", got)
}
}