ananke/internal/config/config_test.go

201 lines
5.8 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestLoadAcceptsUPSTargets(t *testing.T) {
tmp := t.TempDir()
cfgPath := filepath.Join(tmp, "ananke.yaml")
raw := `
control_planes: [titan-0a, titan-0b, titan-0c]
expected_flux_branch: main
iac_repo_path: /opt/titan-iac
ups:
enabled: true
provider: nut
targets:
- name: pyrphoros
target: pyrphoros@localhost
shutdown:
default_budget_seconds: 300
state:
run_history_path: /tmp/runs.json
lock_path: /tmp/ananke.lock
`
if err := os.WriteFile(cfgPath, []byte(strings.TrimSpace(raw)), 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("load config: %v", err)
}
if len(cfg.UPS.Targets) != 1 || cfg.UPS.Targets[0].Target != "pyrphoros@localhost" {
t.Fatalf("unexpected UPS targets: %#v", cfg.UPS.Targets)
}
}
func TestValidateForwardShutdownRequiresConfigPath(t *testing.T) {
cfg := defaults()
cfg.Coordination.ForwardShutdownHost = "titan-db"
cfg.Coordination.ForwardShutdownConfig = ""
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for missing forward_shutdown_config")
}
}
func TestValidateRejectsUnknownRole(t *testing.T) {
cfg := defaults()
cfg.Coordination.Role = "unknown"
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for unknown coordination role")
}
}
func TestValidateRejectsEmptyPeerHostEntry(t *testing.T) {
cfg := defaults()
cfg.Coordination.PeerHosts = []string{"titan-24", " "}
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for empty peer_hosts entry")
}
}
func TestValidateRejectsUnknownEtcdRestoreControlPlane(t *testing.T) {
cfg := defaults()
cfg.Startup.EtcdRestoreControlPlane = "titan-missing"
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for unknown etcd restore control plane")
}
}
func TestLoadSetsCoordinationGuardDefaults(t *testing.T) {
tmp := t.TempDir()
cfgPath := filepath.Join(tmp, "ananke.yaml")
raw := `
control_planes: [titan-0a, titan-0b, titan-0c]
expected_flux_branch: main
iac_repo_path: /opt/titan-iac
coordination:
role: coordinator
ups:
enabled: false
state:
run_history_path: /tmp/runs.json
lock_path: /tmp/ananke.lock
`
if err := os.WriteFile(cfgPath, []byte(strings.TrimSpace(raw)), 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("load config: %v", err)
}
if cfg.Coordination.StartupGuardMaxAgeSec <= 0 {
t.Fatalf("expected startup guard max age default > 0, got %d", cfg.Coordination.StartupGuardMaxAgeSec)
}
if cfg.Startup.EtcdRestoreControlPlane == "" {
t.Fatalf("expected startup etcd restore control plane default to be set")
}
if cfg.Startup.TimeSyncMode == "" {
t.Fatalf("expected startup time sync mode default to be set")
}
if cfg.Startup.VaultUnsealKeyFile == "" {
t.Fatalf("expected startup vault unseal key file default to be set")
}
if cfg.Startup.ShutdownCooldownSeconds <= 0 {
t.Fatalf("expected startup shutdown cooldown default > 0, got %d", cfg.Startup.ShutdownCooldownSeconds)
}
if cfg.Startup.VaultUnsealBreakglassTimeout <= 0 {
t.Fatalf("expected startup break-glass timeout default > 0, got %d", cfg.Startup.VaultUnsealBreakglassTimeout)
}
}
func TestValidateRejectsInvalidStartupShutdownCooldown(t *testing.T) {
cfg := defaults()
cfg.Startup.ShutdownCooldownSeconds = 0
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid startup shutdown_cooldown_seconds")
}
}
func TestValidateRejectsInvalidTimeSyncMode(t *testing.T) {
cfg := defaults()
cfg.Startup.TimeSyncMode = "invalid"
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid time_sync_mode")
}
}
func TestValidateRejectsBadStoragePVCFormat(t *testing.T) {
cfg := defaults()
cfg.Startup.StorageCriticalPVCs = []string{"vault-data-vault-0"}
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid storage_critical_pvcs entry")
}
}
func TestValidateRejectsMissingPostStartProbesWhenRequired(t *testing.T) {
cfg := defaults()
cfg.Startup.RequirePostStartProbes = true
cfg.Startup.PostStartProbes = nil
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error when post start probes are required but empty")
}
}
func TestValidateRejectsMissingServiceChecklistWhenRequired(t *testing.T) {
cfg := defaults()
cfg.Startup.RequireServiceChecklist = true
cfg.Startup.ServiceChecklist = nil
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error when service checklist is required but empty")
}
}
func TestValidateRejectsBadServiceChecklistURL(t *testing.T) {
cfg := defaults()
cfg.Startup.ServiceChecklist = []ServiceChecklistCheck{
{
Name: "grafana",
URL: "not-a-url",
AcceptedStatuses: []int{200},
TimeoutSeconds: 12,
},
}
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid service checklist url")
}
}
func TestValidateRejectsBadIgnoreFluxKustomizationFormat(t *testing.T) {
cfg := defaults()
cfg.Startup.IgnoreFluxKustomizations = []string{"jellyfin"}
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid ignore_flux_kustomizations entry")
}
}
func TestValidateRejectsBadIgnoreWorkloadFormat(t *testing.T) {
cfg := defaults()
cfg.Startup.IgnoreWorkloads = []string{"maintenance/metis/extra/value"}
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid ignore_workloads entry")
}
}
func TestValidateRejectsInvalidRequiredNodeLabel(t *testing.T) {
cfg := defaults()
cfg.Startup.RequiredNodeLabels = map[string]map[string]string{
"titan-09": {
"": "true",
},
}
if err := cfg.Validate(); err == nil {
t.Fatalf("expected validation error for invalid required_node_labels entry")
}
}