// SPDX-License-Identifier: MIT package client import ( "context" "encoding/json" "os" "os/exec" "path/filepath" "strings" "testing" "prx/internal/proto" ) // writeFakeTool installs an executable shell script as name in a temporary // directory and prepends that directory to PATH for the duration of the test. func writeFakeTool(t *testing.T, name, script string) { t.Helper() dir := t.TempDir() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(script), 0o755); err != nil { t.Fatalf("write fake %s: %v", name, err) } t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) } // The helper exits non-zero when an operation fails, but its structured // answer on stdout must still reach the caller — otherwise the real reason // for the failure is replaced by a bare "exit status 1". func TestRunElevatedPreservesStructuredResult(t *testing.T) { writeFakeTool(t, "sudo", `#!/bin/sh echo '{"action":"apply","ok":false,"error":"engine start failed"}' exit 1 `) out, err := runElevated(context.Background(), "sudo", "/usr/lib/prx/prx-helper", []byte(`{"action":"apply"}`)) if err != nil { t.Fatalf("structured result should be preferred over the exit status: %v", err) } var res proto.Result if err := json.Unmarshal(out, &res); err != nil { t.Fatalf("decode: %v", err) } if res.OK || res.Error != "engine start failed" { t.Fatalf("result = %+v, want OK=false with the helper's error", res) } } func TestRunElevatedSudoPasswordRequired(t *testing.T) { writeFakeTool(t, "sudo", `#!/bin/sh echo "sudo: a password is required" >&2 exit 1 `) _, err := runElevated(context.Background(), "sudo", "/usr/lib/prx/prx-helper", []byte("{}")) if err == nil || !strings.Contains(err.Error(), "password") { t.Fatalf("err = %v, want a password hint", err) } if !strings.Contains(err.Error(), "sudo prx") { t.Fatalf("error should tell the user what to do: %v", err) } } func TestRunElevatedHelperProducesNothing(t *testing.T) { writeFakeTool(t, "sudo", "#!/bin/sh\nexit 0\n") _, err := runElevated(context.Background(), "sudo", "/usr/lib/prx/prx-helper", []byte("{}")) if err == nil || !strings.Contains(err.Error(), "no result") { t.Fatalf("err = %v, want 'no result'", err) } } func TestRunElevatedNonJSONOutputIsAnError(t *testing.T) { writeFakeTool(t, "sudo", "#!/bin/sh\necho 'boom: not json'\nexit 3\n") _, err := runElevated(context.Background(), "sudo", "/usr/lib/prx/prx-helper", []byte("{}")) if err == nil || !strings.Contains(err.Error(), "boom: not json") { t.Fatalf("err = %v, want the tool's output", err) } } func TestClassifyFailurePKexecCancellation(t *testing.T) { cmd := exec.Command("sh", "-c", "exit 126") err := cmd.Run() if err == nil { t.Fatal("expected 'sh -c exit 126' to fail") } got := classifyFailure("pkexec", err, nil, nil) if !strings.Contains(got.Error(), "cancelled") { t.Fatalf("got %q, want a cancellation message", got) } } func TestClassifyFailureUnknownToolError(t *testing.T) { got := classifyFailure("sudo", exec.Command("sh", "-c", "exit 5").Run(), nil, []byte("sudo: unknown user: ahp")) if !strings.Contains(got.Error(), "unknown user") { t.Fatalf("got %q, want the stderr detail", got) } } func TestClassifyFailureIncludesStdout(t *testing.T) { got := classifyFailure("pkexec", exec.Command("sh", "-c", "exit 5").Run(), []byte("helper exploded"), nil) if !strings.Contains(got.Error(), "helper exploded") { t.Fatalf("got %q, want the stdout detail", got) } } func TestHasStructuredResult(t *testing.T) { if !hasStructuredResult([]byte(`{"ok":true}`)) { t.Error("valid Result should be detected") } for _, bad := range []string{"", " \n", "not json", "[]", "42"} { if hasStructuredResult([]byte(bad)) { t.Errorf("hasStructuredResult(%q) = true, want false", bad) } } } func TestFindHelperHonoursEnvOverride(t *testing.T) { dir := t.TempDir() p := filepath.Join(dir, "prx-helper") if err := os.WriteFile(p, []byte("#!/bin/sh\n"), 0o755); err != nil { t.Fatal(err) } t.Setenv("PRX_HELPER", p) got, err := FindHelper() if err != nil { t.Fatalf("FindHelper: %v", err) } if got != p { t.Fatalf("got %q, want %q", got, p) } t.Setenv("PRX_HELPER", filepath.Join(dir, "does-not-exist")) if _, err := FindHelper(); err == nil { t.Fatal("expected an error for a broken PRX_HELPER override") } } // The helper must be discoverable on every common FHS layout, not just the one // used by the machine prx was developed on. func TestHelperCandidatesCoverCommonLayouts(t *testing.T) { joined := strings.Join(helperCandidates(), "\n") for _, want := range []string{ "/usr/lib/prx/prx-helper", "/usr/libexec/prx/prx-helper", "/usr/local/lib/prx/prx-helper", "/usr/local/libexec/prx/prx-helper", } { if !strings.Contains(joined, want) { t.Errorf("helperCandidates missing %s", want) } } }