package desktop import ( "context" "encoding/json" "net" "os" "path/filepath" "strconv" "strings" "testing" ) // A machine in a directory: a process table, the X servers' socket directory and a runtime base. type fakeMachine struct { t *testing.T proc, x11, runtime string uid int } func newMachine(t *testing.T) *fakeMachine { root, err := os.MkdirTemp("", "desk") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(root) }) m := &fakeMachine{t: t, proc: filepath.Join(root, "p"), x11: filepath.Join(root, "x"), runtime: filepath.Join(root, "r"), uid: os.Getuid()} for _, d := range []string{m.proc, m.x11, filepath.Join(m.runtime, strconv.Itoa(m.uid))} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } return m } func (m *fakeMachine) socket(path string) { l, err := net.Listen("unix", path) if err != nil { m.t.Fatal(err) } m.t.Cleanup(func() { l.Close() }) } func (m *fakeMachine) process(pid int, comm string, start int, env ...string) { dir := filepath.Join(m.proc, strconv.Itoa(pid)) if err := os.MkdirAll(dir, 0o755); err != nil { m.t.Fatal(err) } os.WriteFile(filepath.Join(dir, "environ"), []byte(strings.Join(env, "\x00")+"\x00"), 0o600) os.WriteFile(filepath.Join(dir, "comm"), []byte(comm+"\n"), 0o644) // pid (comm) state ppid pgrp session tty tpgid flags minflt cminflt majflt cmajflt utime stime // cutime cstime priority nice threads itrealvalue starttime ... stat := strconv.Itoa(pid) + " (" + comm + ") S 1 1 1 0 -1 0 0 0 0 0 0 0 0 0 20 0 1 0 " + strconv.Itoa(start) + " 0 0" os.WriteFile(filepath.Join(dir, "stat"), []byte(stat), 0o644) } func (m *fakeMachine) finder(logind func(string) (map[string]string, error), prefer ...string) Finder { return Finder{Proc: m.proc, X11Sockets: m.x11, RuntimeBase: m.runtime, UID: m.uid, Prefer: prefer, Logind: logind} } func active(id string) (map[string]string, error) { return map[string]string{"Active": "yes", "State": "active", "Remote": "no", "Type": "x11"}, nil } func TestTheSessionIsFoundInTheWindowManagersEnvironmentAndOnlyItsWordsAreRead(t *testing.T) { m := newMachine(t) m.socket(filepath.Join(m.x11, "X1")) run := filepath.Join(m.runtime, strconv.Itoa(m.uid)) m.socket(filepath.Join(run, "bus")) m.process(100, "lemurs-child", 5, "DISPLAY=:1", "XDG_SESSION_ID=1") m.process(200, "i3", 10, "DISPLAY=:1", "XAUTHORITY=/home/op/.Xauthority", "XDG_SESSION_ID=1", "XDG_SESSION_TYPE=x11", "XDG_CURRENT_DESKTOP=i3", "XDG_RUNTIME_DIR="+run, "DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/dbus-private", "NPM_TOKEN=secret", "OPENAI_API_KEY=secret") m.process(300, "zsh", 50, "TERM=xterm") // no display: not a candidate s, err := m.finder(active, "i3").Find() if err != nil { t.Fatal(err) } if s.FoundIn.PID != 200 || s.Display != ":1" || s.XAuthority != "/home/op/.Xauthority" || s.ID != "1" || s.Type != "x11" || s.Desktop != "i3" { t.Fatalf("session: %+v", s) } if s.Bus != "unix:path="+filepath.Join(run, "bus") || s.SessionBus != "unix:path=/tmp/dbus-private" { t.Fatalf("the user manager's bus first, the session's private one reported beside it: %q %q", s.Bus, s.SessionBus) } if s.Active == nil || !*s.Active { t.Fatal("logind's word is carried") } env := strings.Join(s.Env([]string{"PATH=/usr/bin", "DISPLAY=:9", "HOME=/home/op"}), "\n") for _, want := range []string{"PATH=/usr/bin", "HOME=/home/op", "DISPLAY=:1", "XAUTHORITY=/home/op/.Xauthority", "DBUS_SESSION_BUS_ADDRESS=unix:path=" + filepath.Join(run, "bus"), "XDG_RUNTIME_DIR=" + run} { if !strings.Contains(env, want) { t.Errorf("env lacks %s:\n%s", want, env) } } if strings.Contains(env, ":9") || strings.Contains(env, "secret") || strings.Contains(env, "NPM_TOKEN") { t.Fatalf("the base's display is replaced and no other word of the session's process passes:\n%s", env) } b, _ := json.Marshal(s) if strings.Contains(string(b), "secret") { t.Fatal("the answer carries a word outside the session's") } } func TestWithoutAPreferenceTheOldestProcessOfTheLiveSessionWins(t *testing.T) { m := newMachine(t) m.socket(filepath.Join(m.x11, "X0")) m.process(410, "xterm", 90, "DISPLAY=:0", "XDG_SESSION_ID=3") m.process(400, "openbox", 20, "DISPLAY=:0", "XDG_SESSION_ID=3") s, err := m.finder(nil).Find() if err != nil || s.FoundIn.PID != 400 { t.Fatalf("%+v %v", s, err) } if s.RuntimeDir != filepath.Join(m.runtime, strconv.Itoa(m.uid)) || s.Bus != "" { t.Fatalf("an absent runtime directory word falls back to the account's, and no bus socket means no bus: %+v", s) } } func TestALeftoverProcessOfAnEndedSessionIsNotTheSession(t *testing.T) { m := newMachine(t) m.process(500, "i3", 10, "DISPLAY=:2", "XDG_SESSION_ID=7") // no X2 socket _, err := m.finder(active, "i3").Find() if !IsNoSession(err) || !strings.Contains(err.Error(), "socket is gone") { t.Fatalf("%v", err) } var answer map[string]any if json.Unmarshal([]byte(err.Error()), &answer) != nil || answer["error"] != "no-graphical-session" { t.Fatalf("the refusal is structured: %s", err) } } func TestNoProcessWithADisplayIsAClearNoSession(t *testing.T) { m := newMachine(t) m.process(600, "sshd", 1, "SSH_CONNECTION=x") _, err := m.finder(active).Find() if !IsNoSession(err) || !strings.Contains(err.Error(), "no process of uid") { t.Fatalf("%v", err) } } func TestAnActiveLocalSessionBeatsAnInactiveOneAndARemoteOneIsRefused(t *testing.T) { m := newMachine(t) m.socket(filepath.Join(m.x11, "X0")) m.socket(filepath.Join(m.x11, "X1")) m.process(700, "i3", 5, "DISPLAY=:0", "XDG_SESSION_ID=a") m.process(800, "i3", 9, "DISPLAY=:1", "XDG_SESSION_ID=b") logind := func(id string) (map[string]string, error) { if id == "a" { return map[string]string{"Active": "no", "State": "online", "Remote": "no"}, nil } return map[string]string{"Active": "yes", "State": "active", "Remote": "no"}, nil } s, err := m.finder(logind, "i3").Find() if err != nil || s.FoundIn.PID != 800 || s.Display != ":1" { t.Fatalf("the active session: %+v %v", s, err) } remote := func(string) (map[string]string, error) { return map[string]string{"Active": "yes", "Remote": "yes"}, nil } if _, err := m.finder(remote).Find(); !IsNoSession(err) { t.Fatalf("a remote session is not the operator's desktop: %v", err) } } func TestAWaylandSessionIsFoundByItsCompositorsSocket(t *testing.T) { m := newMachine(t) run := filepath.Join(m.runtime, strconv.Itoa(m.uid)) m.socket(filepath.Join(run, "wayland-1")) m.process(900, "sway", 3, "WAYLAND_DISPLAY=wayland-1", "XDG_RUNTIME_DIR="+run, "SWAYSOCK=/run/x.sock") s, err := m.finder(nil, "sway").Find() if err != nil || s.Type != "wayland" || s.WaylandDisplay != "wayland-1" { t.Fatalf("%+v %v", s, err) } if !strings.Contains(strings.Join(s.Env(nil), " "), "SWAYSOCK=/run/x.sock") { t.Fatal("the compositor's socket word passes") } } func TestADisplayOnAnotherHostIsNotTheLocalSession(t *testing.T) { for d, want := range map[string]bool{":0": true, ":1.0": true, "unix:2": true, "localhost:10.0": false, "host:0": false, "": false, ":x": false} { if _, ok := DisplayNumber(d); ok != want { t.Errorf("%q: %v", d, ok) } } } func TestACommandIsBoundedAndItsFailureNamed(t *testing.T) { r := Exec(context.Background(), os.Environ(), []byte("hello"), "cat") if !r.OK() || r.Stdout != "hello" { t.Fatalf("%+v", r) } r = Exec(context.Background(), os.Environ(), nil, "sh", "-c", "echo no >&2; exit 3") if r.OK() || r.Code != 3 || !strings.Contains(r.Err().Error(), "exited 3: no") { t.Fatalf("%+v", r) } r = Exec(context.Background(), os.Environ(), nil, "no-such-program-here") if r.OK() || r.Code != 127 { t.Fatalf("%+v", r) } r = Exec(context.Background(), os.Environ(), nil, "sh", "-c", "head -c 400000 /dev/zero") if !r.Truncated || len(r.Stdout) != MostOutput { t.Fatalf("cut at %d: %d %v", MostOutput, len(r.Stdout), r.Truncated) } } func TestArgumentsAreReadStrictly(t *testing.T) { a := Args{"name": " x ", "n": float64(3), "f": 1.5, "b": true, "l": []any{"a", "b"}} if v, err := a.Text("name"); err != nil || v != "x" { t.Fatal(v, err) } if _, err := a.Text("missing"); err == nil { t.Fatal("a missing required text") } if n, err := a.Whole("n", 0, 1, 5); err != nil || n != 3 { t.Fatal(n, err) } if _, err := a.Whole("f", 0, 0, 5); err == nil { t.Fatal("1.5 is not whole") } if _, err := a.Whole("n", 0, 4, 5); err == nil { t.Fatal("out of range") } if b, given, err := a.Bool("b"); !b || !given || err != nil { t.Fatal("bool") } if _, _, err := a.Bool("name"); err == nil { t.Fatal("text is not a bool") } if l, err := a.Strings("l"); err != nil || len(l) != 2 { t.Fatal(l, err) } if _, err := a.OneOf("name", "", "y", "z"); err == nil { t.Fatal("not one of") } } func TestAPathIsKeptInsideTheHome(t *testing.T) { t.Setenv("MESH_OPERATOR_HOME", "/home/op") for in, want := range map[string]string{"~/a.png": "/home/op/a.png", "b/c": "/home/op/b/c", "/home/op/d": "/home/op/d", "~": "/home/op"} { if got, err := InHome(in); err != nil || got != want { t.Errorf("%s: %s %v", in, got, err) } } for _, out := range []string{"/etc/passwd", "~/../other", "../x"} { if _, err := InHome(out); err == nil { t.Errorf("%s was accepted", out) } } } func TestPropertiesAreParsed(t *testing.T) { p := ParseProperties("Active=yes\nState=active\nDisplay=\n") if p["Active"] != "yes" || p["State"] != "active" || p["Display"] != "" { t.Fatal(p) } }