171 lines
9.4 KiB
Go
171 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A wait for a person is not a failure, and one module's verdict is not every module's (novox/hq ADR 0254,
|
|
// issue 318).
|
|
|
|
func relogin(module, account string) inventory.ResourceHealth {
|
|
return inventory.ResourceHealth{Module: module, Resource: module + "." + account, Kind: link.KindAccount, Target: account,
|
|
Account: account, State: link.StateUnhealthy, Reason: link.ReasonRelogin + ": " + account + " is in the group " + module +
|
|
", and its running session began before it was; log out of every session and in again, or reboot"}
|
|
}
|
|
|
|
func userUnit(module, account string) inventory.ResourceHealth {
|
|
return inventory.ResourceHealth{Module: module, Resource: module + ".daemon", Kind: link.KindUnit,
|
|
Target: module + "-daemon.service", Account: account, State: link.StateUnhealthy,
|
|
Reason: "failed in the account's own service manager (exit-code)"}
|
|
}
|
|
|
|
// Only what depends on the new login alone is a wait; anything else unhealthy of the module is judged as
|
|
// before, so that a fault is never excused.
|
|
func TestOnlyWhatDependsOnTheNewLoginIsAWait(t *testing.T) {
|
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
|
notInGroup := relogin("lights", "operator")
|
|
notInGroup.Reason = "not in the group lights: the apply has not put operator there; its outcome says why"
|
|
systemUnit := userUnit("lights", "operator")
|
|
systemUnit.Account = ""
|
|
otherAccount := userUnit("lights", "guest")
|
|
container := inventory.ResourceHealth{Module: "lights", Resource: "lights.web", Kind: "container", Target: "lights",
|
|
State: link.StateUnhealthy, Reason: "down"}
|
|
otherModule := userUnit("sound", "operator")
|
|
starting := userUnit("lights", "operator")
|
|
starting.State = link.StateStarting
|
|
byTarget := relogin("lights", "operator")
|
|
byTarget.Account = "" // an account said by its target alone is still that account
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
rs []inventory.ResourceHealth
|
|
waits bool
|
|
}{
|
|
{"the account alone", []inventory.ResourceHealth{relogin("lights", "operator")}, true},
|
|
{"the account and its unit in that account's manager", []inventory.ResourceHealth{relogin("lights", "operator"),
|
|
userUnit("lights", "operator")}, true},
|
|
{"an account named by its target", []inventory.ResourceHealth{byTarget, userUnit("lights", "operator")}, true},
|
|
{"another module's unit is not this module's", []inventory.ResourceHealth{relogin("lights", "operator"), otherModule}, true},
|
|
{"a unit still starting is no fault", []inventory.ResourceHealth{relogin("lights", "operator"), starting}, true},
|
|
{"a unit and no account", []inventory.ResourceHealth{userUnit("lights", "operator")}, false},
|
|
{"an account not in its group", []inventory.ResourceHealth{notInGroup, userUnit("lights", "operator")}, false},
|
|
{"a unit whose manager is not said", []inventory.ResourceHealth{relogin("lights", "operator"), systemUnit}, false},
|
|
{"a unit in another account's manager", []inventory.ResourceHealth{relogin("lights", "operator"), otherAccount}, false},
|
|
{"a container beside the wait", []inventory.ResourceHealth{relogin("lights", "operator"), container}, false},
|
|
} {
|
|
said, waits := personWait("lights", "laptop", c.rs)
|
|
if waits != c.waits {
|
|
t.Errorf("%s: waits %v; want %v", c.name, waits, c.waits)
|
|
continue
|
|
}
|
|
if waits && !strings.HasPrefix(said, "relogin needed on laptop: lights waits for a new login of operator") {
|
|
t.Errorf("%s: said %q", c.name, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The gate reads a wait for a person as its own reading, never a fault, and passes with it; the same module
|
|
// with a container down beside it is not yet healthy, as before.
|
|
func TestAWaitForAPersonIsAPassCarriedAlong(t *testing.T) {
|
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
|
now := time.Now()
|
|
since := now.Add(-time.Minute)
|
|
account := relogin("lights", "operator")
|
|
f := gateFacts{now: now, health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: now,
|
|
Resources: []inventory.ResourceHealth{account, userUnit("lights", "operator")}}},
|
|
groupsAdded: map[string]bool{"lights": true}}
|
|
if h, why := moduleHealthWord("lights", "laptop", since, f); h != healthPerson || !strings.Contains(why, "relogin needed on laptop") {
|
|
t.Fatalf("a wait for a new login reads %v %q; want a wait for a person", h, why)
|
|
}
|
|
// **Only a move that put the account in a new group is excused** (issue 318 review): a later build that
|
|
// adds no group did not bring the wait, and is judged as before.
|
|
f.groupsAdded["lights"] = false
|
|
if h, why := moduleHealthWord("lights", "laptop", since, f); h != healthNotYet {
|
|
t.Fatalf("a wait the move did not bring reads %v %q; want not yet", h, why)
|
|
}
|
|
f.groupsAdded["lights"] = true
|
|
h := f.health["laptop"]
|
|
h.Resources = append(h.Resources, inventory.ResourceHealth{Module: "lights", Resource: "lights.web", Kind: "container",
|
|
Target: "lights", State: link.StateUnhealthy, Reason: "down"})
|
|
f.health["laptop"] = h
|
|
if got, why := moduleHealthWord("lights", "laptop", since, f); got != healthNotYet {
|
|
t.Fatalf("a container down beside the wait reads %v %q; want not yet", got, why)
|
|
}
|
|
}
|
|
|
|
// The module's condition says the wait in one sentence for a person, and clears on the first statement that
|
|
// no longer says it — said as the module's own fault when its unit still fails after the new login.
|
|
|
|
// A module that is healthy on its own keeps its pass when another module of the same send fails at the
|
|
// bound: it is neither put back nor left without a verdict for every other walk to wait on.
|
|
|
|
// What the operator reads of the wait (ADR 0253, ADR 0254): it needs them, so it is never quiet, and it offers
|
|
// no button, since nothing but their own new login can do it.
|
|
func TestTheReloginConditionNeedsTheOperatorAndOffersNoButton(t *testing.T) {
|
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
|
o := reloginObservation("openrazer", "g14", "relogin needed on g14: …", "operator", []inventory.ResourceHealth{
|
|
relogin("openrazer", "operator"), userUnit("openrazer", "operator")})
|
|
plainExample(t, o, "openrazer waits for a new login on g14",
|
|
"Needs you: log out of g14 completely and log in again, or restart it. Openrazer put your account in a "+
|
|
"group it needs. You logged in before that, so openrazer cannot run until you log in again. Its "+
|
|
"update is in place and nothing was undone.")
|
|
if len(o.Actions) != 0 || o.Needs == "" {
|
|
t.Errorf("relogin: needs %q, actions %+v; want needs and no button", o.Needs, o.Actions)
|
|
}
|
|
// **Not "your account" when it is not the operator's** (issue 318 review).
|
|
other := reloginObservation("openrazer", "g14", "relogin needed on g14: …", "operator", []inventory.ResourceHealth{
|
|
relogin("openrazer", "guest"), userUnit("openrazer", "guest")})
|
|
plainExample(t, other, "openrazer waits for a new login on g14",
|
|
"Needs you: have the account it names log out of g14 completely and log in again, or restart g14. Openrazer "+
|
|
"put an account on g14 in a group it needs. That account logged in before that, so openrazer cannot run "+
|
|
"until it logs in again. Its update is in place and nothing was undone.")
|
|
if unknown := reloginObservation("openrazer", "g14", "…", "", []inventory.ResourceHealth{relogin("openrazer", "operator")}); strings.Contains(unknown.Explanation, "your account") {
|
|
t.Errorf("an operator not known is still told it is their account: %q", unknown.Explanation)
|
|
}
|
|
// The kind's own wording, for a condition raised without words, names the module whole, dots and all,
|
|
// and never claims the account is the operator's.
|
|
w := plainWordings[kindReloginNeeded](conditions.Observation{Scope: conditions.ScopeModule, ID: "razer.lights.g14", Machine: "g14"})
|
|
if w.Headline != "razer.lights waits for a new login on g14" || w.Needs == "" || len(w.Actions) != 0 ||
|
|
strings.Contains(w.Explanation, "your account") {
|
|
t.Errorf("the kind's wording: %+v", w)
|
|
}
|
|
}
|
|
|
|
// When a module not working turns into one waiting for a new login, and back, the clearing line says what it
|
|
// became, never that it works again (issue 318 review).
|
|
|
|
// Which moves add an account group, read from the builds' manifests (issue 318 review).
|
|
func TestOnlyAMoveThatAddsAnAccountGroupCanBringAWait(t *testing.T) {
|
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
|
user := func(groups ...any) catalogue.Manifest {
|
|
return catalogue.Manifest{Module: "lights", Resources: []map[string]any{{"id": "operator", "type": "user",
|
|
"name": "operator", "groups": groups}}}
|
|
}
|
|
none := catalogue.Manifest{Module: "lights"}
|
|
for _, c := range []struct {
|
|
name string
|
|
from catalogue.Manifest
|
|
had bool
|
|
to catalogue.Manifest
|
|
addsSome bool
|
|
}{
|
|
{"a group added", none, true, user("lights"), true},
|
|
{"the same group kept", user("lights"), true, user("lights"), false},
|
|
{"a second group added", user("lights"), true, user("lights", "video"), true},
|
|
{"a group taken away", user("lights", "video"), true, user("lights"), false},
|
|
{"new to the machine, with a group", none, false, user("lights"), true},
|
|
{"new to the machine, no group", none, false, none, false},
|
|
} {
|
|
if got := addsAccountGroups(c.from, c.had, c.to); got != c.addsSome {
|
|
t.Errorf("%s: adds %v; want %v", c.name, got, c.addsSome)
|
|
}
|
|
}
|
|
}
|