Files
prx/internal/client/client_test.go
T

153 lines
4.8 KiB
Go
Raw Normal View History

// 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)
}
}
}