ananke/internal/config/validate_matrix_test.go

165 lines
9.8 KiB
Go

package config
import "testing"
// TestValidateRejectsInvalidFieldsMatrix runs one orchestration or CLI step.
// Signature: TestValidateRejectsInvalidFieldsMatrix(t *testing.T).
// Why: exercises validation branches so every config guardrail is test-covered.
func TestValidateRejectsInvalidFieldsMatrix(t *testing.T) {
base := defaults()
base.UPS.Target = "pyrphoros@localhost"
if err := base.Validate(); err != nil {
t.Fatalf("test baseline config must validate: %v", err)
}
cases := []struct {
name string
mutate func(*Config)
}{
{"empty_control_planes", func(c *Config) { c.ControlPlanes = nil }},
{"empty_expected_flux_branch", func(c *Config) { c.ExpectedFluxBranch = "" }},
{"empty_expected_flux_source", func(c *Config) { c.ExpectedFluxSource = "" }},
{"empty_iac_repo_path", func(c *Config) { c.IACRepoPath = "" }},
{"bad_shutdown_budget", func(c *Config) { c.Shutdown.DefaultBudgetSeconds = 0 }},
{"bad_shutdown_history_samples", func(c *Config) { c.Shutdown.HistoryMinSamples = 0 }},
{"bad_shutdown_emergency_budget", func(c *Config) { c.Shutdown.EmergencyBudgetSec = 0 }},
{"bad_shutdown_emergency_samples", func(c *Config) { c.Shutdown.EmergencyMinSamples = 0 }},
{"bad_drain_parallelism", func(c *Config) { c.Shutdown.DrainParallelism = 0 }},
{"bad_scale_parallelism", func(c *Config) { c.Shutdown.ScaleParallelism = 0 }},
{"bad_ssh_parallelism", func(c *Config) { c.Shutdown.SSHParallelism = 0 }},
{"bad_api_wait", func(c *Config) { c.Startup.APIWaitSeconds = 0 }},
{"bad_api_poll", func(c *Config) { c.Startup.APIPollSeconds = 0 }},
{"bad_min_battery_percent", func(c *Config) { c.Startup.MinimumBatteryPercent = 101 }},
{"bad_node_inventory_poll", func(c *Config) { c.Startup.NodeInventoryReachPollSeconds = 0 }},
{"bad_empty_node_inventory_required_node", func(c *Config) { c.Startup.NodeInventoryReachRequiredNodes = []string{"titan-0a", ""} }},
{"bad_time_sync_wait", func(c *Config) { c.Startup.TimeSyncWaitSeconds = 0 }},
{"bad_time_sync_poll", func(c *Config) { c.Startup.TimeSyncPollSeconds = 0 }},
{"bad_time_sync_quorum", func(c *Config) { c.Startup.TimeSyncMode = "quorum"; c.Startup.TimeSyncQuorum = 0 }},
{"bad_storage_wait", func(c *Config) { c.Startup.StorageReadyWaitSeconds = 0 }},
{"bad_storage_poll", func(c *Config) { c.Startup.StorageReadyPollSeconds = 0 }},
{"bad_storage_min_ready_nodes", func(c *Config) { c.Startup.StorageMinReadyNodes = 0 }},
{"bad_post_start_wait", func(c *Config) { c.Startup.PostStartProbeWaitSeconds = 0 }},
{"bad_post_start_poll", func(c *Config) { c.Startup.PostStartProbePollSeconds = 0 }},
{"bad_service_checklist_wait", func(c *Config) { c.Startup.ServiceChecklistWaitSeconds = 0 }},
{"bad_service_checklist_poll", func(c *Config) { c.Startup.ServiceChecklistPollSeconds = 0 }},
{"bad_service_checklist_stability", func(c *Config) { c.Startup.ServiceChecklistStabilitySec = -1 }},
{"bad_service_checklist_item_name", func(c *Config) {
c.Startup.ServiceChecklist = []ServiceChecklistCheck{{Name: "", URL: "https://ok", TimeoutSeconds: 5}}
}},
{"bad_service_checklist_item_timeout", func(c *Config) {
c.Startup.ServiceChecklist = []ServiceChecklistCheck{{Name: "x", URL: "https://ok", TimeoutSeconds: 0}}
}},
{"bad_service_checklist_http_code", func(c *Config) {
c.Startup.ServiceChecklist = []ServiceChecklistCheck{{Name: "x", URL: "https://ok", TimeoutSeconds: 5, AcceptedStatuses: []int{999}}}
}},
{"bad_critical_endpoint_wait", func(c *Config) { c.Startup.CriticalServiceEndpointWaitSec = 0 }},
{"bad_critical_endpoint_poll", func(c *Config) { c.Startup.CriticalServiceEndpointPollSec = 0 }},
{"bad_empty_critical_endpoints_when_required", func(c *Config) {
c.Startup.RequireCriticalServiceEndpoints = true
c.Startup.CriticalServiceEndpoints = nil
}},
{"bad_empty_critical_endpoint_entry", func(c *Config) {
c.Startup.CriticalServiceEndpoints = []string{"", "monitoring/victoria-metrics-single-server"}
}},
{"bad_malformed_critical_endpoint_entry", func(c *Config) {
c.Startup.CriticalServiceEndpoints = []string{"monitoring-victoria-metrics-single-server"}
}},
{"bad_ingress_wait", func(c *Config) { c.Startup.IngressChecklistWaitSeconds = 0 }},
{"bad_ingress_poll", func(c *Config) { c.Startup.IngressChecklistPollSeconds = 0 }},
{"bad_ingress_http_code", func(c *Config) { c.Startup.IngressChecklistAccepted = []int{700} }},
{"bad_ingress_ignore_entry", func(c *Config) { c.Startup.IngressChecklistIgnoreHosts = []string{"", "grafana.bstein.dev"} }},
{"bad_node_ssh_wait", func(c *Config) { c.Startup.NodeSSHAuthWaitSeconds = 0 }},
{"bad_node_ssh_poll", func(c *Config) { c.Startup.NodeSSHAuthPollSeconds = 0 }},
{"bad_empty_node_ssh_required_node", func(c *Config) { c.Startup.NodeSSHAuthRequiredNodes = []string{"titan-0a", ""} }},
{"bad_flux_wait", func(c *Config) { c.Startup.FluxHealthWaitSeconds = 0 }},
{"bad_flux_poll", func(c *Config) { c.Startup.FluxHealthPollSeconds = 0 }},
{"bad_empty_flux_required_kustomization", func(c *Config) { c.Startup.FluxHealthRequiredKustomizations = []string{"flux-system/core", ""} }},
{"bad_malformed_flux_required_kustomization", func(c *Config) { c.Startup.FluxHealthRequiredKustomizations = []string{"flux-system-core"} }},
{"bad_workload_wait", func(c *Config) { c.Startup.WorkloadConvergenceWaitSeconds = 0 }},
{"bad_workload_poll", func(c *Config) { c.Startup.WorkloadConvergencePollSeconds = 0 }},
{"bad_empty_required_workload_namespace", func(c *Config) { c.Startup.WorkloadConvergenceRequiredNamespaces = []string{"monitoring", ""} }},
{"bad_stuck_pod_grace", func(c *Config) { c.Startup.StuckPodGraceSeconds = 0 }},
{"bad_empty_post_start_probe_entry", func(c *Config) { c.Startup.PostStartProbes = []string{"https://ok", ""} }},
{"bad_empty_ignore_flux_entry", func(c *Config) { c.Startup.IgnoreFluxKustomizations = []string{"", "ns/name"} }},
{"bad_empty_ignore_workloads_entry", func(c *Config) { c.Startup.IgnoreWorkloads = []string{"", "ns/name"} }},
{"bad_empty_ignore_workload_namespaces_entry", func(c *Config) { c.Startup.IgnoreWorkloadNamespaces = []string{"", "vault"} }},
{"bad_overlap_flux_required_and_ignored", func(c *Config) {
c.Startup.FluxHealthRequiredKustomizations = []string{"flux-system/core"}
c.Startup.IgnoreFluxKustomizations = []string{"flux-system/core"}
}},
{"bad_overlap_workload_required_and_ignored", func(c *Config) {
c.Startup.WorkloadConvergenceRequiredNamespaces = []string{"monitoring"}
c.Startup.IgnoreWorkloadNamespaces = []string{"monitoring"}
}},
{"bad_empty_ignore_unavailable_nodes_entry", func(c *Config) { c.Startup.IgnoreUnavailableNodes = []string{"", "titan-22"} }},
{"bad_empty_vault_key_file", func(c *Config) { c.Startup.VaultUnsealKeyFile = "" }},
{"bad_ssh_port_low", func(c *Config) { c.SSHPort = 0 }},
{"bad_ups_provider", func(c *Config) { c.UPS.Enabled = true; c.UPS.Provider = "" }},
{"bad_ups_target_empty", func(c *Config) { c.UPS.Enabled = true; c.UPS.Provider = "nut"; c.UPS.Target = ""; c.UPS.Targets = nil }},
{"bad_ups_targets_item_empty", func(c *Config) {
c.UPS.Enabled = true
c.UPS.Provider = "nut"
c.UPS.Target = ""
c.UPS.Targets = []UPSTarget{{Name: "x", Target: ""}}
}},
{"bad_startup_guard_age", func(c *Config) { c.Coordination.StartupGuardMaxAgeSec = 0 }},
{"bad_state_run_history_path", func(c *Config) { c.State.RunHistoryPath = "" }},
{"bad_state_intent_path", func(c *Config) { c.State.IntentPath = "" }},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cfg := base
tc.mutate(&cfg)
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for %s", tc.name)
}
})
}
}
// TestApplyDefaultsPopulatesZeroConfig runs one orchestration or CLI step.
// Signature: TestApplyDefaultsPopulatesZeroConfig(t *testing.T).
// Why: validates that defaulting logic fills all critical runtime values.
func TestApplyDefaultsPopulatesZeroConfig(t *testing.T) {
var cfg Config
cfg.ControlPlanes = []string{"titan-0a", "titan-0b", "titan-0c"}
cfg.applyDefaults()
if cfg.ExpectedFluxBranch == "" || cfg.ExpectedFluxSource == "" || cfg.IACRepoPath == "" {
t.Fatalf("expected core defaults to be set")
}
if cfg.State.Dir == "" || cfg.State.ReportsDir == "" || cfg.State.RunHistoryPath == "" || cfg.State.LockPath == "" || cfg.State.IntentPath == "" {
t.Fatalf("expected state defaults to be set")
}
if cfg.Startup.TimeSyncMode == "" || cfg.Startup.EtcdRestoreControlPlane == "" || cfg.Startup.VaultUnsealKeyFile == "" {
t.Fatalf("expected startup defaults to be set")
}
if cfg.Startup.NodeInventoryReachRequiredNodes == nil || cfg.Startup.NodeSSHAuthRequiredNodes == nil ||
cfg.Startup.FluxHealthRequiredKustomizations == nil || cfg.Startup.WorkloadConvergenceRequiredNamespaces == nil {
t.Fatalf("expected startup recovery scope slices to be initialized")
}
if cfg.Startup.CriticalServiceEndpointWaitSec <= 0 || cfg.Startup.CriticalServiceEndpointPollSec <= 0 {
t.Fatalf("expected critical service endpoint timing defaults to be set")
}
if len(cfg.Startup.CriticalServiceEndpoints) == 0 {
t.Fatalf("expected critical service endpoint defaults to be set")
}
if cfg.Shutdown.SSHParallelism <= 0 || cfg.Shutdown.ScaleParallelism <= 0 || cfg.Shutdown.DrainParallelism <= 0 {
t.Fatalf("expected shutdown parallelism defaults to be set")
}
}
// TestValidateAcceptsFullySpecifiedConfig runs one orchestration or CLI step.
// Signature: TestValidateAcceptsFullySpecifiedConfig(t *testing.T).
// Why: covers success branches for forward-shutdown and UPS target validation.
func TestValidateAcceptsFullySpecifiedConfig(t *testing.T) {
cfg := defaults()
cfg.UPS.Target = "pyrphoros@localhost"
cfg.Coordination.ForwardShutdownHost = "titan-db"
cfg.Coordination.ForwardShutdownConfig = "/etc/ananke/ananke.yaml"
cfg.Coordination.PeerHosts = []string{"titan-24"}
if err := cfg.Validate(); err != nil {
t.Fatalf("expected fully specified config to validate: %v", err)
}
}