185 lines
6.4 KiB
Go
185 lines
6.4 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/config"
|
|
"git.stormux.org/storm/fenrir/internal/control"
|
|
"git.stormux.org/storm/fenrir/internal/input"
|
|
"git.stormux.org/storm/fenrir/internal/screen"
|
|
)
|
|
|
|
func TestLoginPromptStartupAndDelayedActivation(t *testing.T) {
|
|
for _, terminal := range []int{1, 2} {
|
|
// tty1 already has its boot login prompt. tty2's login program
|
|
// opens the console later, without a keyboard or screen event.
|
|
prompt := screen.Snapshot{Terminal: terminal, Active: true, Rows: 1, Cols: 7, Cells: []rune("login: ")}
|
|
source := &fakeSource{snapshot: prompt}
|
|
if terminal == 2 {
|
|
source.snapshot = screen.Snapshot{Terminal: terminal}
|
|
}
|
|
voice := &fakeVoice{}
|
|
var modes []bool
|
|
keys := &fakeKeys{poll: func(active bool, _ func(input.Decision)) error {
|
|
modes = append(modes, active)
|
|
return nil
|
|
}}
|
|
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: config.Defaults()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(a.ui.close)
|
|
if err := a.read_screen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := a.poll_input(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if terminal == 2 {
|
|
if a.lastActive || len(voice.messages) != 0 {
|
|
t.Fatal("unused console was captured or read", a.lastActive, voice.messages)
|
|
}
|
|
source.snapshot = prompt
|
|
if err := a.poll_input(false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
wantModes := []bool{true}
|
|
if terminal == 2 {
|
|
wantModes = []bool{false, true}
|
|
}
|
|
if !a.lastActive || !reflect.DeepEqual(modes, wantModes) || !strings.Contains(strings.Join(voice.messages, "|"), "login:") {
|
|
t.Fatal("login prompt did not resume input and speech", terminal, modes, voice.messages)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Model the console's policy boundary independently of the shared application.
|
|
type policySource struct {
|
|
fakeSource
|
|
ignoredVTs []int
|
|
}
|
|
|
|
func (s *policySource) Configure_ignored_vts(numbers []int) {
|
|
s.ignoredVTs = append([]int(nil), numbers...)
|
|
}
|
|
|
|
func (s *policySource) Read() (screen.Snapshot, error) {
|
|
for _, number := range s.ignoredVTs {
|
|
if number == s.snapshot.Terminal {
|
|
return screen.Snapshot{Terminal: number}, nil
|
|
}
|
|
}
|
|
return s.fakeSource.Read()
|
|
}
|
|
|
|
func (s *policySource) State() (int, bool, bool, error) {
|
|
snapshot, err := s.Read()
|
|
return snapshot.Terminal, snapshot.Active, snapshot.NumLock, err
|
|
}
|
|
|
|
func TestRemoteIgnoredVTListsAreAtomic(t *testing.T) {
|
|
base := config.Defaults()
|
|
base.Screen.IgnoredVTs = []int{2, 3}
|
|
for _, text := range []string{
|
|
"SET screen#ignore_screen=7, 8,10;speech#enabled=False",
|
|
"SET screen#ignore_screen=7, 8,10,speech#enabled=False",
|
|
} {
|
|
updated, err := remote_settings(base, text)
|
|
if err != nil || !reflect.DeepEqual(updated.Screen.IgnoredVTs, []int{7, 8, 10}) || updated.Presentation.Speech.Enabled {
|
|
t.Fatal("lost list or following assignment", updated, err)
|
|
}
|
|
}
|
|
for _, value := range []string{"0", "64", "-1", "7,7", "7,", "7,,8", "tty7", "7;unknown#setting=True"} {
|
|
failed, err := remote_settings(base, "SET speech#enabled=False;screen#ignore_screen="+value)
|
|
if err == nil || !reflect.DeepEqual(failed, base) || !reflect.DeepEqual(base.Screen.IgnoredVTs, []int{2, 3}) {
|
|
t.Fatal("invalid list partially applied", value, failed, base, err)
|
|
}
|
|
}
|
|
updated, err := remote_settings(base, "SET screen#ignore_screen=")
|
|
if err != nil || len(updated.Screen.IgnoredVTs) != 0 {
|
|
t.Fatal("empty list did not clear policy", updated.Screen, err)
|
|
}
|
|
}
|
|
|
|
func TestIgnoredVTStartupRemoteReleaseRecoveryAndTerminalIsolation(t *testing.T) {
|
|
for _, terminalMode := range []bool{false, true} {
|
|
settings := config.Defaults()
|
|
settings.Screen.IgnoredVTs = []int{2, 7, 8, 10}
|
|
source := &policySource{fakeSource: fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 5, Cells: []rune("alpha")}}}
|
|
voice := &fakeVoice{}
|
|
var modes []bool
|
|
keys := &fakeKeys{poll: func(active bool, _ func(input.Decision)) error {
|
|
modes = append(modes, active)
|
|
return nil
|
|
}}
|
|
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: settings, TerminalMode: terminalMode})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(a.ui.close)
|
|
if err := a.read_screen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := a.poll_input(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.lastActive != terminalMode || (!terminalMode && len(voice.messages) != 0) {
|
|
t.Fatal("wrong startup policy", terminalMode, a.lastActive, voice.messages)
|
|
}
|
|
request := func(uid int, text string) string {
|
|
t.Helper()
|
|
replies := make(chan string, 1)
|
|
a.remote_request(control.Request{UID: uid, Text: "SETTING " + text, Reply: replies})
|
|
return <-replies
|
|
}
|
|
if reply := request(os.Geteuid()+1, "SET screen#ignore_screen="); reply == "" || !reflect.DeepEqual(a.settings.live.Screen, settings.Screen) {
|
|
t.Fatal("unauthorized policy change", reply, a.settings.live.Screen)
|
|
}
|
|
if reply := request(os.Geteuid(), "SET screen#ignore_screen="); reply != "" {
|
|
t.Fatal(reply)
|
|
}
|
|
if err := a.poll_input(true); err != nil || !a.lastActive {
|
|
t.Fatal("did not recover after remote clear", err)
|
|
}
|
|
a.ui.open_tutorial(nil, nil, nil)
|
|
interrupts := voice.interrupts
|
|
if reply := request(os.Geteuid(), "SET screen#ignore_screen=2,7,8,10"); reply != "" {
|
|
t.Fatal(reply)
|
|
}
|
|
if err := a.poll_input(false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !terminalMode && (a.lastActive || a.ui.active || voice.interrupts <= interrupts || !reflect.DeepEqual(keys.captures, []bool{true, false})) {
|
|
t.Fatal("handoff retained modal capture or speech", a.lastActive, a.ui.active, keys.captures, voice.interrupts)
|
|
}
|
|
if terminalMode && (!a.lastActive || !a.ui.active) {
|
|
t.Fatal("console policy affected X11 session")
|
|
}
|
|
source.snapshot.Terminal = 3
|
|
if err := a.poll_input(false); err != nil || !a.lastActive {
|
|
t.Fatal("allowed VT did not resume", err)
|
|
}
|
|
if !terminalMode && !reflect.DeepEqual(modes, []bool{false, true, false, true}) {
|
|
t.Fatal("wrong handoff order", modes)
|
|
}
|
|
// Applying a saved draft uses the same policy; quick-save must retain it.
|
|
saved := settings
|
|
saved.Screen.IgnoredVTs = []int{3}
|
|
a.apply_saved_settings(saved, false)
|
|
if err := a.poll_input(false); err != nil || a.lastActive != terminalMode {
|
|
t.Fatal("saved policy was not applied", err, a.lastActive)
|
|
}
|
|
if !reflect.DeepEqual(a.settings.saveDraft.Screen.IgnoredVTs, []int{3}) {
|
|
t.Fatal("lost saved policy")
|
|
}
|
|
}
|
|
}
|