Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func TestScreenLockRejectsLocalOutputButPreservesRemoteFeedback(t *testing.T) {
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
worker := newSpeechWorker(logger, nil)
|
||||
worker.running = true // Retain requests for inspection without backend I/O.
|
||||
worker.EnqueueStartup(speechRequest{Text: "Welcome", Cause: "startup"})
|
||||
display := &brailleWorker{cancel: func() {}, connected: true, wake: make(chan struct{}, 1)}
|
||||
display.EnqueueFrame(braille.Frame{Text: "Desktop", Cursor: 0})
|
||||
display.Flash("Notice", config.BrailleOptions{FlashMessages: true, FlashPersistent: true})
|
||||
commands := &commandGrabRecorder{}
|
||||
gate := newFocusGate(commands)
|
||||
gate.speech = worker
|
||||
gate.SetLocked(true, nil, nil, commands, worker, display)
|
||||
if len(worker.pending) != 0 || !worker.startupUntil.IsZero() || display.current().Text != "" || display.baseFrame.Text != "" {
|
||||
t.Fatal("lock retained desktop presentation or startup protection")
|
||||
}
|
||||
// A terminal focus transition must never reopen command grabs during lock.
|
||||
gate.SetSuspended(true)
|
||||
gate.SetSuspended(false)
|
||||
for _, enabled := range commands.grabs() {
|
||||
if enabled {
|
||||
t.Fatal("terminal focus restored grabs while locked")
|
||||
}
|
||||
}
|
||||
guarded := gatedSpeech{speech: worker, gate: gate}
|
||||
guarded.Enqueue(speechRequest{Text: "Private desktop", BypassFocusGate: true})
|
||||
presentNotifications(guarded, []string{"Notification ready"})
|
||||
worker.Enqueue(speechRequest{Text: "Notification", Cause: "notification"})
|
||||
if guarded.InterruptForInput(input.Event{Pressed: true}) {
|
||||
t.Fatal("password key interrupted locker feedback")
|
||||
}
|
||||
display.Enqueue("Private desktop")
|
||||
display.EnqueueMessage("Notification")
|
||||
if len(worker.pending) != 0 || display.current().Text != "" {
|
||||
t.Fatal("local output escaped secure mode")
|
||||
}
|
||||
remote := remoteSpeech{speech: worker, braille: lockFeedbackBraille{display}}
|
||||
if !remote.PresentMessage("Screen locked") || len(worker.pending) != 1 || worker.pending[0].Text != "Screen locked" || display.current().Text != "Screen locked" {
|
||||
t.Fatal("I38lock feedback was suppressed")
|
||||
}
|
||||
gate.SetLocked(false, nil, nil, commands, worker, display)
|
||||
if display.current().Text != "" {
|
||||
t.Fatal("unlock retained locker feedback")
|
||||
}
|
||||
guarded.Enqueue(speechRequest{Text: "Desktop resumed"})
|
||||
if len(worker.pending) != 2 || !commands.grabs()[len(commands.grabs())-1] {
|
||||
t.Fatal("unlock did not resume desktop presentation and grabs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScreenLockOwnerAuthenticationAndReplacement(t *testing.T) {
|
||||
var states []bool
|
||||
monitor := &screenLockMonitor{logger: slog.New(slog.DiscardHandler), setActive: func(active bool) { states = append(states, active) }, trustedOwner: func(_ context.Context, owner string) bool { return owner == ":1.42" }}
|
||||
for _, owner := range []string{":1.41", ":1.42", ":1.42", "", ":1.43"} {
|
||||
monitor.updateOwner(context.Background(), owner)
|
||||
}
|
||||
if !reflect.DeepEqual(states, []bool{true, false}) {
|
||||
t.Fatalf("lock transitions = %v", states)
|
||||
}
|
||||
}
|
||||
|
||||
// Executed under a copied test executable named i38lock, so /proc/PID/exe
|
||||
// exercises the production owner authentication rather than a mocked PID.
|
||||
func TestI38lockOwnerHelper(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_LOCK_HELPER") != "1" {
|
||||
t.Skip("subprocess helper")
|
||||
}
|
||||
connection, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
if reply, err := connection.RequestName(screenLockBusName, dbus.NameFlagDoNotQueue); err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
t.Fatalf("request lock name: %v, %v", reply, err)
|
||||
}
|
||||
fmt.Println("ready")
|
||||
var input [1]byte
|
||||
_, _ = os.Stdin.Read(input[:])
|
||||
_, _ = connection.ReleaseName(screenLockBusName)
|
||||
}
|
||||
|
||||
func TestNativeI38lockOwnerStartupUnlockAndSpoofing(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_DBUS_TEST") != "1" {
|
||||
t.Skip("requires private session bus")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(executable)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "i38lock")
|
||||
if err := os.WriteFile(path, data, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
process := exec.CommandContext(ctx, path, "-test.run=^TestI38lockOwnerHelper$")
|
||||
process.Env = append(os.Environ(), "CTHULHU_LOCK_HELPER=1")
|
||||
stdout, err := process.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stdin, err := process.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
process.Stderr = os.Stderr
|
||||
if err := process.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer process.Process.Kill()
|
||||
line, err := bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil || line != "ready\n" {
|
||||
t.Fatalf("helper ready = %q, %v", line, err)
|
||||
}
|
||||
states := make(chan bool, 8)
|
||||
monitor := &screenLockMonitor{logger: slog.New(slog.DiscardHandler), setActive: func(active bool) { states <- active }}
|
||||
if err := monitor.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer monitor.Stop(context.Background())
|
||||
expect := func(want bool) {
|
||||
t.Helper()
|
||||
select {
|
||||
case got := <-states:
|
||||
if got != want {
|
||||
t.Fatalf("secure state = %v, want %v", got, want)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("missing secure state transition")
|
||||
}
|
||||
}
|
||||
expect(true) // Reader started after the screen was already locked.
|
||||
stdin.Close()
|
||||
if err := process.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect(false)
|
||||
spoof, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer spoof.Close()
|
||||
if _, err := spoof.RequestName(screenLockBusName, dbus.NameFlagDoNotQueue); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-states:
|
||||
t.Fatal("untrusted owner changed secure state")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
_, _ = spoof.ReleaseName(screenLockBusName)
|
||||
// Verify a trusted owner appearing after subscription as well as startup.
|
||||
process = exec.CommandContext(ctx, path, "-test.run=^TestI38lockOwnerHelper$")
|
||||
process.Env = append(os.Environ(), "CTHULHU_LOCK_HELPER=1")
|
||||
stdout, err = process.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stdin, err = process.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := process.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer process.Process.Kill()
|
||||
line, err = bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil || line != "ready\n" {
|
||||
t.Fatalf("helper ready = %q, %v", line, err)
|
||||
}
|
||||
expect(true)
|
||||
stdin.Close()
|
||||
if err := process.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect(false)
|
||||
}
|
||||
Reference in new issue
Block a user