Files

208 lines
8.1 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
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: `<speak>Hello</speak>`, want: windowsErrorNotSupported},
{name: "namespace", ssml: `<speak xmlns="urn:example">Hello</speak>`, want: windowsErrorNotSupported},
{name: "malformed", ssml: `<speak>`, want: windowsErrorInvalidParameter},
{name: "wrong root", ssml: `<voice>Hello</voice>`, 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)
}
}