git/orchard/main
tui/internal/plan/plan_test.go
132 lines4 KB last change raw · history
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")
	}
}