package main import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "proxyctl/internal/app" "proxyctl/internal/gnome" "proxyctl/internal/operationlock" "proxyctl/internal/oplog" "proxyctl/internal/profile" "proxyctl/internal/target" ) type failingIdentityEnvironment struct{} func (failingIdentityEnvironment) CurrentUser() (profile.UserIdentity, error) { return profile.UserIdentity{}, errors.New("identity lookup failed") } func (failingIdentityEnvironment) LookupUser(string) (profile.UserIdentity, error) { return profile.UserIdentity{}, os.ErrNotExist } func (failingIdentityEnvironment) LookupEnv(string) (string, bool) { return "", false } type failingOperationLogger struct{ err error } func (l failingOperationLogger) Write(app.LogEntry) error { return l.err } type processFakeTarget struct { id string state target.State err error calls int confirmation string } func (f *processFakeTarget) ID() string { return f.id } func (f *processFakeTarget) Description() target.Description { return target.Description{Name: f.id} } func (f *processFakeTarget) Availability(context.Context) target.Availability { return target.Availability{Available: true, Evidence: "fake executable found"} } func (f *processFakeTarget) Confirmation(bool) string { return f.confirmation } func (f *processFakeTarget) Inspect(context.Context, *profile.Profile) target.Inspection { return target.Inspection{State: f.state, Evidence: "fake evidence for http://alice:secret@proxy.test"} } func (f *processFakeTarget) SetDesired(context.Context, bool, *profile.Profile) target.OperationResult { f.calls++ if f.err == nil { f.state = target.Disabled } return target.OperationResult{Inspection: target.Inspection{State: f.state, Evidence: "re-inspected fake state"}, Err: f.err} } func (f *processFakeTarget) DiagnosticChecks(context.Context, *profile.Profile) []target.Check { return []target.Check{{Name: "required commands", OK: true, Evidence: "fake helper found"}, {Name: "write authority", OK: true, Evidence: "fake location writable"}} } func TestDoctorCommandUsesFakeTargetsAndIsolatedXDGWithoutMutation(t *testing.T) { xdg := filepath.Join(t.TempDir(), "xdg-config") store := profile.NewFileStore(filepath.Join(xdg, "proxyctl", "profiles.json")) fake := &processFakeTarget{id: "fake", state: target.External} var output bytes.Buffer if err := runMaintenanceCommand([]string{"doctor"}, strings.NewReader(""), &output, store, []target.Target{fake}, nil, nil, []target.Check{{Name: "identity resolution", OK: true, Evidence: "fake user"}, {Name: "storage permissions", OK: true, Evidence: xdg}}); err != nil { t.Fatal(err) } text := output.String() for _, want := range []string{"Profile validity", "identity resolution", "storage permissions", "Target availability", "External", "required commands", "write authority"} { if !strings.Contains(text, want) { t.Errorf("doctor output missing %q: %s", want, text) } } if strings.Contains(text, "secret") { t.Fatalf("doctor leaked credentials: %s", text) } if fake.calls != 0 { t.Fatalf("doctor mutated fake Target %d times", fake.calls) } if _, err := os.Stat(xdg); !os.IsNotExist(err) { t.Fatalf("doctor created isolated XDG path: %v", err) } } func TestDoctorReportsIdentityResolutionFailureStructurally(t *testing.T) { var output bytes.Buffer err := runWithEnvironment([]string{"doctor"}, strings.NewReader(""), &output, failingIdentityEnvironment{}, nil, "/test/proxyctl") if err == nil || !strings.Contains(output.String(), "[FAIL] identity resolution") || !strings.Contains(output.String(), "identity lookup failed") { t.Fatalf("doctor identity failure err=%v output=%q", err, output.String()) } } func TestDoctorReturnsFailureWhenAnyCheckFails(t *testing.T) { store := profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")) fake := &processFakeTarget{id: "fake", state: target.Drifted} var output bytes.Buffer err := runMaintenanceCommand([]string{"doctor"}, strings.NewReader(""), &output, store, []target.Target{fake}, nil, nil, []target.Check{{Name: "identity", OK: true, Evidence: "ok"}}) if err == nil || !strings.Contains(output.String(), "[FAIL]") { t.Fatalf("unhealthy doctor err=%v output=%q", err, output.String()) } } func TestDoctorStorageAuthorityRejectsSymlinkedPathsWithoutTouchingVictim(t *testing.T) { for _, test := range []struct { name string path func(t *testing.T, root, victim string) string }{ { name: "final path", path: func(t *testing.T, root, victim string) string { t.Helper() path := filepath.Join(root, "config", "profiles.json") if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.Symlink(victim, path); err != nil { t.Fatal(err) } return path }, }, { name: "ancestor path", path: func(t *testing.T, root, victim string) string { t.Helper() link := filepath.Join(root, "config") if err := os.Symlink(filepath.Dir(victim), link); err != nil { t.Fatal(err) } return filepath.Join(link, filepath.Base(victim)) }, }, } { t.Run(test.name, func(t *testing.T) { root := t.TempDir() victim := filepath.Join(t.TempDir(), "victim.json") const content = `{"profiles":[]}` if err := os.WriteFile(victim, []byte(content), 0o600); err != nil { t.Fatal(err) } before, err := os.Stat(victim) if err != nil { t.Fatal(err) } configuration := profile.Configuration{Path: test.path(t, root, victim), Owner: profile.UserIdentity{UID: os.Getuid(), GID: os.Getgid()}} checks := diagnosticChecks(configuration) if len(checks) != 2 || checks[1].OK || !strings.Contains(checks[1].Evidence, "symbolic link") { t.Fatalf("Profile storage diagnostics accepted symlink: %#v", checks) } after, err := os.Stat(victim) if err != nil { t.Fatal(err) } got, err := os.ReadFile(victim) if err != nil { t.Fatal(err) } if string(got) != content || !reflect.DeepEqual(before.Sys(), after.Sys()) { t.Fatalf("storage diagnostics changed victim: before=%#v after=%#v content=%q", before, after, got) } }) } } func TestDoctorStoragePermissionCheckIsReadOnly(t *testing.T) { path := filepath.Join(t.TempDir(), "proxyctl", "profiles.json") writeConfig(t, path, `{"profiles":[]}`) if err := os.Chmod(path, 0o644); err != nil { t.Fatal(err) } configuration := profile.Configuration{Path: path, Owner: profile.UserIdentity{UID: os.Getuid(), GID: os.Getgid()}} checks := diagnosticChecks(configuration) if checks[1].OK || !strings.Contains(checks[1].Evidence, "want 600") { t.Fatalf("storage check = %#v", checks[1]) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0o644 { t.Fatalf("doctor permission check changed mode to %o", info.Mode().Perm()) } } func TestCommandsRunAsRealProcessesInsideIsolatedUserState(t *testing.T) { root := t.TempDir() binary := filepath.Join(root, "bin", "proxyctl") if err := os.MkdirAll(filepath.Dir(binary), 0o700); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(root, "home"), 0o700); err != nil { t.Fatal(err) } build := exec.Command("go", "build", "-tags", "proxyctl_process_test", "-o", binary, ".") if output, err := build.CombinedOutput(); err != nil { t.Fatalf("build proxyctl: %v\n%s", err, output) } environment := isolatedProcessEnvironment(root) invoke := func(input string, args ...string) ([]byte, error) { t.Helper() command := exec.Command(binary, args...) command.Env = environment command.Stdin = strings.NewReader(input) return command.CombinedOutput() } defaultOutput, err := invoke("q") if err != nil || !strings.Contains(string(defaultOutput), "proxyctl — Target panel") { t.Fatalf("default process err=%v output=%q", err, defaultOutput) } versionOutput, err := invoke("", "version", "--short") if err != nil || strings.TrimSpace(string(versionOutput)) == "" || strings.Contains(string(versionOutput), " ") { t.Fatalf("version process err=%v output=%q", err, versionOutput) } doctorOutput, doctorErr := invoke("", "doctor") if !strings.Contains(string(doctorOutput), "Profile validity") || (doctorErr != nil && !strings.Contains(string(doctorOutput), "[FAIL]")) { t.Fatalf("doctor process err=%v output=%q", doctorErr, doctorOutput) } resetMarker := filepath.Join(root, "home", "reset-mutation-marker") resetOutput, err := invoke("no\n", "reset") if err != nil || !strings.Contains(string(resetOutput), "Reset cancelled") || !strings.Contains(string(resetOutput), "Docker will restart immediately") { t.Fatalf("reset cancellation process err=%v output=%q", err, resetOutput) } if _, statErr := os.Stat(resetMarker); !errors.Is(statErr, os.ErrNotExist) { t.Fatalf("cancelled real process mutated reset fixture: %v", statErr) } resetOutput, err = invoke("yes\n", "reset") if err != nil || !strings.Contains(string(resetOutput), "shell: skipped (Disabled)") || !strings.Contains(string(resetOutput), "docker-daemon-test: reset (Disabled)") { t.Fatalf("confirmed reset process err=%v output=%q", err, resetOutput) } if data, readErr := os.ReadFile(resetMarker); readErr != nil || string(data) != "reset\n" { t.Fatalf("confirmed real process marker err=%v data=%q", readErr, data) } installOutput, err := invoke("", "shell", "install") if err != nil || !strings.Contains(string(installOutput), "installed") { t.Fatalf("shell install process err=%v output=%q", err, installOutput) } startup := filepath.Join(root, "home", ".bashrc") startupData, err := os.ReadFile(startup) if err != nil || !strings.Contains(string(startupData), "proxyctl shell integration") { t.Fatalf("isolated startup after install err=%v data=%q", err, startupData) } uninstallOutput, err := invoke("", "shell", "uninstall") if err != nil || !strings.Contains(string(uninstallOutput), "uninstalled") { t.Fatalf("shell uninstall process err=%v output=%q", err, uninstallOutput) } startupData, err = os.ReadFile(startup) if err != nil || strings.Contains(string(startupData), "proxyctl shell integration") { t.Fatalf("isolated startup after uninstall err=%v data=%q", err, startupData) } for _, path := range []string{ filepath.Join(root, "etc"), filepath.Join(root, "var"), filepath.Join(root, "home", ".docker"), } { if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("process smoke created prohibited system/Docker fixture %s: %v", path, err) } } } func isolatedProcessEnvironment(root string) []string { blocked := map[string]bool{"HOME": true, "XDG_CONFIG_HOME": true, "XDG_STATE_HOME": true, "SHELL": true, "SUDO_USER": true, "HTTP_PROXY": true, "http_proxy": true, "HTTPS_PROXY": true, "https_proxy": true, "ALL_PROXY": true, "all_proxy": true, "NO_PROXY": true, "no_proxy": true} environment := make([]string, 0, len(os.Environ())+4) for _, item := range os.Environ() { name, _, _ := strings.Cut(item, "=") if !blocked[name] { environment = append(environment, item) } } return append(environment, "HOME="+filepath.Join(root, "home"), "XDG_CONFIG_HOME="+filepath.Join(root, "config"), "XDG_STATE_HOME="+filepath.Join(root, "state"), "SHELL=/bin/bash", "PROXYCTL_PROCESS_TEST_HOME="+filepath.Join(root, "home"), ) } type passwordEchoRunner struct { values map[string]string failPasswordSet bool } func newPasswordEchoRunner(savedPassword string) *passwordEchoRunner { return &passwordEchoRunner{values: map[string]string{ "org.gnome.system.proxy mode": "'auto'", "org.gnome.system.proxy autoconfig-url": "''", "org.gnome.system.proxy ignore-hosts": "[]", "org.gnome.system.proxy.http host": "''", "org.gnome.system.proxy.http port": "0", "org.gnome.system.proxy.http use-authentication": "false", "org.gnome.system.proxy.http authentication-user": "''", "org.gnome.system.proxy.http authentication-password": "'" + savedPassword + "'", "org.gnome.system.proxy.https host": "''", "org.gnome.system.proxy.https port": "0", "org.gnome.system.proxy.socks host": "''", "org.gnome.system.proxy.socks port": "0", }} } func (r *passwordEchoRunner) Run(_ context.Context, command gnome.Command) (string, error) { if len(command.Args) == 1 && command.Args[0] == "list-schemas" { return "org.gnome.system.proxy\norg.gnome.system.proxy.http\norg.gnome.system.proxy.https\norg.gnome.system.proxy.socks\n", nil } if len(command.Args) == 3 && command.Args[0] == "get" { return r.values[command.Args[1]+" "+command.Args[2]] + "\n", nil } if len(command.Args) == 4 && command.Args[0] == "set" { if r.failPasswordSet && command.Args[2] == "authentication-password" { return "", fmt.Errorf("runner echoed saved argument %s: %w", command.Args[3], os.ErrPermission) } r.values[command.Args[1]+" "+command.Args[2]] = command.Args[3] return "", nil } return "", fmt.Errorf("unexpected command: %v", command.Args) } func passwordRestoreFixture(t *testing.T, secret string) (*gnome.Adapter, *passwordEchoRunner) { t.Helper() root := t.TempDir() runner := newPasswordEchoRunner(secret) statePath := filepath.Join(root, "state", "gnome.json") adapter := gnome.New(gnome.Configuration{CommandName: "gsettings", UID: os.Getuid(), GID: os.Getgid(), HomeDir: root, StateFile: statePath}, runner, gnome.NewFileState(statePath, -1, -1, root)) active := &profile.Profile{Name: "office", Mode: profile.AdvancedMode, HTTPURL: "http://proxy.example:8080", Bypass: []string{"localhost"}} if result := adapter.SetDesired(context.Background(), true, active); result.Err != nil || result.Inspection.State != target.Enabled { t.Fatalf("prepare managed GNOME Target: %#v", result) } runner.failPasswordSet = true return adapter, runner } func TestResetSuppressesSavedGNOMEPasswordFromOperationCLIAndLogsWithoutActiveProfile(t *testing.T) { const secret = "GNOME-ORIGINAL-PASSWORD-FULL-CHAIN-07" direct, _ := passwordRestoreFixture(t, secret) operation := direct.SetDesired(context.Background(), false, nil) if operation.Err == nil { t.Fatal("password restore unexpectedly succeeded") } if !errors.Is(operation.Err, os.ErrPermission) { t.Fatalf("OperationResult lost safe permission classification: %v", operation.Err) } if rendered := fmt.Sprint(operation); strings.Contains(rendered, secret) || strings.Contains(rendered, "runner echoed") { t.Fatalf("unsafe OperationResult: %s", rendered) } managed, _ := passwordRestoreFixture(t, secret) root := t.TempDir() config := filepath.Join(root, "config", "profiles.json") writeConfig(t, config, `{"profiles":[]}`) store := profile.NewFileStore(config) stateDir := filepath.Join(root, "state") lock := operationlock.New(filepath.Join(stateDir, "operation.lock")) logger := oplog.New(filepath.Join(stateDir, "operations.log"), 1024, 1) var output bytes.Buffer err := runMaintenanceCommand([]string{"reset"}, strings.NewReader("yes\n"), &output, store, []target.Target{managed}, lock, logger, nil) if err == nil { t.Fatal("reset unexpectedly succeeded") } logData, readErr := os.ReadFile(filepath.Join(stateDir, "operations.log")) if readErr != nil { t.Fatal(readErr) } for surface, value := range map[string]string{"CLI output": output.String(), "returned error": err.Error(), "operation log": string(logData)} { if strings.Contains(value, secret) || strings.Contains(value, "runner echoed") { t.Fatalf("%s exposed saved password or raw runner error: %q", surface, value) } if !strings.Contains(value, "sensitive argument and runner error suppressed") { t.Fatalf("%s lacks fixed safe failure message: %q", surface, value) } } } func TestResetPromptIncludesEveryManagedTargetWarningAndCancellationDoesNotMutate(t *testing.T) { root := t.TempDir() config := filepath.Join(root, "profiles.json") writeConfig(t, config, `{"profiles":[]}`) store := profile.NewFileStore(config) daemon := &processFakeTarget{id: "docker-daemon", state: target.Drifted, confirmation: "Docker will restart immediately and may disrupt running containers and other work."} other := &processFakeTarget{id: "other", state: target.Enabled, confirmation: "Other managed warning."} external := &processFakeTarget{id: "external", state: target.External, confirmation: "must not appear"} var output bytes.Buffer err := runMaintenanceCommand([]string{"reset"}, strings.NewReader("no\n"), &output, store, []target.Target{daemon, other, external}, operationlock.New(filepath.Join(root, "operation.lock")), nil, nil) if err != nil { t.Fatal(err) } text := output.String() for _, want := range []string{"docker-daemon: Docker will restart immediately", "other: Other managed warning", "Reset cancelled"} { if !strings.Contains(text, want) { t.Errorf("reset output missing %q: %q", want, text) } } if strings.Contains(text, "must not appear") || daemon.calls+other.calls+external.calls != 0 { t.Fatalf("cancel output=%q calls=%d/%d/%d", text, daemon.calls, other.calls, external.calls) } } func TestResetCommandReportsCompletedTargetTruthWhenOperationLoggingFails(t *testing.T) { root := t.TempDir() config := filepath.Join(root, "profiles.json") writeConfig(t, config, `{"profiles":[]}`) managed := &processFakeTarget{id: "managed", state: target.Enabled} var output bytes.Buffer err := runMaintenanceCommand([]string{"reset"}, strings.NewReader("yes\n"), &output, profile.NewFileStore(config), []target.Target{managed}, operationlock.New(filepath.Join(root, "operation.lock")), failingOperationLogger{err: errors.New("log device full")}, nil) if err == nil || !strings.Contains(err.Error(), "log device full") { t.Fatalf("logger failure err=%v output=%q", err, output.String()) } if managed.calls != 1 || managed.state != target.Disabled || !strings.Contains(output.String(), "managed: reset (Disabled)") || !strings.Contains(output.String(), "operation logging failed") { t.Fatalf("logger failure truth output=%q calls=%d state=%s", output.String(), managed.calls, managed.state) } } func TestResetCommandCancellationAndTruthfulPartialFailureWithFakeTargets(t *testing.T) { root := t.TempDir() config := filepath.Join(root, "xdg-config", "proxyctl", "profiles.json") writeConfig(t, config, `{"profiles":[]}`) store := profile.NewFileStore(config) good := &processFakeTarget{id: "good", state: target.Enabled} bad := &processFakeTarget{id: "bad", state: target.Drifted, err: errors.New("remove http://bob:hunter2@proxy.test failed")} external := &processFakeTarget{id: "external", state: target.External} stateDir := filepath.Join(root, "xdg-state", "proxyctl") lock := operationlock.New(filepath.Join(stateDir, "operation.lock")) logger := oplog.New(filepath.Join(stateDir, "operations.log"), 1024, 1) var output bytes.Buffer if err := runMaintenanceCommand([]string{"reset"}, strings.NewReader("no\n"), &output, store, []target.Target{good, bad, external}, lock, logger, nil); err != nil { t.Fatal(err) } if good.calls+bad.calls+external.calls != 0 || !strings.Contains(output.String(), "cancelled") { t.Fatalf("cancel result output=%q calls=%d/%d/%d", output.String(), good.calls, bad.calls, external.calls) } output.Reset() err := runMaintenanceCommand([]string{"reset"}, strings.NewReader("yes\n"), &output, store, []target.Target{good, bad, external}, lock, logger, nil) if err == nil { t.Fatal("partial reset returned success") } text := output.String() if good.calls != 1 || bad.calls != 1 || external.calls != 0 || !strings.Contains(text, "good: reset (Disabled)") || !strings.Contains(text, "bad: failed (Drifted)") || !strings.Contains(text, "fresh inspection: fake evidence for http://alice:REDACTED@proxy.test") || !strings.Contains(text, "external: skipped (External)") { t.Fatalf("partial reset output=%q calls=%d/%d/%d", text, good.calls, bad.calls, external.calls) } if strings.Contains(text, "hunter2") || strings.Contains(text, "secret") || strings.Contains(err.Error(), "hunter2") { t.Fatalf("reset leaked credentials: output=%q err=%v", text, err) } logData, readErr := os.ReadFile(filepath.Join(stateDir, "operations.log")) if readErr != nil { t.Fatal(readErr) } if strings.Contains(string(logData), "hunter2") || strings.Contains(string(logData), "bob:") { t.Fatalf("log leaked full authenticated URL: %s", logData) } }