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.
204 lines
7.4 KiB
Go
204 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTheManifestDeclaresNothingTheHostCouldNotInstall(t *testing.T) {
|
|
m := readManifest(t)
|
|
holdsTheBundle(t, m, "snapd")
|
|
// snapd is not in the official repositories: no package, and no unit that only that package
|
|
// brings, until the mesh's package repository builds it (research 027 question 1).
|
|
if len(m.Resources) != 0 {
|
|
t.Errorf("resources %v", m.Resources)
|
|
}
|
|
}
|
|
|
|
const listAll = `Name Version Rev Tracking Publisher Notes
|
|
bare 1.0 5 latest/stable canonical** base
|
|
code 04c0d99f 266 latest/stable vscode** disabled,classic
|
|
code 07f806f9 267 latest/stable vscode** classic
|
|
gtk-common-themes 0.1-81-g442e511 1535 latest/stable canonical** -
|
|
`
|
|
|
|
func TestListReadsEachRevisionAndMarksTheDisabledOnes(t *testing.T) {
|
|
f := using(t, func(string, Cmd) Result { return ok(listAll) })
|
|
got, err := List()
|
|
if err != nil || len(got.Snaps) != 4 || got.Disabled != 1 || got.Active != 3 {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if s := got.Snaps[1]; s.Name != "code" || s.Revision != "266" || !s.Disabled || s.Publisher != "vscode" || strings.Join(s.Notes, ",") != "disabled,classic" {
|
|
t.Errorf("%+v", s)
|
|
}
|
|
if s := got.Snaps[3]; len(s.Notes) != 0 || s.Disabled {
|
|
t.Errorf("%+v", s)
|
|
}
|
|
if f.lines()[0] != "snap list --all --unicode=never --color=never" {
|
|
t.Errorf("%v", f.lines())
|
|
}
|
|
}
|
|
|
|
func TestToolsSayWhenSnapdIsNotInstalled(t *testing.T) {
|
|
using(t, func(string, Cmd) Result { return Result{Status: 127, Error: "not-found"} })
|
|
if _, err := List(); err == nil || !strings.Contains(err.Error(), "not in the official repositories") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
got, err := Status()
|
|
if err != nil || got.Installed || !strings.Contains(strings.Join(got.Findings, ";"), "not installed") {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestStatusNamesWhatIsWrongWithTheInstallation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
wasLSM, wasRoot := lsmFile, snapRoot
|
|
lsmFile, snapRoot = filepath.Join(dir, "lsm"), filepath.Join(dir, "snap")
|
|
defer func() { lsmFile, snapRoot = wasLSM, wasRoot }()
|
|
_ = os.WriteFile(lsmFile, []byte("capability,landlock,lockdown,yama,bpf\n"), 0o644)
|
|
_ = os.Mkdir(snapRoot, 0o755)
|
|
using(t, func(line string, c Cmd) Result {
|
|
switch {
|
|
case line == "snap version":
|
|
return ok("snap 2.76.2-2\nsnapd 2.76.2-2\nseries 16\n")
|
|
case strings.HasSuffix(line, "is-enabled snapd.service"), strings.HasSuffix(line, "is-enabled apparmor.service"):
|
|
return Result{Status: 1, Stdout: "disabled\n"}
|
|
case strings.Contains(line, "is-enabled"):
|
|
return ok("enabled\n")
|
|
case strings.Contains(line, "is-active snapd.service"):
|
|
return ok("active\n")
|
|
}
|
|
return Result{Status: 3, Stdout: "inactive\n"}
|
|
})
|
|
got, err := Status()
|
|
if err != nil || !got.Installed || got.Version != "2.76.2-2" || got.AppArmor || !got.ClassicRoot || len(got.Units) != 4 {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if got.Units[1].Enabled != "disabled" || got.Units[1].Active != "active" {
|
|
t.Errorf("socket-activated service: %+v", got.Units[1])
|
|
}
|
|
all := strings.Join(got.Findings, ";")
|
|
if !strings.Contains(all, "without their confinement") || !strings.Contains(all, "nothing enforces") || strings.Contains(all, "/snap does not exist") {
|
|
t.Errorf("%s", all)
|
|
}
|
|
}
|
|
|
|
func TestInfoReadsFieldsCommandsAndChannels(t *testing.T) {
|
|
using(t, func(string, Cmd) Result {
|
|
return ok(`name: code
|
|
summary: Code editing. Redefined.
|
|
publisher: Visual Studio Code (vscode**)
|
|
license: unset
|
|
description: |
|
|
Visual Studio Code is a new choice
|
|
of tool.
|
|
commands:
|
|
- code
|
|
- code.url-handler
|
|
tracking: latest/stable
|
|
channels:
|
|
latest/stable: 07f806f9 2026-09-30 (267) 543MB classic
|
|
latest/candidate: ^
|
|
`)
|
|
})
|
|
got, err := Info("code")
|
|
if err != nil || got.Fields["summary"] != "Code editing. Redefined." || len(got.Commands) != 2 || got.Fields["tracking"] != "latest/stable" {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if got.Channels["latest/stable"] != "07f806f9 2026-09-30 (267) 543MB classic" || got.Fields["description"] != "Visual Studio Code is a new choice of tool." {
|
|
t.Errorf("%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdatesAndChangesReadNothingToDoAsEmpty(t *testing.T) {
|
|
using(t, func(line string, c Cmd) Result {
|
|
if strings.Contains(line, "refresh --list") {
|
|
return Result{Stderr: "All snaps up to date.\n"}
|
|
}
|
|
return Result{Status: 1, Stderr: "error: no changes found\n"}
|
|
})
|
|
u, err := Updates()
|
|
if err != nil || u["count"] != 0 {
|
|
t.Fatalf("%v %v", u, err)
|
|
}
|
|
c, err := Changes("")
|
|
if err != nil || len(c["changes"].([]Change)) != 0 {
|
|
t.Fatalf("%v %v", c, err)
|
|
}
|
|
if _, err := Changes("12; reboot"); err == nil {
|
|
t.Error("a change id that is not a number")
|
|
}
|
|
}
|
|
|
|
func TestChangesAndTasksAreReadByColumn(t *testing.T) {
|
|
using(t, func(line string, c Cmd) Result {
|
|
if strings.Contains(line, "tasks") {
|
|
return ok("Status Spawn Ready Summary\nDone 2026-10-01T18:57:00+02:00 2026-10-01T18:57:10+02:00 Download snap \"code\" (267)\n")
|
|
}
|
|
return ok("ID Status Spawn Ready Summary\n42 Doing 2026-10-04T10:00:00+02:00 - Install \"hello\" snap\n")
|
|
})
|
|
c, err := Changes("")
|
|
ch := c["changes"].([]Change)
|
|
if err != nil || len(ch) != 1 || ch[0].ID != "42" || ch[0].Ready != "" || ch[0].Summary != `Install "hello" snap` {
|
|
t.Fatalf("%+v %v", c, err)
|
|
}
|
|
c, _ = Changes("42")
|
|
if tasks := c["tasks"].([]Change); len(tasks) != 1 || tasks[0].Status != "Done" || tasks[0].Summary != `Download snap "code" (267)` {
|
|
t.Errorf("%+v", c)
|
|
}
|
|
}
|
|
|
|
func TestActsGoThroughSudoWithoutWaitingAndAnswerTheChange(t *testing.T) {
|
|
f := using(t, func(line string, c Cmd) Result {
|
|
if strings.Contains(line, "refresh") {
|
|
return Result{Stderr: "All snaps up to date.\n"}
|
|
}
|
|
return ok("57\n")
|
|
})
|
|
got, err := Install("hello-world", "latest/edge", true)
|
|
if err != nil || got.Change != "57" || !strings.Contains(got.Follow, "57") {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if _, err := Remove("hello-world", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := Refresh("")
|
|
if err != nil || r.Change != "" || !strings.Contains(r.Said, "up to date") {
|
|
t.Fatalf("%+v %v", r, err)
|
|
}
|
|
want := "sudo -n snap install --no-wait --channel=latest/edge --classic hello-world\n" +
|
|
"sudo -n snap remove --no-wait --purge hello-world\n" +
|
|
"sudo -n snap refresh --no-wait"
|
|
if got := strings.Join(f.lines(), "\n"); got != want {
|
|
t.Errorf("asked\n%s", got)
|
|
}
|
|
for _, bad := range []string{"--classic", "Hello", "a b", "../x"} {
|
|
if err := checkSnapName(bad); err == nil {
|
|
t.Errorf("%q accepted as a snap name", bad)
|
|
}
|
|
}
|
|
if _, err := Install("x", "--dangerous", false); err == nil {
|
|
t.Error("an option as a channel")
|
|
}
|
|
}
|
|
|
|
func TestDiskUsageMeasuresEachRevisionAndWhatDisabledOnesTake(t *testing.T) {
|
|
dir := t.TempDir()
|
|
was := snapsDir
|
|
snapsDir = dir
|
|
defer func() { snapsDir = was }()
|
|
for name, size := range map[string]int{"code_266.snap": 500, "code_267.snap": 510, "bare_5.snap": 4} {
|
|
_ = os.WriteFile(filepath.Join(dir, name), make([]byte, size), 0o600)
|
|
}
|
|
using(t, func(string, Cmd) Result { return ok(listAll) })
|
|
got, err := DiskUsage()
|
|
if err != nil || got.TotalBytes != 1014 || got.Reclaimable != 500 || len(got.Snaps) != 2 {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if c := got.Snaps[0]; c.Name != "code" || c.Bytes != 1010 || !c.Revisions[0].Disabled || c.Revisions[1].Disabled {
|
|
t.Errorf("%+v", c)
|
|
}
|
|
}
|