Files
mesh-host/internal/accounts/accounts_test.go
T
jochen ab4ca44f98
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/module-groups delivered: every member is delivered
Give back only the groups the mesh added, and say when a new login is needed (hq ADR 0252, issue 247)
A module puts the operator's account in a group by declaring the account with that group alone.
The node-engine now records each group it added, takes back only those when nothing declared still
asks for them, refuses a group the machine lacks before usermod runs, and states each such account
as its module's resource of kind account: relogin needed while the running session lacks the group.
2026-10-08 12:04:08 +02:00

166 lines
5.5 KiB
Go

package accounts
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/novox/mesh-host/internal/declaration"
)
// The account judge (novox/hq ADR 0252, "how it is checked"): an account in a group its running session
// lacks is unhealthy with "relogin needed"; the same account with no session, or a session that has the
// group, is healthy; one the database does not list in the group is unhealthy saying so; and everything
// asked of the machine is a read.
// machine is a fake: the database's groups, the manager's process and its status file.
type machine struct {
t *testing.T
proc string
inDB string
pid string
held string // the Groups line of the manager's status
asked []string
failsID bool
}
func (m *machine) run(_ context.Context, name string, args ...string) (string, error) {
m.asked = append(m.asked, name+" "+strings.Join(args, " "))
switch {
case name == "id":
if m.failsID {
return "", errors.New("id exited 1")
}
return m.inDB + "\n", nil
case name == "getent" && args[0] == "passwd":
return "operator:x:1500:1500::/home/operator:/bin/zsh\n", nil
case name == "getent" && args[0] == "group":
switch args[1] {
case "openrazer":
return "openrazer:x:964:operator\n", nil
case "wheel":
return "wheel:x:998:operator\n", nil
}
return "", errors.New("getent exited 2: ")
case name == "systemctl":
return m.pid + "\n", nil
}
return "", errors.New("not a command the judge may run")
}
func (m *machine) status() {
m.t.Helper()
if m.pid == "" || m.pid == "0" {
return
}
dir := filepath.Join(m.proc, m.pid)
if err := os.MkdirAll(dir, 0o755); err != nil {
m.t.Fatal(err)
}
body := "Name:\tsystemd\nUid:\t1500\t1500\t1500\t1500\nGid:\t1500\t1500\t1500\t1500\nGroups:\t" + m.held + "\n"
if err := os.WriteFile(filepath.Join(dir, "status"), []byte(body), 0o644); err != nil {
m.t.Fatal(err)
}
}
var razer = Account{Module: "openrazer", ID: "openrazer.account", Name: "operator", Groups: []string{"openrazer"}}
func look(t *testing.T, m *machine) Verdict {
t.Helper()
m.status()
j := New(Exec{Run: m.run, Proc: m.proc})
j.Now = func() time.Time { return time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) }
j.Set([]Account{razer})
st, _ := j.Look(t.Context())
if len(st.Accounts) != 1 {
t.Fatalf("the statement: %+v", st)
}
return st.Accounts[0]
}
func TestAGroupTheRunningSessionLacksSaysReloginNeeded(t *testing.T) {
m := &machine{t: t, proc: t.TempDir(), inDB: "operator wheel openrazer", pid: "4242", held: "998"}
v := look(t, m)
if v.State != Unhealthy || !strings.HasPrefix(v.Reason, ReasonRelogin) || !strings.Contains(v.Reason, "openrazer") ||
!strings.Contains(v.Reason, "log out") {
t.Fatalf("a session without the group: %+v", v)
}
if v.Module != "openrazer" || v.ID != "openrazer.account" {
t.Errorf("the verdict is not the module's: %+v", v)
}
for _, a := range m.asked {
read := strings.HasPrefix(a, "id -nG ") || strings.HasPrefix(a, "getent ") ||
strings.HasPrefix(a, "systemctl show --property=MainPID --value user@")
if !read {
t.Errorf("the judge asked something that is not a read: %q", a)
}
}
}
func TestASessionThatHasTheGroupIsHealthy(t *testing.T) {
m := &machine{t: t, proc: t.TempDir(), inDB: "operator wheel openrazer", pid: "4242", held: "998 964"}
if v := look(t, m); v.State != Healthy || v.Reason != "" {
t.Fatalf("a session with the group: %+v", v)
}
}
func TestNoSessionIsHealthyTheNextLoginTakesTheGroup(t *testing.T) {
m := &machine{t: t, proc: t.TempDir(), inDB: "operator openrazer", pid: "0"}
if v := look(t, m); v.State != Healthy {
t.Fatalf("no session: %+v", v)
}
}
func TestAnAccountTheDatabaseDoesNotListIsUnhealthySayingSo(t *testing.T) {
m := &machine{t: t, proc: t.TempDir(), inDB: "operator wheel", pid: "4242", held: "998"}
v := look(t, m)
if v.State != Unhealthy || !strings.HasPrefix(v.Reason, "not in the group openrazer") {
t.Fatalf("not in the group: %+v", v)
}
}
func TestADatabaseThatDoesNotAnswerIsUnknownNeverHealthy(t *testing.T) {
m := &machine{t: t, proc: t.TempDir(), failsID: true}
if v := look(t, m); v.State != Unknown {
t.Fatalf("an unread database: %+v", v)
}
}
func TestOnlyAModulesAccountWithGroupsIsJudged(t *testing.T) {
d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"zsh.login","type":"user","name":"operator","shell":"/bin/zsh"},
{"id":"openrazer.account","type":"user","name":"operator","groups":["openrazer"]},
{"id":"docker.account","type":"user","name":"operator","groups":["docker"]},
{"id":"meshown","type":"user","name":"mesh","groups":["x"]}
]}`))
if err != nil {
t.Fatal(err)
}
got := Of(d, map[string]bool{"docker.account": true})
if len(got) != 1 || got[0].ID != "openrazer.account" || got[0].Module != "openrazer" {
t.Fatalf("judged: %+v", got)
}
}
func TestAVerdictThatHoldsCountsItsStreakAndAChangeIsSaid(t *testing.T) {
m := &machine{t: t, proc: t.TempDir(), inDB: "operator openrazer", pid: "4242", held: "998"}
m.status()
j := New(Exec{Run: m.run, Proc: m.proc})
j.Set([]Account{razer})
_, changed := j.Look(t.Context())
st, again := j.Look(t.Context())
if !changed || again || st.Accounts[0].Streak != 2 {
t.Fatalf("first look changed %v, second %v, streak %d", changed, again, st.Accounts[0].Streak)
}
m.held = "998 964"
m.status()
st, changed = j.Look(t.Context())
if !changed || st.Accounts[0].State != Healthy || st.Accounts[0].Streak != 0 {
t.Fatalf("after the new login: changed %v, %+v", changed, st.Accounts[0])
}
}