Files
mesh-catalog/modules/lemurs/cmd/lemurs-tools/lemurs_test.go
T
jochen 98eb3fe33c lemurs: the login manager holds node-login-manager, its config in the current format (hq ADR 0208)
The official package in place of lemurs-git, and /etc/lemurs/config.toml in lemurs
0.4's structure. It offers only the session scripts that modules place in
/etc/lemurs/wms and /etc/lemurs/wayland, never a package's bare desktop entry, which
skips the session's start. The service is enabled and never started, stopped or
restarted by a push.

The tools are the seat's sessions, the default session (lemurs's cache, through
sudo -n) and logins from the journal and lemurs's own log. The package swap from
lemurs-git is a one-off step for the operator, listed in the README.
2026-10-04 13:21:47 +02:00

145 lines
5.6 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"lemurs/internal/desktop"
)
// aMachine lays out a login manager in a directory: its configuration naming directories under it,
// two X scripts (one not executable), a package's desktop entry and a cache.
func aMachine(t *testing.T) (lemurs, string, *[]string) {
root := t.TempDir()
for _, d := range []string{"wms", "wayland", "xsessions", "wayland-sessions"} {
os.MkdirAll(filepath.Join(root, d), 0o755)
}
os.WriteFile(filepath.Join(root, "wms", "i3"), []byte("#!/bin/sh\n# the session\nexec /bin/sh \"$HOME/.xinitrc\"\n"), 0o755)
os.WriteFile(filepath.Join(root, "wms", "notes"), []byte("not a session\n"), 0o644)
os.WriteFile(filepath.Join(root, "xsessions", "i3.desktop"), []byte("[Desktop Entry]\nName=i3\nExec=i3\nType=Application\n"), 0o644)
os.WriteFile(filepath.Join(root, "cache"), []byte("i3\nop\n"), 0o644)
config := strings.Join([]string{
`tty = 2`, `cache_path = "` + root + `/cache"`,
`[x11]`, `x11_display = ":1"`, `scripts_path = "` + root + `/wms"`, `xsessions_path = "` + root + `/xsessions"`,
`[wayland]`, `scripts_path = "` + root + `/wayland"`, `wayland_sessions_path = "` + root + `/wayland-sessions"`,
}, "\n")
os.WriteFile(filepath.Join(root, "config.toml"), []byte(config), 0o644)
var ran []string
d := desktop.Desk{
Find: func() (*desktop.Session, error) { return nil, &desktop.NoSession{Reason: "none"} },
Run: func(_ context.Context, _ []string, stdin []byte, name string, args ...string) desktop.Result {
line := strings.Join(append([]string{name}, args...), " ")
ran = append(ran, line+" <<"+string(stdin))
if name == "journalctl" {
return desktop.Result{Stdout: journal}
}
return desktop.Result{}
},
}
return lemurs{d: d, config: filepath.Join(root, "config.toml"), log: filepath.Join(root, "lemurs.log"), uid: 1000}, root, &ran
}
const journal = `-- Boot a0 --
2026-10-02T13:08:44+02:00 host lemurs[1001]: gkr-pam: unable to locate daemon control file
2026-10-02T13:08:40+02:00 host lemurs[1001]: pam_unix(lemurs:auth): authentication failure; logname= uid=0 euid=0 tty=tty2 ruser= rhost= user=op
2026-10-02T13:08:44+02:00 host lemurs[2141]: pam_unix(lemurs:session): session opened for user op(uid=1000) by op(uid=0)
2026-10-02T18:00:01+02:00 host lemurs[2141]: pam_unix(lemurs:session): session closed for user op
`
func TestTheSessionsAreTheEntriesLemursOffersInItsOrder(t *testing.T) {
l, root, _ := aMachine(t)
got, err := l.sessions(context.Background(), desktop.Args{})
if err != nil {
t.Fatal(err)
}
s := got.(map[string]any)["sessions"].([]Session)
if len(s) != 3 || s[0].Source != "desktop-entry" || s[0].Exec != "i3" || s[1].Name != "i3" || s[1].Source != "script" {
t.Fatalf("%+v", s)
}
if s[1].Exec != `exec /bin/sh "$HOME/.xinitrc"` || !s[1].Offered {
t.Fatalf("a script answers what it runs: %+v", s[1])
}
if s[2].Name != "notes" || s[2].Executable || s[2].Offered {
t.Fatalf("a script that is not executable is not offered: %+v", s[2])
}
if d := got.(map[string]any)["default"].(Cached); d.Session != "i3" || d.Account != "op" {
t.Fatalf("%+v", d)
}
_ = root
}
func TestTheModulesConfigurationIsReadAsLemursReadsIt(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "config", "config.toml"))
if err != nil {
t.Fatal(err)
}
c := ParseConfig(string(raw))
want := Config{Cache: "/var/cache/lemurs", X11Scripts: "/etc/lemurs/wms", X11Sessions: "/etc/lemurs/xsessions",
WaylandScripts: "/etc/lemurs/wayland", WaylandEntries: "/etc/lemurs/wayland-sessions", Display: ":1", TTY: 2}
if c != want {
t.Fatalf("%+v", c)
}
}
func TestTheDefaultSessionIsOneLemursOffersAndIsWrittenThroughSudo(t *testing.T) {
l, root, ran := aMachine(t)
if _, err := l.defaultSession(context.Background(), desktop.Args{"session": "notes"}); err == nil {
t.Fatal("a session lemurs does not offer is refused")
}
if _, err := l.defaultSession(context.Background(), desktop.Args{"session": "i3", "account": "op; rm"}); err == nil {
t.Fatal("an account that is not a name is refused")
}
got, err := l.defaultSession(context.Background(), desktop.Args{"session": "i3"})
if err != nil {
t.Fatal(err)
}
if len(*ran) != 1 || (*ran)[0] != "sudo -n tee "+root+"/cache <<i3\nop\n" {
t.Fatalf("%q", *ran)
}
if !strings.Contains(asJSON(got), `"shown"`) {
t.Fatalf("it says when it shows: %s", asJSON(got))
}
l.uid = 0
*ran = nil
l.defaultSession(context.Background(), desktop.Args{"session": "i3"})
if !strings.HasPrefix((*ran)[0], "tee ") {
t.Fatalf("as root, no sudo: %q", *ran)
}
}
func TestLoginsAreReadFromTheJournalAndLemursLog(t *testing.T) {
l, root, _ := aMachine(t)
os.WriteFile(filepath.Join(root, "lemurs.log"), []byte(
"[2026-10-02T11:08:44Z INFO lemurs] Starting new session for 'op' in environment 'X { xinitrc_path: \"i3\" }'\n"+
"[2026-10-02T11:08:44Z INFO lemurs::auth] Login attempt for 'op'\n"), 0o644)
got, err := l.logins(context.Background(), desktop.Args{"limit": float64(2)})
if err != nil {
t.Fatal(err)
}
m := got.(map[string]any)
ev := m["events"].([]Login)
if len(ev) != 2 || ev[0].Event != "opened" || ev[1].Event != "closed" || ev[1].Account != "op" || m["cut"] != true {
t.Fatalf("the newest two, cut: %+v", ev)
}
all := ParseJournal(journal)
if len(all) != 3 || all[0].Event != "failed" || all[0].Account != "op" {
t.Fatalf("%+v", all)
}
st := m["started"].([]Started)
if len(st) != 1 || st[0].Environment != `X { xinitrc_path: "i3" }` {
t.Fatalf("%+v", st)
}
if _, err := l.logins(context.Background(), desktop.Args{"since": "-1d; reboot"}); err == nil {
t.Fatal("since is a time")
}
}
func asJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}