package plan
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
const golden = `{
"repo": [{"name":"linux","old":"6.16.7-1","new":"6.16.8-1","repo":"core",
"download":1468006,"delta":-1048576}],
"aur": [{"name":"enca-bin","old":"1.19-4","new":"1.19-5","tag":"!!",
"note":"will be skipped","details":["pipes a download into a shell"]}],
"vcs": [{"name":"helix-git","delta":"r9021"}],
"checks": [{"ok":false,"label":"databases","detail":"2d old · refreshed"}],
"flatpak": [],
"vulnerabilities": {"fixable":["vim"],"unfixable":[]},
"hygiene": {"cache_reclaimable":993710975,"pacnew":["/etc/x.pacnew"],
"orphans":[],"reboot":"","stale_processes":[{"pid":1,"name":"x","path":"/usr/bin/x"}]},
"audits": {"enca-bin":[{"rule":"pipe-to-shell","severity":"BLOCK",
"title":"pipes a download into a shell","evidence":"curl x | sh"}]}
}`
func decode(t *testing.T) *Plan {
t.Helper()
var p Plan
if err := json.NewDecoder(strings.NewReader(golden)).Decode(&p); err != nil {
t.Fatalf("decode: %v", err)
}
return &p
}
func TestDecodeReadsTheBackendContract(t *testing.T) {
p := decode(t)
if len(p.Repo) != 1 || p.Repo[0].Name != "linux" {
t.Fatalf("repo not decoded: %+v", p.Repo)
}
if p.Repo[0].Delta != -1048576 {
t.Errorf("delta = %d, want -1048576", p.Repo[0].Delta)
}
if p.Aur[0].Tag != "!!" {
t.Errorf("tag = %q", p.Aur[0].Tag)
}
if got := p.Audits["enca-bin"][0].Severity; got != "BLOCK" {
t.Errorf("severity = %q", got)
}
if p.Hygiene.CacheReclaimable != 993710975 {
t.Errorf("cache = %d", p.Hygiene.CacheReclaimable)
}
if len(p.Hygiene.Stale) != 1 || p.Hygiene.Stale[0].PID != 1 {
t.Errorf("stale not decoded: %+v", p.Hygiene.Stale)
}
}
func TestUnknownFieldsAreTolerated(t *testing.T) {
// the two halves must be updatable independently, so a field this side has
// never heard of cannot be a parse failure
var p Plan
in := `{"repo":[],"something_new":{"nested":[1,2]},"aur":[]}`
if err := json.NewDecoder(strings.NewReader(in)).Decode(&p); err != nil {
t.Fatalf("a newer backend broke the older front end: %v", err)
}
}
func TestCountsReportBlockedPackages(t *testing.T) {
repo, aur, blocked := decode(t).Counts()
if repo != 1 || aur != 1 || blocked != 1 {
t.Errorf("Counts = %d,%d,%d want 1,1,1", repo, aur, blocked)
}
}
func TestFindingGlyphMatchesThePythonRenderer(t *testing.T) {
// the two front ends must use the same marker for the same severity
cases := map[string]string{"BLOCK": "!!", "LOOK": "!", "INFO": "·", "": "·"}
for severity, want := range cases {
if got := (Finding{Severity: severity}).Glyph(); got != want {
t.Errorf("Glyph(%q) = %q, want %q", severity, got, want)
}
}
}
func TestBackendErrorCarriesStderr(t *testing.T) {
err := &Error{Args: []string{"python3", "-m", "updater"},
Stderr: "boom: no such module\n", Err: os.ErrNotExist}
msg := err.Error()
if !strings.Contains(msg, "boom: no such module") {
t.Errorf("stderr was dropped: %q", msg)
}
if !strings.Contains(msg, "python3") {
t.Errorf("the command was dropped: %q", msg)
}
}
func TestBackendErrorFallsBackToTheUnderlyingError(t *testing.T) {
err := &Error{Args: []string{"python3"}, Err: os.ErrPermission}
if !strings.Contains(err.Error(), "permission") {
t.Errorf("fallback message missing: %q", err.Error())
}
}
// TestLoadAgainstTheRealBackend is the integration test: it runs the actual
// Python entry point and parses its actual output. Skipped when the tree is not
// reachable, so the suite still runs on a machine without it.
func TestLoadAgainstTheRealBackend(t *testing.T) {
root, err := filepath.Abs("../..")
if err != nil {
t.Skip("cannot resolve the repo root")
}
if _, err := os.Stat(filepath.Join(root, "..", "updater", "cli.py")); err != nil {
t.Skip("python backend not present next to the Go module")
}
b := Backend{Python: "python3", Module: "updater",
Env: []string{"PYTHONPATH=" + filepath.Dir(root)},
Args: []string{"--offline", "--no-security"}}
doc, err := b.Load(context.Background())
if err != nil {
t.Skipf("backend unavailable here: %v", err)
}
if doc == nil {
t.Fatal("nil plan with no error")
}
// stdout must have been pure JSON, which Load would have failed on otherwise
if doc.Checks == nil {
t.Log("note: no checks reported")
}
}