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" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!tech:example.org" politics = "!politics:example.org" ` func TestLoadValid(t *testing.T) { toml := validMatrix + ` [storage] db_path = "/tmp/test.db" [[sources]] name = "Test Source" feed_url = "https://example.com/rss" tier = 1 poll_interval_minutes = 20 direct_route = "tech" enabled = true ` cfg := loadFromString(t, toml) 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 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) } if cfg.Sources[0].DirectRoute != "tech" { t.Errorf("direct_route = %q, want tech", cfg.Sources[0].DirectRoute) } } func TestEnvVarExpansion(t *testing.T) { t.Setenv("TEST_PETE_PASS", "secret_pass_123") toml := ` [matrix] homeserver = "https://matrix.example.org" user_id = "@pete:example.org" password = "${TEST_PETE_PASS}" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!tech:example.org" [storage] db_path = "/tmp/test.db" ` cfg := loadFromString(t, toml) if cfg.Matrix.Password != "secret_pass_123" { t.Errorf("password = %q, want secret_pass_123", cfg.Matrix.Password) } } func TestDefaults(t *testing.T) { toml := validMatrix + ` [storage] db_path = "/tmp/test.db" [[sources]] name = "S" feed_url = "https://example.com/rss" tier = 1 poll_interval_minutes = 20 direct_route = "tech" enabled = true ` cfg := loadFromString(t, toml) 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.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) } } func TestValidationErrors(t *testing.T) { tests := []struct { name string toml string }{ {"missing homeserver", ` [matrix] user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" `}, {"missing user_id", ` [matrix] homeserver = "https://h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" `}, {"missing password", ` [matrix] homeserver = "https://h" user_id = "@p:h" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" `}, {"no channels", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [storage] db_path = "/tmp/t.db" `}, {"missing db_path", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] `}, {"invalid tier", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" [[sources]] name = "S" feed_url = "https://e.com/rss" tier = 5 poll_interval_minutes = 20 direct_route = "tech" enabled = true `}, {"missing source name", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" [[sources]] feed_url = "https://e.com/rss" tier = 1 poll_interval_minutes = 20 direct_route = "tech" enabled = true `}, {"enabled source missing direct_route", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" [[sources]] name = "S" feed_url = "https://e.com/rss" tier = 1 poll_interval_minutes = 20 enabled = true `}, {"push enabled without auth", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" [web.push] enabled = true vapid_public_key = "pub" vapid_private_key = "priv" subject = "mailto:a@b.c" `}, {"push enabled missing keys", ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" [web.auth] enabled = true issuer = "https://i" client_id = "id" client_secret = "secret" redirect_url = "https://r/cb" session_secret = "session_secret_16chars_long" [web.push] enabled = true subject = "mailto:a@b.c" `}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.toml") os.WriteFile(path, []byte(tt.toml), 0o644) _, err := Load(path) if err == nil { t.Error("expected validation error, got nil") } }) } } func TestDisabledSourceSkipsDirectRouteCheck(t *testing.T) { // A disabled source without direct_route must not fail validation. toml := ` [matrix] homeserver = "https://h" user_id = "@p:h" password = "pw" pickle_key = "test_pickle_key_1234" [matrix.channels] tech = "!t:e" [storage] db_path = "/tmp/t.db" [[sources]] name = "Off" feed_url = "https://e.com/rss" tier = 1 poll_interval_minutes = 20 enabled = false ` cfg := loadFromString(t, toml) if cfg.Sources[0].DirectRoute != "" { t.Errorf("expected empty direct_route, got %q", cfg.Sources[0].DirectRoute) } } func loadFromString(t *testing.T, tomlContent string) *Config { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.toml") if err := os.WriteFile(path, []byte(tomlContent), 0o644); err != nil { t.Fatal(err) } cfg, err := Load(path) if err != nil { t.Fatal(err) } return cfg }