package config import ( "os" "path/filepath" "strings" "testing" ) func env(kv map[string]string) func(string) string { return func(k string) string { return kv[k] } } func TestDirForPlattformpfade(t *testing.T) { cases := []struct { name string goos string env map[string]string want string errMsg string }{ { name: "linux mit HOME", goos: "linux", env: map[string]string{"HOME": "/home/u"}, want: "/home/u/.config/talk", }, { name: "linux XDG hat Vorrang", goos: "linux", env: map[string]string{"HOME": "/home/u", "XDG_CONFIG_HOME": "/xdg"}, want: "/xdg/talk", }, { name: "linux relativer XDG wird ignoriert", goos: "linux", env: map[string]string{"HOME": "/home/u", "XDG_CONFIG_HOME": "relativ"}, want: "/home/u/.config/talk", }, { name: "linux ohne HOME ist Fehler", goos: "linux", env: map[string]string{}, errMsg: ErrNoHome.Error(), }, { name: "macos", goos: "darwin", env: map[string]string{"HOME": "/Users/u"}, want: "/Users/u/Library/Application Support/talk", }, { name: "macos ohne HOME ist Fehler", goos: "darwin", env: map[string]string{}, errMsg: ErrNoHome.Error(), }, { name: "windows", goos: "windows", env: map[string]string{"APPDATA": `C:\Users\u\AppData\Roaming`}, want: filepath.Join(`C:\Users\u\AppData\Roaming`, "talk"), }, { name: "windows ohne APPDATA ist Fehler", goos: "windows", env: map[string]string{}, errMsg: ErrNoHome.Error(), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := dirFor(tc.goos, env(tc.env)) if tc.errMsg != "" { if err == nil || err.Error() != tc.errMsg { t.Fatalf("dirFor(%q) Fehler = %v, want %q", tc.goos, err, tc.errMsg) } return } if err != nil { t.Fatalf("dirFor(%q): unerwarteter Fehler %v", tc.goos, err) } if got != tc.want { t.Errorf("dirFor(%q) = %q, want %q", tc.goos, got, tc.want) } }) } } // TestLoadMerge ist tischgesteuert für die Schichtenfolge // Defaults → TOML-Datei → Env-Override. func TestLoadMerge(t *testing.T) { writeTOML := func(t *testing.T, content string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.toml") if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } return path } t.Run("nur Defaults", func(t *testing.T) { cfg, err := Load(Options{ File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"), Getenv: env(nil), }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Audio.Sink != SinkOto { t.Errorf("Audio.Sink = %q, want Default %q", cfg.Audio.Sink, SinkOto) } }) t.Run("TOML überschreibt Default", func(t *testing.T) { cfg, err := Load(Options{ File: writeTOML(t, "[audio]\nsink = \"null\"\n"), Getenv: env(nil), }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Audio.Sink != SinkNull { t.Errorf("Audio.Sink = %q, want %q aus TOML", cfg.Audio.Sink, SinkNull) } }) t.Run("leere TOML-Sektion lässt Default stehen", func(t *testing.T) { cfg, err := Load(Options{ File: writeTOML(t, "# nur ein Kommentar\n[audio]\n"), Getenv: env(nil), }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Audio.Sink != SinkOto { t.Errorf("Audio.Sink = %q, want Default %q", cfg.Audio.Sink, SinkOto) } }) t.Run("Env schlägt Datei", func(t *testing.T) { cfg, err := Load(Options{ File: writeTOML(t, "[audio]\nsink = \"oto\"\n"), Getenv: env(map[string]string{"TALK_AUDIO_SINK": "null"}), }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Audio.Sink != SinkNull { t.Errorf("Audio.Sink = %q, want Env-Override %q", cfg.Audio.Sink, SinkNull) } }) t.Run("Env allein ohne Datei", func(t *testing.T) { cfg, err := Load(Options{ File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"), Getenv: env(map[string]string{"TALK_AUDIO_SINK": "null"}), }) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Audio.Sink != SinkNull { t.Errorf("Audio.Sink = %q, want %q", cfg.Audio.Sink, SinkNull) } }) t.Run("ungültiger Sink-Wert in Datei ist klarer Fehler", func(t *testing.T) { _, err := Load(Options{ File: writeTOML(t, "[audio]\nsink = \"mpv\"\n"), Getenv: env(nil), }) if err == nil || !strings.Contains(err.Error(), "audio.sink") { t.Fatalf("Load-Fehler = %v, will klaren audio.sink-Fehler", err) } }) t.Run("ungültiger Env-Wert ist klarer Fehler", func(t *testing.T) { _, err := Load(Options{ File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"), Getenv: env(map[string]string{"TALK_AUDIO_SINK": "bogus"}), }) if err == nil || !strings.Contains(err.Error(), "TALK") && !strings.Contains(err.Error(), "audio.sink") { t.Fatalf("Load-Fehler = %v, will klaren audio.sink-Fehler", err) } }) t.Run("kaputte TOML-Syntax ist klarer Fehler", func(t *testing.T) { _, err := Load(Options{ File: writeTOML(t, "[audio\nsink = \n"), Getenv: env(nil), }) if err == nil { t.Fatal("Load bei kaputter TOML: kein Fehler") } }) t.Run("ohne Home läuft Load mit Defaults weiter", func(t *testing.T) { cfg, err := Load(Options{Getenv: env(nil)}) if err != nil { t.Fatalf("Load ohne HOME: %v", err) } if cfg.Audio.Sink != SinkOto { t.Errorf("Audio.Sink = %q, want Default %q", cfg.Audio.Sink, SinkOto) } }) }