// SPDX-License-Identifier: MIT package cli import ( "bytes" "context" "errors" "os" "strings" "testing" "prx/internal/config" "prx/internal/execx" "prx/internal/proto" ) func TestCheckMark(t *testing.T) { cases := []struct { check proto.Check want string }{ {proto.Check{OK: true}, "ok"}, {proto.Check{OK: false, Warn: true}, "warn"}, {proto.Check{OK: false}, "FAIL"}, {proto.Check{OK: false, Fatal: true}, "FAIL"}, } for _, tc := range cases { if got := checkMark(tc.check); got != tc.want { t.Errorf("checkMark(%+v) = %q, want %q", tc.check, got, tc.want) } } } func TestBuildPlanMapsConfig(t *testing.T) { cfg := config.Default() cfg.Network.WANInterface = "wlan0" cfg.Network.WANGateway = "192.168.1.1" cfg.Network.WANGateway6 = "2001:db8::1" cfg.Health.Probe = "9.9.9.9:443" cfg.Bypass.ExtraCIDRs = []string{"203.0.113.0/24"} a := &app{} plan := a.buildPlan(cfg, proto.ActionApply) if plan.Action != proto.ActionApply { t.Errorf("action = %q", plan.Action) } if plan.WANInterface != "wlan0" || plan.WANGateway4 != "192.168.1.1" || plan.WANGateway6 != "2001:db8::1" { t.Errorf("network fields not mapped: %+v", plan) } if plan.HealthProbe != "9.9.9.9:443" { t.Errorf("health probe not mapped: %q", plan.HealthProbe) } if len(plan.BypassCIDRs) != 1 || plan.BypassCIDRs[0] != "203.0.113.0/24" { t.Errorf("bypass CIDRs not mapped: %v", plan.BypassCIDRs) } if plan.StatePath != config.DefaultStatePath { t.Errorf("state path not mapped: %q", plan.StatePath) } } func TestPrintResultFormatsChecks(t *testing.T) { buf := &bytes.Buffer{} a := &app{out: buf} a.printResult(proto.Result{ Messages: []string{"done"}, Checks: []proto.Check{ {Name: "nft", OK: true, Detail: "present"}, {Name: "tun", OK: false, Warn: true}, }, }) out := buf.String() for _, needle := range []string{" done", "[ok] nft: present", "[warn] tun"} { if !strings.Contains(out, needle) { t.Errorf("output missing %q:\n%s", needle, out) } } } func TestFormatResultError(t *testing.T) { if err := formatResultError(proto.Result{Error: "boom"}); err == nil || err.Error() != "boom" { t.Fatalf("formatResultError = %v", err) } if err := formatResultError(proto.Result{}); err == nil { t.Fatal("expected a generic error when Result.Error is empty") } } func TestNftVersionWarning(t *testing.T) { cases := []struct { in string warn bool }{ {"nftables v0.9.3 (Lester Gomes)\n", true}, {"nftables v0.9.4 (Fearless Fred)\n", false}, {"nftables v0.9.0 (Topsy)\n", true}, {"nftables v1.0.9 (Sunday, 1 September 2024)\n", false}, {"nftables v2.0.1\n", false}, {"unparseable output", false}, } for _, tc := range cases { got := nftVersionWarning(tc.in) if tc.warn && got == "" { t.Errorf("nftVersionWarning(%q) = \"\", want a warning", tc.in) } if !tc.warn && got != "" { t.Errorf("nftVersionWarning(%q) = %q, want no warning", tc.in, got) } } } func TestIsPermissionError(t *testing.T) { if !isPermissionError(errors.New("nft list: exit status 1: Error: Operation not permitted")) { t.Error("expected a permission error to be recognised") } if isPermissionError(errors.New("exec: nft: executable file not found in $PATH")) { t.Error("a missing binary must not be reported as a permission error") } } // privilegeCheck must always return a named check and, for a non-root caller, // never panic even when nothing is installed. func TestPrivilegeCheckAlwaysNamed(t *testing.T) { a := &app{} c := a.privilegeCheck(context.Background(), &execx.Fake{}) if c.Name != "privilege" { t.Fatalf("check name = %q, want %q", c.Name, "privilege") } if os.Geteuid() == 0 && !c.OK { t.Fatalf("root should pass the privilege check: %+v", c) } } func TestPackageHint(t *testing.T) { for bin, want := range map[string]string{ "nft": "install nftables", "ip": "install iproute2", "systemctl": "install systemd", "journalctl": "install systemd", } { if got := packageHint(bin); got != want { t.Errorf("packageHint(%q) = %q, want %q", bin, got, want) } } } // errorText(nil) must be empty so a valid state is not reported as unreadable. func TestErrorText(t *testing.T) { if got := errorText(nil); got != "" { t.Fatalf("errorText(nil) = %q, want \"\"", got) } if got := errorText(errors.New("boom")); got != "boom" { t.Fatalf("errorText(err) = %q, want %q", got, "boom") } }