package config import ( "os" "path/filepath" "strings" "testing" "time" ) // writeConfig drops the given YAML body into a temp file and returns its path. func writeConfig(t *testing.T, body string) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatalf("write config: %v", err) } return path } func TestLoadFullConfig(t *testing.T) { path := writeConfig(t, ` server: host: "127.0.0.1" port: 9001 read_timeout: 7s write_timeout: 8s public_url: "https://search.example.com" log: level: debug format: json search: default_provider: steam `) cfg, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Server.Host != "127.0.0.1" { t.Errorf("host = %q", cfg.Server.Host) } if cfg.Server.Port != 9001 { t.Errorf("port = %d", cfg.Server.Port) } if cfg.Server.ReadTimeout != 7*time.Second { t.Errorf("read_timeout = %v", cfg.Server.ReadTimeout) } if cfg.Server.WriteTimeout != 8*time.Second { t.Errorf("write_timeout = %v", cfg.Server.WriteTimeout) } if cfg.Server.PublicURL != "https://search.example.com" { t.Errorf("public_url = %q", cfg.Server.PublicURL) } if cfg.Log.Level != "debug" || cfg.Log.Format != "json" { t.Errorf("log = %+v", cfg.Log) } if cfg.Search.DefaultProvider != "steam" { t.Errorf("default_provider = %q", cfg.Search.DefaultProvider) } } func TestLoadAppliesDefaults(t *testing.T) { // Only public_url is required and has no default. path := writeConfig(t, ` server: public_url: "http://localhost:8080" `) cfg, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Server.Host != "0.0.0.0" { t.Errorf("default host = %q", cfg.Server.Host) } if cfg.Server.Port != 8080 { t.Errorf("default port = %d", cfg.Server.Port) } if cfg.Server.ReadTimeout != 15*time.Second { t.Errorf("default read_timeout = %v", cfg.Server.ReadTimeout) } if cfg.Log.Level != "info" || cfg.Log.Format != "human" { t.Errorf("default log = %+v", cfg.Log) } if cfg.Search.DefaultProvider != "gh" { t.Errorf("default provider = %q", cfg.Search.DefaultProvider) } } func TestLoadEnvOverride(t *testing.T) { path := writeConfig(t, ` server: public_url: "http://localhost:8080" `) t.Setenv("APP_SERVER_PORT", "9090") t.Setenv("APP_LOG_LEVEL", "warn") t.Setenv("APP_SEARCH_DEFAULT_PROVIDER", "ud") cfg, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Server.Port != 9090 { t.Errorf("env override port = %d", cfg.Server.Port) } if cfg.Log.Level != "warn" { t.Errorf("env override log.level = %q", cfg.Log.Level) } if cfg.Search.DefaultProvider != "ud" { t.Errorf("env override default_provider = %q", cfg.Search.DefaultProvider) } } func TestLoadMissingFile(t *testing.T) { _, err := Load(filepath.Join(t.TempDir(), "does-not-exist.yaml")) if err == nil { t.Fatal("expected error for missing file") } } func TestLoadInvalidYAML(t *testing.T) { path := writeConfig(t, "this: is: not: valid: yaml:\n - [") if _, err := Load(path); err == nil { t.Fatal("expected parse error") } } func TestLoadUnknownKeyRejected(t *testing.T) { // UnmarshalExact rejects unknown keys, which catches typos. path := writeConfig(t, ` server: public_url: "http://localhost:8080" bogus_field: 42 `) _, err := Load(path) if err == nil { t.Fatal("expected error for unknown key") } if !strings.Contains(err.Error(), "bogus_field") && !strings.Contains(err.Error(), "unmarshal") { t.Errorf("error should mention unknown key or unmarshal, got: %v", err) } } func TestLoadValidationErrors(t *testing.T) { cases := []struct { name string body string }{ { name: "missing public_url", body: ` server: host: "0.0.0.0" port: 8080 `, }, { name: "invalid public_url", body: ` server: public_url: "not a url" `, }, { name: "port out of range", body: ` server: public_url: "http://localhost:8080" port: 70000 `, }, { name: "unknown log level", body: ` server: public_url: "http://localhost:8080" log: level: shout `, }, { name: "unknown log format", body: ` server: public_url: "http://localhost:8080" log: format: yaml `, }, { name: "unknown default provider", body: ` server: public_url: "http://localhost:8080" search: default_provider: yahoo `, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { path := writeConfig(t, tc.body) if _, err := Load(path); err == nil { t.Fatal("expected validation error") } }) } } func TestValidateDirectly(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "0.0.0.0", Port: 8080, PublicURL: "http://localhost:8080"}, Log: LogConfig{Level: "info", Format: "human"}, Search: SearchConfig{DefaultProvider: "gh"}, } if err := cfg.Validate(); err != nil { t.Fatalf("Validate on good config: %v", err) } cfg.Search.DefaultProvider = "" if err := cfg.Validate(); err == nil { t.Fatal("expected validation error for empty default_provider") } }