package config_test import ( "os" "path/filepath" "strings" "testing" "time" "sourcecraft.dev/bigbes/lethe/internal/config" ) // validForwardAuthYAML is a baseline valid configuration with forward-auth // enabled. Tests typically clone and tweak this string for negative cases. const validForwardAuthYAML = ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice", "bob"] admins: ["alice"] forward_auth: enabled: true oidc: enabled: false logging: level: "info" format: "tint" ingest: {} ` const validOIDCYAML = ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: false oidc: enabled: true issuer: "https://auth.example.com" audience: "lethe" logging: level: "info" format: "tint" ingest: {} ` const validOIDCDevStubYAML = ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: false oidc: enabled: true issuer: "https://auth.example.com" audience: "lethe" dev_stub: enabled: true bind: "127.0.0.1:8181" token_ttl: "1h" logging: level: "info" format: "tint" ingest: {} ` const validBothEnabledYAML = ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: true oidc: enabled: true issuer: "https://auth.example.com" audience: "lethe" logging: level: "info" format: "tint" ingest: {} ` func writeYAML(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 yaml: %v", err) } return path } func TestLoad_ValidForwardAuth(t *testing.T) { path := writeYAML(t, validForwardAuthYAML) cfg, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Server.Bind != "127.0.0.1:8080" { t.Errorf("Server.Bind = %q", cfg.Server.Bind) } if !cfg.Auth.ForwardAuth.Enabled { t.Error("ForwardAuth.Enabled = false, want true") } } func TestLoad_ValidOIDC(t *testing.T) { path := writeYAML(t, validOIDCYAML) cfg, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if !cfg.Auth.OIDC.Enabled { t.Error("OIDC.Enabled = false, want true") } if cfg.Auth.OIDC.Issuer != "https://auth.example.com" { t.Errorf("OIDC.Issuer = %q", cfg.Auth.OIDC.Issuer) } } func TestLoad_ValidBothEnabled(t *testing.T) { path := writeYAML(t, validBothEnabledYAML) if _, err := config.Load(path); err != nil { t.Fatalf("Load: %v", err) } } func TestLoad_ValidOIDCDevStub(t *testing.T) { path := writeYAML(t, validOIDCDevStubYAML) cfg, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if !cfg.Auth.OIDC.DevStub.Enabled { t.Error("DevStub.Enabled = false, want true") } if cfg.Auth.OIDC.DevStub.Bind != "127.0.0.1:8181" { t.Errorf("DevStub.Bind = %q, want 127.0.0.1:8181", cfg.Auth.OIDC.DevStub.Bind) } if cfg.Auth.OIDC.DevStub.TokenTTL != time.Hour { t.Errorf("DevStub.TokenTTL = %s, want 1h", cfg.Auth.OIDC.DevStub.TokenTTL) } } func TestLoad_DevStubBindNonLoopback_Rejects(t *testing.T) { body := strings.Replace(validOIDCDevStubYAML, `"127.0.0.1:8181"`, `"0.0.0.0:8181"`, 1) if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for non-loopback dev_stub bind, got nil") } } func TestLoad_Defaults(t *testing.T) { path := writeYAML(t, validForwardAuthYAML) cfg, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Auth.ForwardAuth.UserHeader != "Remote-User" { t.Errorf("ForwardAuth.UserHeader = %q, want Remote-User", cfg.Auth.ForwardAuth.UserHeader) } if cfg.Auth.OIDC.UsernameClaim != "preferred_username" { t.Errorf("OIDC.UsernameClaim = %q, want preferred_username", cfg.Auth.OIDC.UsernameClaim) } if cfg.Ingest.MaxBodyBytes != 16*1024*1024 { t.Errorf("Ingest.MaxBodyBytes = %d, want %d", cfg.Ingest.MaxBodyBytes, 16*1024*1024) } if cfg.Ingest.MaxTurnContentBytes != 4*1024*1024 { t.Errorf("Ingest.MaxTurnContentBytes = %d, want %d", cfg.Ingest.MaxTurnContentBytes, 4*1024*1024) } if cfg.Ingest.ChunkSize != 500 { t.Errorf("Ingest.ChunkSize = %d, want 500", cfg.Ingest.ChunkSize) } if cfg.Database.BusyTimeout != 5*time.Second { t.Errorf("Database.BusyTimeout = %s, want 5s", cfg.Database.BusyTimeout) } if cfg.Server.ShutdownGrace != 10*time.Second { t.Errorf("Server.ShutdownGrace = %s, want 10s", cfg.Server.ShutdownGrace) } if cfg.Auth.OIDC.DevStub.TokenTTL != 24*time.Hour { t.Errorf("OIDC.DevStub.TokenTTL = %s, want 24h", cfg.Auth.OIDC.DevStub.TokenTTL) } } func TestLoad_EmptyAllowlistRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: [] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for empty allowed_users, got nil") } } func TestLoad_NonLoopbackBindRejected(t *testing.T) { body := strings.Replace(validForwardAuthYAML, `"127.0.0.1:8080"`, `"0.0.0.0:8080"`, 1) if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for non-loopback bind, got nil") } } func TestLoad_BothAuthModesDisabledRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: false oidc: enabled: false logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error when both auth modes are disabled, got nil") } } func TestLoad_OIDCEnabledWithoutIssuerRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: false oidc: enabled: true audience: "lethe" logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for OIDC enabled without issuer, got nil") } } func TestLoad_OIDCEnabledWithNonURLIssuerRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: false oidc: enabled: true issuer: "not a url" audience: "lethe" logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for non-URL issuer, got nil") } } func TestLoad_AdminNotInAllowedUsersRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] admins: ["mallory"] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error when admin is not in allowed_users, got nil") } } func TestLoad_EmptyAdminsAccepted(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] admins: [] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err != nil { t.Fatalf("expected empty admins to be accepted, got %v", err) } } func TestLoad_MissingDatabasePathRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: {} auth: allowed_users: ["alice"] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for missing database.path, got nil") } } func TestLoad_MaxTurnContentBytesGreaterThanMaxBodyBytesRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: max_body_bytes: 1024 max_turn_content_bytes: 4096 ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error when max_turn_content_bytes > max_body_bytes, got nil") } } func TestLoad_UnknownYAMLKeyRejected(t *testing.T) { body := ` server: bind: "127.0.0.1:8080" totally_unknown_key: 42 database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: {} ` if _, err := config.Load(writeYAML(t, body)); err == nil { t.Fatal("expected error for unknown YAML key, got nil") } } func TestLoad_EnvOverride(t *testing.T) { // Use a YAML body without admins so the override test only exercises // the slice-decoding path (LETHE_AUTH_ALLOWED_USERS overrides YAML). body := ` server: bind: "127.0.0.1:8080" database: path: "./lethe.db" auth: allowed_users: ["alice"] forward_auth: enabled: true logging: level: "info" format: "tint" ingest: {} ` path := writeYAML(t, body) t.Setenv("LETHE_AUTH_ALLOWED_USERS", "carol,dave") cfg, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } got := cfg.Auth.AllowedUsers if len(got) != 2 || got[0] != "carol" || got[1] != "dave" { t.Errorf("AllowedUsers = %v, want [carol dave]", got) } } func TestMustLoad_PanicsOnError(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatal("expected MustLoad to panic on missing file") } }() config.MustLoad("/nonexistent/path/that/should/not/exist.yaml") }