package system import ( "context" "errors" "strings" "testing" "github.com/novox/mesh-host/internal/declaration" ) // recorder answers a fixed map of commands and remembers what it was asked. // // What is being tested is which commands each system issues and how it reads the answers, so // the commands are real command shapes taken from the tools themselves. type recorder struct { answers map[string]string // first two argv words -> stdout fails map[string]bool calls []string } func (r *recorder) run(ctx context.Context, name string, args ...string) (string, error) { r.calls = append(r.calls, strings.TrimSpace(name+" "+strings.Join(args, " "))) // Two keys, most specific first. `systemctl show` and `systemctl is-enabled` need telling // apart, while `rc-service docker status` puts the UNIT where the verb would be — so a // binary-only key is needed too. keys := []string{name} if len(args) > 0 { keys = []string{name + " " + args[0], name} } for _, key := range keys { if r.fails[key] { return r.answers[key], errors.New("exit status 1") } if out, ok := r.answers[key]; ok { return out, nil } } return "", errors.New("exit status 127: not found") } func sys(t *testing.T, name string) System { t.Helper() s, err := For(name) if err != nil { t.Fatal(err) } return s } func TestEverySystemIsNamedAndReachable(t *testing.T) { for _, want := range []string{"arch", "alpine", "android"} { if _, err := For(want); err != nil { t.Errorf("the %s host cannot be built: %v", want, err) } } if _, err := For("debian"); err == nil { t.Error("a system nobody has written was returned instead of refused") } else if !strings.Contains(err.Error(), "arch") { t.Errorf("the refusal does not say which hosts exist: %v", err) } // A host built without -X main.builtFor must refuse rather than default to something. if _, err := For(""); err == nil { t.Error("a host built for nothing was accepted") } } // --- what each system can do ----------------------------------------------------------------- func TestAndroidRefusesTheShapesItCannotDo(t *testing.T) { // The point of a partial host: refused whole, before anything is applied, naming what this // host does implement. Not attempted-and-failed half way through. d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[ {"id":"f","type":"file","path":"/data/x","content":"a\n"}, {"id":"p","type":"package","package":"docker"} ]}`)) if err != nil { t.Fatal(err) } refusal := Check(sys(t, "android"), d) if refusal == nil { t.Fatal("the android host accepted a package") } for _, want := range []string{"package", "android", "file", "directory", "action"} { if !strings.Contains(refusal.Error(), want) { t.Errorf("the refusal does not mention %q: %v", want, refusal) } } } func TestAndroidAcceptsThePortableShapes(t *testing.T) { // file, directory and action need only a filesystem and a way to run something. A host that // can do nothing else can still do these, which is what makes a partial host a real one. d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[ {"id":"d","type":"directory","path":"/data/mesh"}, {"id":"f","type":"file","path":"/data/mesh/x","content":"a\n"}, {"id":"a","type":"action","command":["true"],"verify":["true"]} ]}`)) if err != nil { t.Fatal(err) } if err := Check(sys(t, "android"), d); err != nil { t.Errorf("the android host refused a portable declaration: %v", err) } } func TestArchAndAlpineDoEveryShape(t *testing.T) { for _, name := range []string{"arch", "alpine"} { s := sys(t, name) for _, shape := range everyShape() { if !Supports(s, shape) { t.Errorf("the %s host does not implement %s", name, shape) } } } } // --- confirming the machine is the one the host was built for -------------------------------- func TestAHostOnTheWrongMachineSaysSo(t *testing.T) { // Installing the arch host on Alpine must fail once, at the start, rather than later inside // a package manager that is not there. alpineMachine := &recorder{answers: map[string]string{"apk info": "apk-tools-2.14.0\n"}} if err := sys(t, "arch").Confirm(context.Background(), alpineMachine.run); err == nil { t.Fatal("the arch host confirmed itself on an Alpine machine") } else if !strings.Contains(err.Error(), "not Arch") { t.Errorf("the failure does not say what is wrong: %v", err) } archMachine := &recorder{answers: map[string]string{"pacman -Q": "pacman 7.0.0-1\n"}} if err := sys(t, "alpine").Confirm(context.Background(), archMachine.run); err == nil { t.Fatal("the alpine host confirmed itself on an Arch machine") } } // --- packages --------------------------------------------------------------------------------- func TestApkReportsAbsenceByEmptyOutputNotByExitCode(t *testing.T) { // The difference that matters between apk and pacman, and it is invisible until it bites. // // pacman -Q missing -> exits NON-ZERO // apk info -e missing -> exits ZERO and prints NOTHING // // So reading apk's exit code the way pacman's is read reports every package as installed. r := &recorder{answers: map[string]string{ "apk info": "", // installed-check for a package that is not there }} // apk-tools is what Confirm asks about; both go through the same key, so the empty answer // stands in for "not installed" while the call still succeeds. installed, err := sys(t, "alpine").PackageInstalled(context.Background(), r.run, "docker") if err == nil && installed { t.Error("apk's empty output was read as 'installed'") } } func TestApkFindsAnInstalledPackage(t *testing.T) { r := &recorder{answers: map[string]string{"apk info": "docker-24.0.7-r0\n"}} installed, err := sys(t, "alpine").PackageInstalled(context.Background(), r.run, "docker") if err != nil { t.Fatalf("could not ask: %v", err) } if !installed { t.Error("an installed package was reported missing") } } func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) { // Both systems, same trap: a package manager that cannot answer must not read as "nothing // is installed", or the host reinstalls on a machine whose database is broken. for _, name := range []string{"arch", "alpine"} { r := &recorder{} // answers nothing; every command fails if _, err := sys(t, name).PackageInstalled(context.Background(), r.run, "docker"); err == nil { t.Errorf("%s: a broken package database was read as 'not installed'", name) } } } // --- services ---------------------------------------------------------------------------------- func TestAServiceThatDoesNotExistIsNeverReportedStopped(t *testing.T) { // The most important thing both service managers must get right, and they say it // differently: systemd through LoadState=not-found, OpenRC in prose. for _, tc := range []struct{ name, key, out string }{ {"arch", "systemctl show", "LoadState=not-found\nActiveState=inactive\n"}, {"alpine", "rc-service", " * rc-service: service `nope' does not exist\n"}, } { r := &recorder{answers: map[string]string{tc.key: tc.out}} state, err := sys(t, tc.name).ServiceState(context.Background(), r.run, "nope") if err == nil { t.Errorf("%s: a service that does not exist was reported as %q", tc.name, state) continue } // Asserted on the DIAGNOSIS, not on "does not exist" — the fall-through error echoes // the raw output, which contains that phrase, so matching it passed even with the // distinction removed. Only the correct branch explains why absence is not stopped. if !strings.Contains(err.Error(), "absence as success") { t.Errorf("%s: failed for the wrong reason: %v", tc.name, err) } } } func TestOpenRCStateIsReadFromItsOwnWords(t *testing.T) { for _, tc := range []struct{ out, want string }{ {" * status: started\n", "running"}, {" * status: stopped\n", "stopped"}, {" * status: crashed\n", "stopped"}, // not up, so starting it is the right next act } { r := &recorder{answers: map[string]string{"rc-service": tc.out}} got, err := sys(t, "alpine").ServiceState(context.Background(), r.run, "docker") if err != nil { t.Errorf("%q: %v", tc.out, err) continue } if got != tc.want { t.Errorf("%q read as %q, expected %q", tc.out, got, tc.want) } } } func TestOpenRCBootStateComesFromTheRunlevel(t *testing.T) { // OpenRC has no `is-enabled`. What it has is the runlevel listing, so "starts at boot" // becomes "appears here". r := &recorder{answers: map[string]string{ "rc-update show": " docker | default\n sshd | default\n", }} s := sys(t, "alpine") got, err := s.ServiceBoot(context.Background(), r.run, "docker") if err != nil || got != "enabled" { t.Errorf("a service in the default runlevel read as %q (%v)", got, err) } got, err = s.ServiceBoot(context.Background(), r.run, "chronyd") if err != nil || got != "disabled" { t.Errorf("a service not in any runlevel read as %q (%v)", got, err) } } func TestEachSystemUsesItsOwnCommands(t *testing.T) { // The whole point of ADR 0005: the alpine host must never reach for systemctl, and the arch // host must never reach for rc-service. for _, tc := range []struct{ name, forbidden string }{ {"arch", "rc-service"}, {"arch", "apk"}, {"alpine", "systemctl"}, {"alpine", "pacman"}, } { r := &recorder{answers: map[string]string{ "systemctl show": "LoadState=loaded\nActiveState=active\n", "rc-service": " * status: started\n", "pacman -Q": "pacman 7.0.0\n", "apk info": "apk-tools-2.14\n", "rc-update show": " docker | default\n", "systemctl is-enabled": "enabled\n", }} s := sys(t, tc.name) _, _ = s.ServiceState(context.Background(), r.run, "docker") _, _ = s.ServiceBoot(context.Background(), r.run, "docker") _, _ = s.PackageInstalled(context.Background(), r.run, "docker") for _, call := range r.calls { if strings.HasPrefix(call, tc.forbidden) { t.Errorf("the %s host called %q", tc.name, call) } } } } func TestAndroidsUnreachableAppliersFailLoudly(t *testing.T) { // Check refuses these shapes before an applier is reached, so these are unreachable — and // they say so rather than returning a zero value, in case "unreachable" ever stops being // true. s := sys(t, "android") r := &recorder{} if _, err := s.PackageInstalled(context.Background(), r.run, "x"); !errors.Is(err, ErrUnsupported) { t.Errorf("android's package applier did not report it as unsupported: %v", err) } if _, err := s.ServiceState(context.Background(), r.run, "x"); !errors.Is(err, ErrUnsupported) { t.Errorf("android's service applier did not report it as unsupported: %v", err) } } func TestALoginThatIsNotThereIsAnAnswerAndABrokenDatabaseIsNot(t *testing.T) { // The distinction this package takes trouble over everywhere else, applied to users. A user // database that cannot be read must not be reported as "no such user" — absence read as // fact is the fault the whole host exists to prevent. notFound := func(context.Context, string, ...string) (string, error) { return "", errors.New("exit status 2") } if _, exists, err := LookUpUser(context.Background(), notFound, "nobody"); err != nil { t.Fatalf("a missing user was reported as a failure: %v", err) } else if exists { t.Fatal("a missing user was reported as present") } broken := func(context.Context, string, ...string) (string, error) { return "", errors.New("exit status 71: cannot read /etc/passwd") } if _, exists, err := LookUpUser(context.Background(), broken, "somebody"); err == nil { t.Fatal("a broken user database was reported as an answer") } else if exists { t.Fatal("a broken user database reported a user as present") } } func TestALoginIsReadFromThePasswdEntry(t *testing.T) { answering := func(context.Context, string, ...string) (string, error) { return "worker:x:1001:1001:,,,:/home/worker:/usr/bin/zsh\n", nil } login, exists, err := LookUpUser(context.Background(), answering, "worker") if err != nil || !exists { t.Fatalf("exists=%v err=%v", exists, err) } if login.Home != "/home/worker" || login.Shell != "/usr/bin/zsh" { t.Fatalf("got %+v", login) } } func TestSomethingThatIsNotAPasswdEntryIsRefused(t *testing.T) { // Rather than read as a login with empty fields, which would have the host decide the shell // differs and set it on every apply for ever. nonsense := func(context.Context, string, ...string) (string, error) { return "who knows\n", nil } if _, _, err := LookUpUser(context.Background(), nonsense, "worker"); err == nil { t.Fatal("nonsense was read as a login") } } func TestAPartialHostRefusesUsersAndAllowsArchives(t *testing.T) { // An archive needs a filesystem and a way to fetch; a user needs a user database this host is // allowed to write, which Android does not have. speaks := map[declaration.Type]bool{} for _, shape := range (android{}).Shapes() { speaks[shape] = true } if !speaks[declaration.TypeArchive] { t.Error("a partial host refuses archives, which need only a filesystem") } if speaks[declaration.TypeUser] { t.Error("a partial host claims to manage users") } if err := (android{}).CreateUser(context.Background(), nil, "a", "", ""); err == nil { t.Error("a partial host created a user") } } // A stale index and a wrong declaration fail identically, and are fixed in completely different // places. // // novox/hq 04-ISSUES/002: the package exists, the declaration is correct, and the machine is // asking the mirrors for a version they have already replaced. Reported as a generic install // failure it sends somebody to check the manifest, which is the one thing that is right. func TestAStaleIndexIsNamedRatherThanReportedAsAFailedInstall(t *testing.T) { said := "error: failed retrieving file 'dnsmasq-2.90-1-x86_64.pkg.tar.zst' from mirror.one : " + "The requested URL returned error: 404\n" + "error: failed retrieving file 'dnsmasq-2.90-1-x86_64.pkg.tar.zst' from mirror.two : " + "The requested URL returned error: 404\n" + "error: failed to commit transaction (failed to retrieve some files)" run := func(context.Context, string, ...string) (string, error) { return said, errors.New("exit status 1") } err := (arch{}).InstallPackage(context.Background(), run, "dnsmasq") if err == nil { t.Fatal("an install that failed reported success") } for _, want := range []string{"stale package index", "upgrading the machine", "partial upgrade"} { if !strings.Contains(err.Error(), want) { t.Fatalf("the failure does not say %q, so it reads as a wrong declaration:\n%v", want, err) } } // And the package manager's own words, which were being thrown away entirely. if !strings.Contains(err.Error(), "404") { t.Fatalf("what the package manager said was discarded:\n%v", err) } } // An ordinary failure is not dressed up as a stale index: saying "upgrade the machine" about a // package that does not exist sends somebody to do something large and useless. func TestAnOrdinaryInstallFailureIsNotCalledAStaleIndex(t *testing.T) { run := func(context.Context, string, ...string) (string, error) { return "error: target not found: nosuchpackage", errors.New("exit status 1") } err := (arch{}).InstallPackage(context.Background(), run, "nosuchpackage") if err == nil { t.Fatal("an install that failed reported success") } if strings.Contains(err.Error(), "stale package index") { t.Fatalf("a package that does not exist was called a stale index:\n%v", err) } if !strings.Contains(err.Error(), "target not found") { t.Fatalf("what the package manager said was discarded:\n%v", err) } } // One mirror failing is transient and retrying is the answer. Every mirror saying the file is gone // is the index being old. func TestOneMirrorFailingIsNotAStaleIndex(t *testing.T) { run := func(context.Context, string, ...string) (string, error) { return "warning: failed retrieving file 'x.pkg.tar.zst' from mirror.one : timeout", errors.New("exit status 1") } err := (arch{}).InstallPackage(context.Background(), run, "x") if err != nil && strings.Contains(err.Error(), "stale package index") { t.Fatalf("one mirror timing out was called a stale index:\n%v", err) } } // And an install that works still works. func TestAnInstallThatSucceedsSaysNothing(t *testing.T) { run := func(context.Context, string, ...string) (string, error) { return "installed", nil } if err := (arch{}).InstallPackage(context.Background(), run, "dnsmasq"); err != nil { t.Fatalf("a successful install reported a failure: %v", err) } }