208 lines
8.1 KiB
Go
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)
|
|
}
|
|
}
|