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")
}
}