// Cthulhu screen reader. // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // You should have received a copy of the GNU General Public License along with // this program. If not, see . package remote import ( "bytes" "log/slog" "strings" "testing" "github.com/godbus/dbus/v5" ) func TestEndpointDiagnosticsDoNotLogRequestContents(t *testing.T) { const private = "Private game text 7a9e" var output bytes.Buffer logger := slog.New(slog.NewJSONHandler(&output, &slog.HandlerOptions{Level: slog.LevelDebug})) endpoint := newEndpoint(logger, &recordingHandler{result: true}, func(dbus.Sender) bool { return true }) endpoint.startAccepting() sender := dbus.Sender(":1.5") endpoint.PresentMessage(sender, private) endpoint.SpeakText(sender, private, true) endpoint.BrailleMessage(sender, private) endpoint.SpeakSsml(sender, private, private) endpoint.PresentAccessibleEvent(sender, private, private, private, private, private, private, private, 1, 1, private) if strings.Contains(output.String(), private) { t.Fatalf("debug diagnostics exposed request text: %q", output.String()) } if !strings.Contains(output.String(), `"method":"PresentAccessibleEvent"`) { t.Fatalf("debug diagnostics lost method outcome: %q", output.String()) } } func TestEndpointDispatchesSpeechMethods(t *testing.T) { handler := &recordingHandler{result: true} endpoint := authorizedEndpoint(handler) sender := dbus.Sender(":1.5") if accepted, err := endpoint.PresentMessage(sender, "Status"); err != nil || !accepted { t.Fatalf("PresentMessage = (%v, %v), want (true, nil)", accepted, err) } if accepted, err := endpoint.SpeakText(sender, "Queued", false); err != nil || !accepted { t.Fatalf("SpeakText queued = (%v, %v), want (true, nil)", accepted, err) } if accepted, err := endpoint.SpeakText(sender, "Now", true); err != nil || !accepted { t.Fatalf("SpeakText interrupting = (%v, %v), want (true, nil)", accepted, err) } if accepted, err := endpoint.CancelSpeech(sender); err != nil || !accepted { t.Fatalf("CancelSpeech = (%v, %v), want (true, nil)", accepted, err) } if accepted, err := endpoint.PresentAccessibleEvent(sender, "42:0:0", "focus", "Start", "button", "", "", "", 1, 3, "Game"); err != nil || !accepted { t.Fatalf("PresentAccessibleEvent = (%v, %v), want (true, nil)", accepted, err) } handler.mutex.Lock() defer handler.mutex.Unlock() if got := strings.Join(handler.presented, ","); got != "Status" { t.Fatalf("presented = %q, want Status", got) } if got := strings.Join(handler.spoken, ","); got != "Queued,Now" { t.Fatalf("spoken = %q, want Queued,Now", got) } if len(handler.interrupts) != 2 || handler.interrupts[0] || !handler.interrupts[1] { t.Fatalf("interrupts = %v, want [false true]", handler.interrupts) } if handler.canceled != 1 { t.Fatalf("canceled = %d, want 1", handler.canceled) } if len(handler.wine) != 1 || handler.wine[0].Name != "Start" || handler.wine[0].Count != 3 { t.Fatalf("wine event = %+v", handler.wine) } } func TestEndpointLogsMethodsAndOutcomes(t *testing.T) { var output bytes.Buffer logger := slog.New(slog.NewTextHandler(&output, &slog.HandlerOptions{Level: slog.LevelDebug})) endpoint := newEndpoint(logger, &recordingHandler{result: true}, func(dbus.Sender) bool { return true }) endpoint.startAccepting() if accepted, err := endpoint.SpeakText(dbus.Sender(":1.9"), "Prism message", true); err != nil || !accepted { t.Fatalf("SpeakText = (%v, %v), want (true, nil)", accepted, err) } if accepted, err := endpoint.PresentAccessibleEvent( dbus.Sender(":1.9"), "window:1", "focus", "Start", "button", "Ready", "focused", "Description", 1, 3, "Game", ); err != nil || !accepted { t.Fatalf("PresentAccessibleEvent = (%v, %v), want (true, nil)", accepted, err) } logged := output.String() for _, expected := range []string{ "method=SpeakText", "method=PresentAccessibleEvent", "result=true", } { if !strings.Contains(logged, expected) { t.Fatalf("debug log missing %q:\n%s", expected, logged) } } for _, private := range []string{"Prism message", "Start", "Ready", "Description"} { if strings.Contains(logged, private) { t.Fatalf("debug log exposed %q:\n%s", private, logged) } } } func TestEndpointRejectsUnavailableInvalidAndOversizedRequests(t *testing.T) { handler := &recordingHandler{result: false} endpoint := authorizedEndpoint(handler) sender := dbus.Sender(":1.6") if accepted, _ := endpoint.BrailleMessage(sender, "Braille"); accepted { t.Fatal("BrailleMessage succeeded without a braille backend") } if accepted, _ := endpoint.SpeakText(sender, "", false); accepted { t.Fatal("SpeakText accepted empty text") } if accepted, _ := endpoint.SpeakText(sender, strings.Repeat("x", maxTextBytes+1), false); accepted { t.Fatal("SpeakText accepted oversized text") } handler.mutex.Lock() defer handler.mutex.Unlock() if len(handler.spoken) != 0 { t.Fatalf("invalid speech reached handler: %v", handler.spoken) } if got := strings.Join(handler.braille, ","); got != "Braille" { t.Fatalf("braille handler calls = %q, want Braille", got) } } func TestEndpointRejectsUnauthorizedAndStoppedCalls(t *testing.T) { handler := &recordingHandler{result: true} endpoint := newEndpoint(slog.New(slog.DiscardHandler), handler, func(dbus.Sender) bool { return false }) endpoint.startAccepting() if accepted, err := endpoint.SpeakText(dbus.Sender(":1.7"), "Private", false); accepted || err == nil || err.Name != "org.freedesktop.DBus.Error.AccessDenied" { t.Fatalf("SpeakText = (%v, %v), want access denied", accepted, err) } idle := endpoint.stopAccepting() select { case <-idle: default: t.Fatal("idle endpoint did not stop immediately") } if accepted, err := endpoint.CancelSpeech(dbus.Sender(":1.7")); accepted || err == nil || err.Name != "org.stormux.Cthulhu1.Error.Unavailable" { t.Fatalf("CancelSpeech = (%v, %v), want unavailable", accepted, err) } } func TestSpeakSsmlMatchesUnsupportedCompatibilityContract(t *testing.T) { endpoint := authorizedEndpoint(&recordingHandler{result: true}) sender := dbus.Sender(":1.8") tests := []struct { name string ssml string want int32 }{ {name: "valid", ssml: `Hello`, want: windowsErrorNotSupported}, {name: "namespace", ssml: `Hello`, want: windowsErrorNotSupported}, {name: "malformed", ssml: ``, want: windowsErrorInvalidParameter}, {name: "wrong root", ssml: `Hello`, want: windowsErrorInvalidParameter}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := endpoint.SpeakSsml(sender, "request", test.ssml) if err != nil || got != test.want { t.Fatalf("SpeakSsml = (%d, %v), want (%d, nil)", got, err, test.want) } }) } } func TestSpeechManagerAuthorizationAndShutdown(t *testing.T) { handler := &recordingHandler{result: true} endpoint := newEndpoint(nil, handler, func(dbus.Sender) bool { return false }) endpoint.startAccepting() manager := &speechManagerEndpoint{endpoint: endpoint} if accepted, err := manager.InterruptSpeech(":1.5", true); accepted || err == nil || err.Name != "org.freedesktop.DBus.Error.AccessDenied" { t.Fatalf("unauthorized InterruptSpeech: accepted=%v error=%v", accepted, err) } endpoint.authorize = func(dbus.Sender) bool { return true } if accepted, err := manager.InterruptSpeech(":1.5", false); !accepted || err != nil { t.Fatalf("authorized call: %v, %v", accepted, err) } <-endpoint.stopAccepting() if accepted, err := manager.InterruptSpeech(":1.5", true); accepted || err == nil { t.Fatal("call accepted after shutdown") } if handler.canceled != 1 { t.Fatalf("cancellations = %d", handler.canceled) } }