Files
talk/internal/config/config_test.go
T
m3ta-chironandClaude Fable 5.1 85fa64cfe6 refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.

Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.

Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 11:15:35 +02:00

632 lines
21 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"code.m3ta.dev/m3tam3re/talk/internal/queue"
)
func env(kv map[string]string) func(string) string {
return func(k string) string { return kv[k] }
}
// writeTOML writes content to a temp config.toml and returns its path.
func writeTOML(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
}
// missingFile returns a path that does not exist (defaults + env only).
func missingFile(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "does-not-exist.toml")
}
func TestDirForPlatformPaths(t *testing.T) {
cases := []struct {
name string
goos string
env map[string]string
want string
errMsg string
}{
{"linux with HOME", "linux", map[string]string{"HOME": "/home/u"}, "/home/u/.config/talk", ""},
{"linux XDG wins", "linux", map[string]string{"HOME": "/home/u", "XDG_CONFIG_HOME": "/xdg"}, "/xdg/talk", ""},
{"linux relative XDG is ignored", "linux", map[string]string{"HOME": "/home/u", "XDG_CONFIG_HOME": "relative"}, "/home/u/.config/talk", ""},
{"linux without HOME is an error", "linux", map[string]string{}, "", ErrNoHome.Error()},
{"macos", "darwin", map[string]string{"HOME": "/Users/u"}, "/Users/u/Library/Application Support/talk", ""},
{"macos without HOME is an error", "darwin", map[string]string{}, "", ErrNoHome.Error()},
{"windows", "windows", map[string]string{"APPDATA": `C:\Users\u\AppData\Roaming`}, filepath.Join(`C:\Users\u\AppData\Roaming`, "talk"), ""},
{"windows without APPDATA is an error", "windows", map[string]string{}, "", 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) error = %v, want %q", tc.goos, err, tc.errMsg)
}
return
}
if err != nil {
t.Fatalf("dirFor(%q): unexpected error %v", tc.goos, err)
}
if got != tc.want {
t.Errorf("dirFor(%q) = %q, want %q", tc.goos, got, tc.want)
}
})
}
}
// TestLoadMerge pins the layering defaults → TOML file → env override.
func TestLoadMerge(t *testing.T) {
t.Run("defaults only", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), 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 overrides 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 from TOML", cfg.Audio.Sink, SinkNull)
}
})
t.Run("empty TOML section keeps default", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, "# just a comment\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 beats file", 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 alone without file", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), 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("invalid sink in file is a clear error", 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 error = %v, want a clear audio.sink error", err)
}
})
t.Run("invalid env value is a clear error", func(t *testing.T) {
_, err := Load(Options{File: missingFile(t), Getenv: env(map[string]string{"TALK_AUDIO_SINK": "bogus"})})
if err == nil || !strings.Contains(err.Error(), "audio.sink") {
t.Fatalf("Load error = %v, want a clear audio.sink error", err)
}
})
t.Run("broken TOML syntax is a clear error", func(t *testing.T) {
if _, err := Load(Options{File: writeTOML(t, "[audio\nsink = \n"), Getenv: env(nil)}); err == nil {
t.Fatal("Load with broken TOML: no error")
}
})
t.Run("without HOME Load continues with defaults", func(t *testing.T) {
cfg, err := Load(Options{Getenv: env(nil)})
if err != nil {
t.Fatalf("Load without HOME: %v", err)
}
if cfg.Audio.Sink != SinkOto {
t.Errorf("Audio.Sink = %q, want default %q", cfg.Audio.Sink, SinkOto)
}
})
}
// TestLoadProviderChain covers the provider pin (file/env), per-provider
// endpoints/models and the voice mapping.
func TestLoadProviderChain(t *testing.T) {
toml := `provider = "openai"
[providers.elevenlabs]
endpoint = "https://eleven.example.com"
[providers.openai]
endpoint = "https://openai.example.com"
model = "gpt-4o-mini-tts"
[providers.edge]
endpoint = "wss://edge.example.com/synth"
[voice.elevenlabs]
de = "MyGermanVoice"
en = "MyVoice"
[voice.edge]
de = "de-DE-KatjaNeural"
`
t.Run("TOML yields pin, endpoints and voice mapping", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, toml), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Provider != "openai" {
t.Errorf("Provider = %q, want %q", cfg.Provider, "openai")
}
if got := cfg.Providers["elevenlabs"].Endpoint; got != "https://eleven.example.com" {
t.Errorf("Providers[elevenlabs].Endpoint = %q", got)
}
if got := cfg.Providers["openai"].Model; got != "gpt-4o-mini-tts" {
t.Errorf("Providers[openai].Model = %q", got)
}
if got := cfg.Providers["edge"].Endpoint; got != "wss://edge.example.com/synth" {
t.Errorf("Providers[edge].Endpoint = %q", got)
}
if got := cfg.Voice["elevenlabs"]["de"]; got != "MyGermanVoice" {
t.Errorf("Voice[elevenlabs][de] = %q", got)
}
if got := cfg.Voice["elevenlabs"]["en"]; got != "MyVoice" {
t.Errorf("Voice[elevenlabs][en] = %q", got)
}
})
t.Run("TALK_PROVIDER beats the file pin", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, toml), Getenv: env(map[string]string{"TALK_PROVIDER": "edge"})})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Provider != "edge" {
t.Errorf("Provider = %q, want env override %q", cfg.Provider, "edge")
}
})
t.Run("nothing configured: no pin, no endpoints", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Provider != "" || cfg.Providers != nil || cfg.Voice != nil {
t.Errorf("provider configuration = %+v, want empty", cfg)
}
})
}
// TestLoadWrapLabel: the `talk wrap` announcement label, defaults → TOML →
// env in the usual layering.
func TestLoadWrapLabel(t *testing.T) {
t.Run("default empty (command base name)", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Wrap.Label != "" {
t.Errorf("Wrap.Label = %q, want empty (default)", cfg.Wrap.Label)
}
})
t.Run("TOML overrides default", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, "[wrap]\nlabel = \"Deployment\"\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Wrap.Label != "Deployment" {
t.Errorf("Wrap.Label = %q, want %q from TOML", cfg.Wrap.Label, "Deployment")
}
})
t.Run("TALK_WRAP_LABEL beats the file", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, "[wrap]\nlabel = \"Deployment\"\n"),
Getenv: env(map[string]string{"TALK_WRAP_LABEL": "Nightly Build"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Wrap.Label != "Nightly Build" {
t.Errorf("Wrap.Label = %q, want env override %q", cfg.Wrap.Label, "Nightly Build")
}
})
}
// TestLoadServerPort: daemon port by default (7800), TOML [server] port and
// env override TALK_PORT including validation.
func TestLoadServerPort(t *testing.T) {
t.Run("default 7800", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Server.Port != DefaultServerPort {
t.Errorf("Server.Port = %d, want default %d", cfg.Server.Port, DefaultServerPort)
}
})
t.Run("TOML overrides default", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, "[server]\nport = 7999\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Server.Port != 7999 {
t.Errorf("Server.Port = %d, want 7999 from TOML", cfg.Server.Port)
}
})
t.Run("TALK_PORT beats the file", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, "[server]\nport = 7999\n"),
Getenv: env(map[string]string{"TALK_PORT": "7912"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Server.Port != 7912 {
t.Errorf("Server.Port = %d, want env override 7912", cfg.Server.Port)
}
})
t.Run("invalid TALK_PORT is a clear error", func(t *testing.T) {
for _, bad := range []string{"null", "0", "-1", "70000"} {
_, err := Load(Options{File: missingFile(t), Getenv: env(map[string]string{"TALK_PORT": bad})})
if err == nil || !strings.Contains(err.Error(), "TALK_PORT") {
t.Fatalf("TALK_PORT=%q: error = %v, want a clear TALK_PORT error", bad, err)
}
}
})
}
func TestLoadAllowedOrigins(t *testing.T) {
t.Run("default: empty list (no CORS)", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(cfg.Server.AllowedOrigins) != 0 {
t.Errorf("AllowedOrigins = %v, want empty (door shut)", cfg.Server.AllowedOrigins)
}
})
t.Run("TOML list", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, "[server]\nallowed_origins = [\"https://chat.example.com\"]\n"),
Getenv: env(nil),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if strings.Join(cfg.Server.AllowedOrigins, "|") != "https://chat.example.com" {
t.Errorf("AllowedOrigins = %v", cfg.Server.AllowedOrigins)
}
})
t.Run("TALK_ALLOWED_ORIGINS beats the file (several, with spaces)", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, "[server]\nallowed_origins = [\"https://file.example\"]\n"),
Getenv: env(map[string]string{"TALK_ALLOWED_ORIGINS": "https://chat.example.com, https://grid.example"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
want := "https://chat.example.com|https://grid.example"
if strings.Join(cfg.Server.AllowedOrigins, "|") != want {
t.Errorf("AllowedOrigins = %v, want %v", cfg.Server.AllowedOrigins, want)
}
})
t.Run("TALK_ALLOWED_ORIGINS=off clears the file list", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, "[server]\nallowed_origins = [\"https://file.example\"]\n"),
Getenv: env(map[string]string{"TALK_ALLOWED_ORIGINS": "off"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(cfg.Server.AllowedOrigins) != 0 {
t.Errorf("AllowedOrigins = %v, want empty (off)", cfg.Server.AllowedOrigins)
}
})
t.Run("wildcard is an allowed value", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(map[string]string{"TALK_ALLOWED_ORIGINS": "*"})})
if err != nil {
t.Fatalf("Load: %v", err)
}
if strings.Join(cfg.Server.AllowedOrigins, "|") != "*" {
t.Errorf("AllowedOrigins = %v, want [*]", cfg.Server.AllowedOrigins)
}
})
t.Run("empty entries are clear errors", func(t *testing.T) {
for _, tc := range []struct {
name string
file string
envv string
}{
{"env", "", "https://a.example,,https://b.example"},
{"file", "[server]\nallowed_origins = [\"https://a.example\", \"\"]\n", ""},
} {
t.Run(tc.name, func(t *testing.T) {
file := missingFile(t)
if tc.file != "" {
file = writeTOML(t, tc.file)
}
_, err := Load(Options{File: file, Getenv: env(map[string]string{"TALK_ALLOWED_ORIGINS": tc.envv})})
if err == nil || !strings.Contains(err.Error(), "allowed_origins") && !strings.Contains(err.Error(), "ALLOWED_ORIGINS") {
t.Fatalf("error = %v, want a clear allowed_origins error", err)
}
})
}
})
}
// TestLoadLaws covers rate limit (default 30 s, "0s" = unlimited), high
// burst (default on) and quiet hours (default off; format validated) with
// env overrides and clear errors.
func TestLoadLaws(t *testing.T) {
t.Run("defaults: 30 s, burst on, quiet hours off, mode discard", func(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != DefaultRateLimit {
t.Errorf("Queue.RateLimit = %v, want default %v", cfg.Queue.RateLimit, DefaultRateLimit)
}
if !cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = false, want default true")
}
if cfg.QuietHours != "" {
t.Errorf("QuietHours = %q, want default empty (off)", cfg.QuietHours)
}
if cfg.QuietMode != queue.QuietModeDiscard {
t.Errorf("QuietMode = %q, want default %q", cfg.QuietMode, queue.QuietModeDiscard)
}
})
t.Run("TOML sets all law fields", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, `
quiet_hours = "18:00-08:00"
quiet_mode = "toast"
[queue]
rate_limit = "10s"
allow_high_burst = false
`),
Getenv: env(nil),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != 10*time.Second {
t.Errorf("Queue.RateLimit = %v, want 10s", cfg.Queue.RateLimit)
}
if cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = true, want false from TOML")
}
if cfg.QuietHours != "18:00-08:00" {
t.Errorf("QuietHours = %q, want window from TOML", cfg.QuietHours)
}
if cfg.QuietMode != queue.QuietModeToast {
t.Errorf("QuietMode = %q, want toast", cfg.QuietMode)
}
})
t.Run("partial [queue] table keeps defaults", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, "[queue]\nrate_limit = \"0s\"\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != 0 {
t.Errorf("Queue.RateLimit = %v, want 0 (unlimited)", cfg.Queue.RateLimit)
}
if !cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = false, want unchanged default true")
}
if cfg.QuietMode != queue.QuietModeDiscard {
t.Errorf("QuietMode = %q, want unchanged default discard", cfg.QuietMode)
}
})
t.Run("env overrides file (including off)", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, `
quiet_hours = "18:00-08:00"
quiet_mode = "discard"
[queue]
rate_limit = "10s"
allow_high_burst = false
`),
Getenv: env(map[string]string{
"TALK_RATE_LIMIT": "1s",
"TALK_ALLOW_HIGH_BURST": "true",
"TALK_QUIET_HOURS": "off",
"TALK_QUIET_MODE": "toast",
}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != time.Second {
t.Errorf("Queue.RateLimit = %v, want 1s from env", cfg.Queue.RateLimit)
}
if !cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = false, want true from env")
}
if cfg.QuietHours != "" {
t.Errorf("QuietHours = %q, want empty (off from env)", cfg.QuietHours)
}
if cfg.QuietMode != queue.QuietModeToast {
t.Errorf("QuietMode = %q, want toast from env", cfg.QuietMode)
}
})
t.Run("invalid law values are clear errors", func(t *testing.T) {
cases := []struct {
name string
toml string
envs map[string]string
want string
}{
{"TALK_RATE_LIMIT garbage", "", map[string]string{"TALK_RATE_LIMIT": "fast"}, "TALK_RATE_LIMIT"},
{"TALK_RATE_LIMIT negative", "", map[string]string{"TALK_RATE_LIMIT": "-5s"}, "TALK_RATE_LIMIT"},
{"rate_limit garbage in file", "[queue]\nrate_limit = \"garbage\"\n", nil, "rate_limit"},
{"TALK_ALLOW_HIGH_BURST garbage", "", map[string]string{"TALK_ALLOW_HIGH_BURST": "maybe"}, "TALK_ALLOW_HIGH_BURST"},
{"quiet_hours garbage in file", "quiet_hours = \"evenings\"\n", nil, "quiet_hours"},
{"TALK_QUIET_HOURS garbage", "", map[string]string{"TALK_QUIET_HOURS": "25:00-26:00"}, "TALK_QUIET_HOURS"},
{"TALK_QUIET_MODE garbage", "", map[string]string{"TALK_QUIET_MODE": "vibrate"}, "TALK_QUIET_MODE"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
file := missingFile(t)
if tc.toml != "" {
file = writeTOML(t, tc.toml)
}
_, err := Load(Options{File: file, Getenv: env(tc.envs)})
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want a clear error mentioning %q", err, tc.want)
}
})
}
})
}
// TestLoadDaemonAutostart covers [daemon] autostart: default on, TOML can
// switch it off, env TALK_AUTOSTART beats the file, garbage is an error.
func TestLoadDaemonAutostart(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if !cfg.Daemon.Autostart {
t.Error("Daemon.Autostart = false, want default true")
}
cfg, err = Load(Options{File: writeTOML(t, "[daemon]\nautostart = false\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Daemon.Autostart {
t.Error("Daemon.Autostart = true, want false from TOML")
}
cfg, err = Load(Options{
File: writeTOML(t, "[daemon]\nautostart = false\n"),
Getenv: env(map[string]string{"TALK_AUTOSTART": "true"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if !cfg.Daemon.Autostart {
t.Error("Daemon.Autostart = false, want true from env (env beats file)")
}
if _, err := Load(Options{File: missingFile(t), Getenv: env(map[string]string{"TALK_AUTOSTART": "maybe"})}); err == nil {
t.Error("TALK_AUTOSTART=maybe: no error, want a clear error")
}
}
// TestLoadLimits covers queue.max_text / queue.max_age: defaults 400 / 2m,
// TOML and env (env beats file), 0 = unlimited, garbage and negative values
// are clear errors.
func TestLoadLimits(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.MaxText != DefaultMaxText || cfg.Queue.MaxAge != DefaultMaxAge {
t.Errorf("defaults = %d/%v, want %d/%v", cfg.Queue.MaxText, cfg.Queue.MaxAge, DefaultMaxText, DefaultMaxAge)
}
cfg, err = Load(Options{File: writeTOML(t, "[queue]\nmax_text = 0\nmax_age = \"45s\"\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.MaxText != 0 || cfg.Queue.MaxAge != 45*time.Second {
t.Errorf("TOML = %d/%v, want 0/45s", cfg.Queue.MaxText, cfg.Queue.MaxAge)
}
cfg, err = Load(Options{
File: writeTOML(t, "[queue]\nmax_text = 100\n"),
Getenv: env(map[string]string{"TALK_MAX_TEXT": "250", "TALK_MAX_AGE": "0s"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.MaxText != 250 || cfg.Queue.MaxAge != 0 {
t.Errorf("env = %d/%v, want 250/0 (env beats file)", cfg.Queue.MaxText, cfg.Queue.MaxAge)
}
for name, kv := range map[string]map[string]string{
"TALK_MAX_TEXT=abc": {"TALK_MAX_TEXT": "abc"},
"TALK_MAX_TEXT=-1": {"TALK_MAX_TEXT": "-1"},
"TALK_MAX_AGE=soon": {"TALK_MAX_AGE": "soon"},
"TALK_MAX_AGE=-1s": {"TALK_MAX_AGE": "-1s"},
} {
if _, err := Load(Options{File: missingFile(t), Getenv: env(kv)}); err == nil {
t.Errorf("%s: no error, want a clear error", name)
}
}
if _, err := Load(Options{File: writeTOML(t, "[queue]\nmax_text = -5\n"), Getenv: env(nil)}); err == nil {
t.Error("max_text = -5 in TOML: no error, want a clear error")
}
}
// TestLoadLang covers the fallback language: default en, TOML, env
// TALK_LANG beats the file, anything but de/en is an error.
func TestLoadLang(t *testing.T) {
cfg, err := Load(Options{File: missingFile(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Lang != "en" {
t.Errorf("Lang = %q, want default en", cfg.Lang)
}
cfg, err = Load(Options{File: writeTOML(t, "lang = \"de\"\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Lang != "de" {
t.Errorf("Lang = %q, want de from TOML", cfg.Lang)
}
cfg, err = Load(Options{File: writeTOML(t, "lang = \"de\"\n"), Getenv: env(map[string]string{"TALK_LANG": "en"})})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Lang != "en" {
t.Errorf("Lang = %q, want en from env", cfg.Lang)
}
if _, err := Load(Options{File: missingFile(t), Getenv: env(map[string]string{"TALK_LANG": "fr"})}); err == nil || !strings.Contains(err.Error(), "lang") {
t.Errorf("TALK_LANG=fr: error = %v, want a clear lang error", err)
}
}