package config import "testing" // The Piper instances are discovered from the environment rather than named in // code, so that a new pair costs a compose service and two .env lines. These // assert the discovery rule, including the two shapes that already exist in the // wild (millenia's systemd unit and the VPS compose file). func TestTTSVoicesDiscovery(t *testing.T) { voices := ttsVoices([]string{ "TTS_ENDPOINT=http://piper-en:5000", "TTS_VOICE_EN=en_US-amy-medium", "TTS_ENDPOINT_ZH=http://piper-zh:5000/", "TTS_VOICE_ZH=zh_CN-huayan-medium", "TTS_ENDPOINT_PT=http://piper-pt:5000", "TTS_VOICE_PT=pt_PT-tugão-medium", "TTS_ENDPOINT_FR=http://piper-fr:5000", "TTS_VOICE_FR=fr_FR-siwis-medium", "TTS_ENDPOINT_ES=http://piper-es:5000", "TTS_VOICE_ES=es_MX-ald-medium", // Noise that must not become a language. "TTS_PATH=/synthesize", "PATH=/usr/bin", }) want := map[string]TTSVoice{ "en": {"http://piper-en:5000", "en_US-amy-medium"}, // The trailing slash is trimmed here so the synthesis path concatenates // cleanly rather than producing a double slash at every call site. "zh": {"http://piper-zh:5000", "zh_CN-huayan-medium"}, "pt": {"http://piper-pt:5000", "pt_PT-tugão-medium"}, // Phase 24's whole TTS change: a fourth language costs two lines here // and a compose service, and no Go at all. "fr": {"http://piper-fr:5000", "fr_FR-siwis-medium"}, // And a fifth cost exactly the same, which is the claim actually being // tested. The voice is Mexican on purpose: the es pack is Latin // American, and es_ES-davefx-medium would read it in the wrong accent. "es": {"http://piper-es:5000", "es_MX-ald-medium"}, } if len(voices) != len(want) { t.Fatalf("discovered %v, want %v", voices, want) } for lang, w := range want { if voices[lang] != w { t.Errorf("%s = %+v, want %+v", lang, voices[lang], w) } } } // Half a configuration is not a language. An endpoint with no voice (or the // reverse) must read as "no voice for this language" — a 404 the client answers // by falling back to Web Speech — rather than as an instance that exists and // errors on every request. func TestTTSVoicesIgnoresHalfConfiguredLanguages(t *testing.T) { voices := ttsVoices([]string{ "TTS_ENDPOINT=http://piper-en:5000", "TTS_VOICE_EN=en_US-amy-medium", "TTS_ENDPOINT_FR=http://piper-fr:5000", // no TTS_VOICE_FR "TTS_VOICE_ES=es_ES-davefx-medium", // no TTS_ENDPOINT_ES }) if _, ok := voices["fr"]; ok { t.Errorf("fr routed with no voice configured") } if _, ok := voices["es"]; ok { t.Errorf("es routed with no endpoint configured") } if len(voices) != 1 { t.Errorf("discovered %v, want English only", voices) } } // A deployment that predates the map names only the endpoints and relies on the // voice defaults; it must sound exactly as it did. func TestTTSVoicesKeepsTheOriginalDefaults(t *testing.T) { voices := ttsVoices([]string{ "TTS_ENDPOINT=http://127.0.0.1:5005", "TTS_ENDPOINT_ZH=http://127.0.0.1:5006", }) if got := voices["en"].Voice; got != "en_US-amy-medium" { t.Errorf("en voice = %q, want the default", got) } if got := voices["zh"].Voice; got != "zh_CN-huayan-medium" { t.Errorf("zh voice = %q, want the default", got) } } // Read-aloud is off when no English instance is configured; nothing else may // switch it on. (tts.New gates on TTSEndpoint, so a stray TTS_ENDPOINT_PT with // no English sibling must not produce a routable map that outlives that gate.) func TestTTSVoicesEmptyWithoutEndpoints(t *testing.T) { if voices := ttsVoices([]string{"TTS_VOICE_EN=en_US-amy-medium"}); len(voices) != 0 { t.Errorf("discovered %v, want none", voices) } } // The fallback to the single local user is right on a laptop and a catastrophe // on a public host, so it is defaulted from the origin Petal knows itself by // rather than left to be remembered. func TestRequireAuthDefaultsFromBaseURL(t *testing.T) { cases := []struct { baseURL string want bool }{ {"http://localhost:8080", false}, {"http://127.0.0.1:8080", false}, {"http://[::1]:8080", false}, {"", false}, // no BASE_URL set at all: the local-dev default {"https://petal.parodia.dev", true}, {"http://petal.parodia.dev", true}, {"https://petal.example.com/", true}, } for _, c := range cases { t.Setenv("BASE_URL", c.baseURL) t.Setenv("PETAL_REQUIRE_AUTH", "") if got := Load().RequireAuth; got != c.want { t.Errorf("BASE_URL=%q: RequireAuth=%v, want %v", c.baseURL, got, c.want) } } } // The default is a default, not a rule: a trusted private network is a real // deployment shape, and so is wanting the guard on locally. func TestRequireAuthExplicitOverride(t *testing.T) { t.Setenv("BASE_URL", "https://petal.parodia.dev") t.Setenv("PETAL_REQUIRE_AUTH", "false") if Load().RequireAuth { t.Error("an explicit false must be honoured on a public origin") } t.Setenv("BASE_URL", "http://localhost:8080") t.Setenv("PETAL_REQUIRE_AUTH", "true") if !Load().RequireAuth { t.Error("an explicit true must be honoured on localhost") } // A typo must not be the thing that disables the guard. t.Setenv("BASE_URL", "https://petal.parodia.dev") t.Setenv("PETAL_REQUIRE_AUTH", "nope") if !Load().RequireAuth { t.Error("an unparseable value must keep the default, not read as false") } }