package config import ( "os" "path/filepath" "testing" ) // validMatrix is a reusable matrix config block for tests. const validMatrix = ` matrix: homeserver: "https://matrix.example.org" user_id: "@pete:example.org" password: "testpass" channels: tech: "!tech:example.org" politics: "!politics:example.org" ` func TestLoadValid(t *testing.T) { yaml := validMatrix + ` ollama: base_url: "http://localhost:11434" model: "gemma4:27b" timeout_seconds: 60 storage: db_path: "/tmp/test.db" sources: - name: "Test Source" feed_url: "https://example.com/rss" tier: 1 enabled: true ` cfg := loadFromString(t, yaml) if cfg.Matrix.Homeserver != "https://matrix.example.org" { t.Errorf("homeserver = %q", cfg.Matrix.Homeserver) } if cfg.Matrix.UserID != "@pete:example.org" { t.Errorf("user_id = %q", cfg.Matrix.UserID) } if cfg.Ollama.TimeoutSeconds != 60 { t.Errorf("timeout = %d, want 60", cfg.Ollama.TimeoutSeconds) } if len(cfg.Sources) != 1 { t.Fatalf("sources = %d, want 1", len(cfg.Sources)) } if cfg.Sources[0].Name != "Test Source" { t.Errorf("source name = %q", cfg.Sources[0].Name) } } func TestEnvVarExpansion(t *testing.T) { t.Setenv("TEST_PETE_PASS", "secret_pass_123") yaml := ` matrix: homeserver: "https://matrix.example.org" user_id: "@pete:example.org" password: "${TEST_PETE_PASS}" channels: tech: "!tech:example.org" ollama: base_url: "http://localhost:11434" model: "gemma4:27b" storage: db_path: "/tmp/test.db" sources: [] ` cfg := loadFromString(t, yaml) if cfg.Matrix.Password != "secret_pass_123" { t.Errorf("password = %q, want secret_pass_123", cfg.Matrix.Password) } } func TestDefaults(t *testing.T) { yaml := validMatrix + ` ollama: base_url: "http://localhost:11434" model: "gemma4:27b" storage: db_path: "/tmp/test.db" sources: - name: "S" feed_url: "https://example.com/rss" tier: 1 enabled: true ` cfg := loadFromString(t, yaml) if cfg.Matrix.DataDir != "./data" { t.Errorf("data_dir default = %q, want ./data", cfg.Matrix.DataDir) } if cfg.Matrix.DisplayName != "Pete" { t.Errorf("display_name default = %q, want Pete", cfg.Matrix.DisplayName) } if cfg.Ollama.TimeoutSeconds != 30 { t.Errorf("timeout default = %d, want 30", cfg.Ollama.TimeoutSeconds) } if cfg.Posting.MinIntervalSeconds != 300 { t.Errorf("min_interval default = %d, want 300", cfg.Posting.MinIntervalSeconds) } if cfg.Posting.BurstCapCount != 3 { t.Errorf("burst_cap default = %d, want 3", cfg.Posting.BurstCapCount) } if cfg.Posting.BurstCapWindowSeconds != 1800 { t.Errorf("burst_window default = %d, want 1800", cfg.Posting.BurstCapWindowSeconds) } if cfg.Storage.RecentWindowHours != 24 { t.Errorf("recent_window default = %d, want 24", cfg.Storage.RecentWindowHours) } if cfg.Storage.ClassificationLogDays != 7 { t.Errorf("classification_log default = %d, want 7", cfg.Storage.ClassificationLogDays) } if cfg.Sources[0].PollIntervalMinutes != 20 { t.Errorf("poll_interval default = %d, want 20", cfg.Sources[0].PollIntervalMinutes) } } // shortMatrix returns a minimal valid matrix block for validation error tests. func shortMatrix(overrides string) string { base := ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ` if overrides != "" { return overrides } return base } func TestValidationErrors(t *testing.T) { tests := []struct { name string yaml string }{ {"missing homeserver", ` matrix: user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} `}, {"missing user_id", ` matrix: homeserver: "https://h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} `}, {"missing password", ` matrix: homeserver: "https://h" user_id: "@p:h" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} `}, {"no channels", ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} `}, {"missing ollama base_url", ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {model: "m"} storage: {db_path: "/tmp/t.db"} `}, {"missing ollama model", ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l"} storage: {db_path: "/tmp/t.db"} `}, {"missing db_path", ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {} `}, {"invalid tier", ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} sources: - name: "S" feed_url: "https://e.com/rss" tier: 5 `}, {"missing source name", ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} sources: - feed_url: "https://e.com/rss" tier: 1 `}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") os.WriteFile(path, []byte(tt.yaml), 0o644) _, err := Load(path) if err == nil { t.Error("expected validation error, got nil") } }) } } func TestDirectRouteNullable(t *testing.T) { yaml := ` matrix: homeserver: "https://h" user_id: "@p:h" password: "pw" channels: {tech: "!t:e"} ollama: {base_url: "http://l", model: "m"} storage: {db_path: "/tmp/t.db"} sources: - name: "Direct" feed_url: "https://e.com/rss" tier: 1 direct_route: "politics" enabled: true - name: "LLM" feed_url: "https://e.com/rss2" tier: 1 direct_route: null enabled: true ` cfg := loadFromString(t, yaml) if cfg.Sources[0].DirectRoute == nil { t.Fatal("direct source should have non-nil DirectRoute") } if *cfg.Sources[0].DirectRoute != "politics" { t.Errorf("direct_route = %q, want politics", *cfg.Sources[0].DirectRoute) } if cfg.Sources[1].DirectRoute != nil { t.Errorf("null source should have nil DirectRoute, got %v", cfg.Sources[1].DirectRoute) } } func loadFromString(t *testing.T, yamlContent string) *Config { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte(yamlContent), 0o644); err != nil { t.Fatal(err) } cfg, err := Load(path) if err != nil { t.Fatal(err) } return cfg }