// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package speech import ( "net" "os" "path/filepath" "strings" "testing" ) func TestResolveServerAddressPrecedence(t *testing.T) { t.Setenv("SUDO_UID", "") runtimeDirectory := t.TempDir() cacheDirectory := t.TempDir() t.Setenv("XDG_RUNTIME_DIR", runtimeDirectory) t.Setenv("XDG_CACHE_HOME", cacheDirectory) t.Setenv("SPEECHD_SOCKET", "/tmp/legacy-speech.sock") t.Setenv("SPEECHD_ADDRESS", "unix_socket:/tmp/address-speech.sock") for _, test := range []struct { name, override, speechAddress, legacySocket, runtimeDir, want string }{ {"explicit", "/tmp/explicit-speech.sock", "unix_socket:/tmp/address-speech.sock", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/explicit-speech.sock"}, {"address", "", "unix_socket:/tmp/address-speech.sock", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/address-speech.sock"}, {"legacy", "", "", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/legacy-speech.sock"}, {"runtime", "", "", "", runtimeDirectory, filepath.Join(runtimeDirectory, "speech-dispatcher", "speechd.sock")}, {"cache", "", "", "", "", filepath.Join(cacheDirectory, "speech-dispatcher", "speechd.sock")}, } { t.Run(test.name, func(t *testing.T) { t.Setenv("SPEECHD_ADDRESS", test.speechAddress) t.Setenv("SPEECHD_SOCKET", test.legacySocket) t.Setenv("XDG_RUNTIME_DIR", test.runtimeDir) got, err := resolve_server_address(test.override) if err != nil || got.network != "unix" || got.address != test.want { t.Fatalf("address = %+v, error = %v; want %q", got, err, test.want) } }) } } func TestParseServerAddress(t *testing.T) { for _, test := range []struct { input, network, address string }{ {"unix_socket:/tmp/speech.sock", "unix", "/tmp/speech.sock"}, {"inet_socket", "tcp4", "127.0.0.1:6560"}, {"inet_socket:localhost:6561", "tcp4", "localhost:6561"}, {"inet_socket::6561", "tcp4", "127.0.0.1:6561"}, } { got, err := parse_server_address(test.input) if err != nil || got.network != test.network || got.address != test.address { t.Fatalf("parse %q = %+v, %v", test.input, got, err) } } for _, address := range []string{"invalid:/tmp/speech.sock", "unix_socket:relative.sock", "inet_socket:localhost:0", "inet_socket:localhost:bad"} { if _, err := parse_server_address(address); err == nil { t.Fatalf("accepted invalid address %q", address) } } } func TestSpeechAddressUsedByClientAndDiscovery(t *testing.T) { for _, network := range []string{"unix", "tcp4"} { t.Run(network, func(t *testing.T) { var server *fakeServer if network == "unix" { server = new_fake_server(t, false) } else { listener, err := net.Listen("tcp4", "127.0.0.1:0") if err != nil { t.Fatal(err) } server = new_fake_server_listener(t, false, listener) } address := "unix_socket:" + server.path if network == "tcp4" { address = "inet_socket:" + server.path } t.Setenv("SPEECHD_ADDRESS", address) t.Setenv("SPEECHD_SOCKET", "/absent.sock") t.Setenv("SPEECHD_CMD", "/nonexistent/speech-dispatcher") client, err := Prepare("speech-dispatcher", Options{Module: "fixture", Voice: "Base Voice"}) if err != nil { t.Fatal(err) } defer client.Close() if _, err := Discover(Options{}); err != nil { t.Fatal(err) } if err := client.Say("connection fixture"); err != nil { t.Fatal(err) } wait_event(t, server.events, "say:connection fixture") if err := client.Cancel(); err != nil { t.Fatal(err) } wait_event(t, server.events, "CANCEL 41") }) } } func TestInvalidAddressDoesNotFallBack(t *testing.T) { server := new_fake_server(t, false) t.Setenv("SPEECHD_ADDRESS", "unsupported:address") t.Setenv("SPEECHD_SOCKET", server.path) if _, err := Open(Options{}); err == nil || !strings.Contains(err.Error(), "unsupported") { t.Fatalf("error = %v", err) } } func TestDefaultSocketWithoutRuntimeOrCacheEnvironment(t *testing.T) { t.Setenv("SUDO_UID", "") t.Setenv("XDG_RUNTIME_DIR", "") t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", t.TempDir()) want := filepath.Join(os.Getenv("HOME"), ".cache", "speech-dispatcher", "speechd.sock") if got := default_socket_path(); got != want { t.Fatalf("socket %q, want %q", got, want) } } func TestAutomaticServerAddressHonorsOverrides(t *testing.T) { t.Setenv("SPEECHD_ADDRESS", "") t.Setenv("SPEECHD_SOCKET", "") if !automatic_server_address("") { t.Fatal("default address disables system fallback") } if automatic_server_address("/custom.sock") { t.Fatal("explicit socket allows system fallback") } t.Setenv("SPEECHD_SOCKET", "/legacy.sock") if automatic_server_address("") { t.Fatal("legacy socket allows system fallback") } t.Setenv("SPEECHD_ADDRESS", "unix_socket:/address.sock") t.Setenv("SPEECHD_SOCKET", "") if automatic_server_address("") { t.Fatal("explicit address allows system fallback") } }