Ctrl K
ComunidadGuías…

snapshot_apply_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
	}
}