package config import ( "errors" "testing" "time" ) func TestReadOptionalEnv(t *testing.T) { t.Run("present non-empty", func(t *testing.T) { lookup := func(k string) (string, bool) { if k == "MY_KEY" { return " value ", true } return "", false } if got := readOptionalEnv(lookup, "MY_KEY", "default"); got != "value" { t.Fatalf("got %q, want %q", got, "value") } }) t.Run("present empty", func(t *testing.T) { lookup := func(k string) (string, bool) { return " ", true } if got := readOptionalEnv(lookup, "MY_KEY", "default"); got != "default" { t.Fatalf("got %q, want %q", got, "default") } }) t.Run("missing", func(t *testing.T) { lookup := func(k string) (string, bool) { return "", false } if got := readOptionalEnv(lookup, "MY_KEY", "default"); got != "default" { t.Fatalf("got %q, want %q", got, "default") } }) } func TestReadRequiredEnv(t *testing.T) { t.Run("present", func(t *testing.T) { lookup := func(k string) (string, bool) { return "my-token", true } if got := readRequiredEnv(lookup, "TOKEN"); got != "my-token" { t.Fatalf("got %q, want %q", got, "my-token") } }) t.Run("missing", func(t *testing.T) { lookup := func(k string) (string, bool) { return "", false } if got := readRequiredEnv(lookup, "TOKEN"); got != "" { t.Fatalf("got %q, want empty", got) } }) } func TestLoadStartupFromLookupEnv(t *testing.T) { t.Run("custom values", func(t *testing.T) { lookup := func(k string) (string, bool) { switch k { case EnvListenAddr: return ":9090", true case EnvSQLiteDSN: return "/data/db.sqlite", true case EnvRepoRoot: return "/srv/sub2api-cn-relay-manager", true case EnvReconcileWorkerEnabled: return "true", true case EnvReconcilePollInterval: return "15m", true case EnvRouteRuntimeBackend: return "redis", true case EnvRedisAddr: return "127.0.0.1:16379", true case EnvRedisPassword: return " redis-pass ", true case EnvRedisDB: return "5", true default: return "", false } } cfg, err := loadStartupFromLookupEnv(lookup) if err != nil { t.Fatal(err) } if cfg.Server.ListenAddr != ":9090" { t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, ":9090") } if cfg.Database.SQLiteDSN != "/data/db.sqlite" { t.Fatalf("SQLiteDSN = %q, want %q", cfg.Database.SQLiteDSN, "/data/db.sqlite") } if cfg.Repository.RepoRoot != "/srv/sub2api-cn-relay-manager" { t.Fatalf("RepoRoot = %q, want %q", cfg.Repository.RepoRoot, "/srv/sub2api-cn-relay-manager") } if !cfg.Reconcile.WorkerEnabled { t.Fatal("WorkerEnabled = false, want true") } if cfg.Reconcile.PollInterval != 15*time.Minute { t.Fatalf("PollInterval = %s, want 15m", cfg.Reconcile.PollInterval) } if cfg.RouteRuntime.Backend != "redis" { t.Fatalf("RouteRuntime.Backend = %q, want redis", cfg.RouteRuntime.Backend) } if cfg.RouteRuntime.Redis.Addr != "127.0.0.1:16379" { t.Fatalf("RouteRuntime.Redis.Addr = %q, want 127.0.0.1:16379", cfg.RouteRuntime.Redis.Addr) } if cfg.RouteRuntime.Redis.Password != "redis-pass" { t.Fatalf("RouteRuntime.Redis.Password = %q, want redis-pass", cfg.RouteRuntime.Redis.Password) } if cfg.RouteRuntime.Redis.DB != 5 { t.Fatalf("RouteRuntime.Redis.DB = %d, want 5", cfg.RouteRuntime.Redis.DB) } }) t.Run("default values", func(t *testing.T) { lookup := func(k string) (string, bool) { return "", false } cfg, err := loadStartupFromLookupEnv(lookup) if err != nil { t.Fatal(err) } if cfg.Server.ListenAddr != DefaultListenAddr { t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, DefaultListenAddr) } if cfg.Database.SQLiteDSN != DefaultSQLiteDSN { t.Fatalf("SQLiteDSN = %q, want %q", cfg.Database.SQLiteDSN, DefaultSQLiteDSN) } if cfg.Repository.RepoRoot != "" { t.Fatalf("RepoRoot = %q, want empty by default", cfg.Repository.RepoRoot) } if cfg.Reconcile.WorkerEnabled { t.Fatal("WorkerEnabled = true, want false by default") } if cfg.Reconcile.PollInterval != DefaultReconcilePollInterval { t.Fatalf("PollInterval = %s, want %s", cfg.Reconcile.PollInterval, DefaultReconcilePollInterval) } if cfg.RouteRuntime.Backend != DefaultRouteRuntimeBackend { t.Fatalf("RouteRuntime.Backend = %q, want %q", cfg.RouteRuntime.Backend, DefaultRouteRuntimeBackend) } if cfg.RouteRuntime.Redis.Addr != "" || cfg.RouteRuntime.Redis.Password != "" || cfg.RouteRuntime.Redis.DB != 0 { t.Fatalf("RouteRuntime.Redis = %+v, want zero value", cfg.RouteRuntime.Redis) } }) t.Run("invalid reconcile interval", func(t *testing.T) { lookup := func(k string) (string, bool) { if k == EnvReconcilePollInterval { return "not-a-duration", true } return "", false } if _, err := loadStartupFromLookupEnv(lookup); err == nil { t.Fatal("loadStartupFromLookupEnv() error = nil, want invalid interval") } }) t.Run("invalid redis db", func(t *testing.T) { lookup := func(k string) (string, bool) { if k == EnvRedisDB { return "-1", true } return "", false } if _, err := loadStartupFromLookupEnv(lookup); err == nil { t.Fatal("loadStartupFromLookupEnv() error = nil, want invalid redis db") } }) } func TestLoadAdminTokenFromLookupEnv(t *testing.T) { t.Run("valid token", func(t *testing.T) { lookup := func(k string) (string, bool) { return " admin-secret-123 ", true } token, err := loadAdminTokenFromLookupEnv(lookup) if err != nil { t.Fatal(err) } if token != "admin-secret-123" { t.Fatalf("token = %q, want %q", token, "admin-secret-123") } }) t.Run("empty token", func(t *testing.T) { lookup := func(k string) (string, bool) { return " ", true } _, err := loadAdminTokenFromLookupEnv(lookup) if err == nil { t.Fatal("expected error for empty token") } }) t.Run("missing env", func(t *testing.T) { lookup := func(k string) (string, bool) { return "", false } _, err := loadAdminTokenFromLookupEnv(lookup) if err == nil { t.Fatal("expected error for missing env") } }) } func TestLoadAdminSessionFromLookupEnv(t *testing.T) { t.Run("uses defaults", func(t *testing.T) { cfg, err := loadAdminSessionFromLookupEnv(func(string) (string, bool) { return "", false }) if err != nil { t.Fatal(err) } if cfg.Username != DefaultAdminUsername { t.Fatalf("Username = %q, want %q", cfg.Username, DefaultAdminUsername) } if cfg.Password != "" { t.Fatalf("Password = %q, want empty", cfg.Password) } if cfg.SessionTTL != DefaultAdminSessionTTL { t.Fatalf("SessionTTL = %s, want %s", cfg.SessionTTL, DefaultAdminSessionTTL) } }) t.Run("loads custom values", func(t *testing.T) { cfg, err := loadAdminSessionFromLookupEnv(func(key string) (string, bool) { switch key { case EnvAdminUsername: return " portal-admin ", true case EnvAdminPassword: return " super-secret ", true case EnvAdminSessionTTL: return "4h", true default: return "", false } }) if err != nil { t.Fatal(err) } if cfg.Username != "portal-admin" { t.Fatalf("Username = %q, want portal-admin", cfg.Username) } if cfg.Password != "super-secret" { t.Fatalf("Password = %q, want super-secret", cfg.Password) } if cfg.SessionTTL != 4*time.Hour { t.Fatalf("SessionTTL = %s, want 4h", cfg.SessionTTL) } }) t.Run("rejects invalid ttl", func(t *testing.T) { _, err := loadAdminSessionFromLookupEnv(func(key string) (string, bool) { if key == EnvAdminSessionTTL { return "bad", true } return "", false }) if err == nil { t.Fatal("expected error for invalid session ttl") } }) } // Verify exported wrappers call the lookup versions. // We can't easily test LoadStartupFromEnv / LoadAdminTokenFromEnv // since they depend on os.LookupEnv, but we verify they compile and don't panic. func TestExportFunctionsExist(t *testing.T) { // Just verify the exported functions are reachable and return the right types _, err := LoadAdminTokenFromEnv() if err != nil && !errors.Is(err, err) { // any result is fine, just proving the function exists } }