snapd is not in the official repositories, and ADR 0205's archive does not fit a daemon with setuid helpers, so the module declares nothing until research 027 question 1 (P2) builds it into the mesh's own repository. Not even its units: on the laptop they do not exist, and the module would fail there. Ten Go tools that work wherever snapd is installed and say so where it is not: status (with AppArmor's absence from the kernel named), list, info, updates, disk usage with the disabled revisions' share, services, changes, and install, remove and refresh through sudo -n with --no-wait, answering snapd's change id.
148 lines
4.6 KiB
Go
148 lines
4.6 KiB
Go
package main
|
|
|
|
// Tests of kit.go, the same in each workstation module.
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fake records the commands asked and answers each from a function of the command line.
|
|
type fake struct {
|
|
asked []Cmd
|
|
answer func(line string, c Cmd) Result
|
|
}
|
|
|
|
func (f *fake) runner() Runner {
|
|
return func(c Cmd) Result {
|
|
f.asked = append(f.asked, c)
|
|
name, args := argv(c)
|
|
line := strings.TrimSpace(name + " " + strings.Join(args, " "))
|
|
if f.answer == nil {
|
|
return Result{}
|
|
}
|
|
return f.answer(line, c)
|
|
}
|
|
}
|
|
|
|
func (f *fake) lines() []string {
|
|
out := []string{}
|
|
for _, c := range f.asked {
|
|
name, args := argv(c)
|
|
out = append(out, strings.TrimSpace(name+" "+strings.Join(args, " ")))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// using installs a fake runner and a non-root uid for one test.
|
|
func using(t *testing.T, answer func(line string, c Cmd) Result) *fake {
|
|
t.Helper()
|
|
f := &fake{answer: answer}
|
|
wasRun, wasUID := run, euid
|
|
run, euid = f.runner(), func() int { return 1000 }
|
|
t.Cleanup(func() { run, euid = wasRun, wasUID })
|
|
return f
|
|
}
|
|
|
|
func ok(stdout string) Result { return Result{Stdout: stdout} }
|
|
|
|
func TestKitAnActThatNeedsRootGoesThroughSudoWithoutAPromptUnlessAlreadyRoot(t *testing.T) {
|
|
was := euid
|
|
defer func() { euid = was }()
|
|
euid = func() int { return 1000 }
|
|
if name, args := argv(Cmd{Name: "x", Args: []string{"a"}, Root: true}); name != "sudo" || strings.Join(args, " ") != "-n x a" {
|
|
t.Fatalf("not root: %s %v", name, args)
|
|
}
|
|
if name, _ := argv(Cmd{Name: "x"}); name != "x" {
|
|
t.Fatalf("a read is run as the account: %s", name)
|
|
}
|
|
euid = func() int { return 0 }
|
|
if name, _ := argv(Cmd{Name: "x", Root: true}); name != "x" {
|
|
t.Fatalf("as root no sudo: %s", name)
|
|
}
|
|
}
|
|
|
|
func TestKitAFailureIsNamedByHowItFailed(t *testing.T) {
|
|
was := euid
|
|
defer func() { euid = was }()
|
|
euid = func() int { return 1000 }
|
|
cases := []struct {
|
|
c Cmd
|
|
r Result
|
|
want string
|
|
}{
|
|
{Cmd{Name: "nothere"}, Result{Status: 127, Error: "not-found"}, "not installed"},
|
|
{Cmd{Name: "x", Root: true}, Result{Status: 127, Error: "not-found"}, "sudo is not installed"},
|
|
{Cmd{Name: "x", Root: true}, Result{Status: 1, Stderr: "sudo: a password is required\n"}, "sudo -n refused"},
|
|
{Cmd{Name: "x", Root: true}, Result{Status: 1, Stderr: "sudo: x: command not found\n"}, "x is not installed"},
|
|
{Cmd{Name: "x"}, Result{Status: 124, Error: "timeout"}, "within 20s"},
|
|
{Cmd{Name: "x", Args: []string{"y"}}, Result{Status: 3, Stderr: "boom\n"}, "x y exited 3: boom"},
|
|
{Cmd{Name: "x"}, Result{Status: 3}, "said nothing"},
|
|
}
|
|
for _, k := range cases {
|
|
err := failure(k.c, k.r)
|
|
if err == nil || !strings.Contains(err.Error(), k.want) {
|
|
t.Errorf("%+v: %v, want %q", k.r, err, k.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKitOutputIsBoundedAndSaysSo(t *testing.T) {
|
|
var w bounded
|
|
big := strings.Repeat("a", MostOutput+10)
|
|
n, _ := w.Write([]byte(big))
|
|
if n != len(big) || w.b.Len() != MostOutput || !w.cut {
|
|
t.Fatalf("kept %d of %d, cut %v", w.b.Len(), len(big), w.cut)
|
|
}
|
|
}
|
|
|
|
func TestKitTheRealRunnerRunsEndsAndReportsAMissingProgram(t *testing.T) {
|
|
r := execRun(Cmd{Name: "sh", Args: []string{"-c", "echo out; echo err >&2; exit 3"}})
|
|
if r.Status != 3 || strings.TrimSpace(r.Stdout) != "out" || strings.TrimSpace(r.Stderr) != "err" {
|
|
t.Fatalf("%+v", r)
|
|
}
|
|
r = execRun(Cmd{Name: "sh", Args: []string{"-c", "sleep 5 & sleep 5"}, Timeout: 200 * time.Millisecond})
|
|
if r.Error != "timeout" {
|
|
t.Fatalf("a slow command: %+v", r)
|
|
}
|
|
r = execRun(Cmd{Name: "no-such-program-anywhere"})
|
|
if r.Error != "not-found" {
|
|
t.Fatalf("a missing program: %+v", r)
|
|
}
|
|
r = execRun(Cmd{Name: "cat", Stdin: "given"})
|
|
if r.Stdout != "given" {
|
|
t.Fatalf("stdin: %+v", r)
|
|
}
|
|
start := time.Now()
|
|
r = execRun(Cmd{Name: "sh", Args: []string{"-c", "echo kept; (sleep 3 &) ; exit 0"}, Detached: true})
|
|
if r.Status != 0 || strings.TrimSpace(r.Stdout) != "kept" || time.Since(start) > 2*time.Second {
|
|
t.Fatalf("a detached command returns when it exits, not when its child does: %+v after %s", r, time.Since(start))
|
|
}
|
|
}
|
|
|
|
func TestKitArgumentsAreReadStrictly(t *testing.T) {
|
|
args := map[string]any{"s": "x", "n": float64(5), "f": 1.5, "b": true, "l": []any{"a", "b"}}
|
|
if _, err := text(args, "missing"); err == nil {
|
|
t.Error("a missing required string")
|
|
}
|
|
if n, _ := optWhole(args, "n", 1, 1, 3); n != 3 {
|
|
t.Errorf("held to most: %d", n)
|
|
}
|
|
if _, err := optWhole(args, "n", 1, 6, 9); err == nil {
|
|
t.Error("below least")
|
|
}
|
|
if _, err := optWhole(args, "f", 1, 0, 9); err == nil {
|
|
t.Error("a fraction")
|
|
}
|
|
if l, _ := optList(args, "l"); len(l) != 2 {
|
|
t.Errorf("list: %v", l)
|
|
}
|
|
if b, _ := optFlag(args, "b", false); !b {
|
|
t.Error("flag")
|
|
}
|
|
if err := plainName("name", "--all"); err == nil {
|
|
t.Error("an option as a name")
|
|
}
|
|
}
|