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
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.
166 lines
5.5 KiB
Go
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])
|
|
}
|
|
}
|