package main import ( "errors" "os" "path/filepath" "strings" "testing" ) type fakeCommandRunner struct { calls []string outputs map[string]string errors map[string]error } func (f *fakeCommandRunner) CombinedOutput(name string, args ...string) ([]byte, error) { call := name + " " + strings.Join(args, " ") f.calls = append(f.calls, call) if out, ok := f.outputs[call]; ok { return []byte(out), f.errors[call] } return nil, f.errors[call] } func TestEnsureOnboardConfigCreatesDefaultConfig(t *testing.T) { configPath := filepath.Join(t.TempDir(), "pinchtab", "config.json") t.Setenv("PINCHTAB_CONFIG", configPath) t.Setenv("PINCHTAB_TOKEN", "") t.Setenv("PINCHTAB_BIND", "") gotPath, cfg, status, err := ensureDaemonConfig(false) if err != nil { t.Fatalf("ensureDaemonConfig returned error: %v", err) } if status != configCreated { t.Fatalf("status = %q, want %q", status, configCreated) } if gotPath != configPath { t.Fatalf("config path = %q, want %q", gotPath, configPath) } if cfg.Server.Bind != "127.0.0.1" { t.Fatalf("bind = %q, want 127.0.0.1", cfg.Server.Bind) } if strings.TrimSpace(cfg.Server.Token) == "" { t.Fatal("expected generated token to be set") } data, err := os.ReadFile(configPath) if err != nil { t.Fatalf("reading config file: %v", err) } content := string(data) if !strings.Contains(content, `"bind": "127.0.0.1"`) { t.Fatalf("expected config file to include bind, got %s", content) } if !strings.Contains(content, `"token": "`) { t.Fatalf("expected config file to include token, got %s", content) } } func TestEnsureOnboardConfigRecoversExistingSecuritySettings(t *testing.T) { configPath := filepath.Join(t.TempDir(), "pinchtab", "config.json") t.Setenv("PINCHTAB_CONFIG", configPath) input := `{ "server": { "bind": "0.0.0.0", "port": "9999", "token": "" }, "browser": { "binary": "/custom/chrome" }, "security": { "allowEvaluate": true } } ` if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { t.Fatalf("creating config dir: %v", err) } if err := os.WriteFile(configPath, []byte(input), 0644); err != nil { t.Fatalf("writing config file: %v", err) } _, cfg, status, err := ensureDaemonConfig(false) if err != nil { t.Fatalf("ensureDaemonConfig returned error: %v", err) } // Only token recovery happens now — security settings are preserved for wizard if status != configRecovered { t.Fatalf("status = %q, want %q", status, configRecovered) } // Bind is preserved as-is (wizard handles security changes, not daemon config) if cfg.Server.Bind != "0.0.0.0" { t.Fatalf("bind = %q, want 0.0.0.0 (preserved)", cfg.Server.Bind) } if cfg.Server.Port != "9999" { t.Fatalf("port = %q, want 9999", cfg.Server.Port) } if cfg.Browser.ChromeBinary != "/custom/chrome" { t.Fatalf("chrome binary = %q, want /custom/chrome", cfg.Browser.ChromeBinary) } // Security settings preserved — not overwritten if !boolPtrValue(cfg.Security.AllowEvaluate) { t.Fatal("expected allowEvaluate to be preserved as true") } // Token should be generated (was empty) if strings.TrimSpace(cfg.Server.Token) == "" { t.Fatal("expected recovery to generate a token") } } func TestSystemdUserManagerInstallWritesUnitAndEnablesService(t *testing.T) { root := t.TempDir() runner := &fakeCommandRunner{} manager := &systemdUserManager{ env: daemonEnvironment{ homeDir: root, osName: "linux", execPath: "/usr/local/bin/pinchtab", xdgConfigHome: filepath.Join(root, ".config"), }, runner: runner, } message, err := manager.Install("/usr/local/bin/pinchtab", "/tmp/pinchtab/config.json") if err != nil { t.Fatalf("Install returned error: %v", err) } if !strings.Contains(message, manager.ServicePath()) { t.Fatalf("install message = %q, want path %q", message, manager.ServicePath()) } data, err := os.ReadFile(manager.ServicePath()) if err != nil { t.Fatalf("reading service file: %v", err) } content := string(data) if !strings.Contains(content, `ExecStart="/usr/local/bin/pinchtab" server`) { t.Fatalf("unexpected unit content: %s", content) } if !strings.Contains(content, `Environment="PINCHTAB_CONFIG=/tmp/pinchtab/config.json"`) { t.Fatalf("expected config env in unit content: %s", content) } expectedCalls := []string{ "systemctl --user daemon-reload", "systemctl --user enable --now pinchtab.service", } if strings.Join(runner.calls, "\n") != strings.Join(expectedCalls, "\n") { t.Fatalf("systemd calls = %v, want %v", runner.calls, expectedCalls) } } func TestLaunchdManagerInstallWritesPlistAndBootstrapsAgent(t *testing.T) { root := t.TempDir() runner := &fakeCommandRunner{} manager := &launchdManager{ env: daemonEnvironment{ homeDir: root, osName: "darwin", execPath: "/Applications/Pinchtab.app/Contents/MacOS/pinchtab", userID: "501", }, runner: runner, } message, err := manager.Install("/Applications/Pinchtab.app/Contents/MacOS/pinchtab", "/tmp/pinchtab/config.json") if err != nil { t.Fatalf("Install returned error: %v", err) } if !strings.Contains(message, manager.ServicePath()) { t.Fatalf("install message = %q, want path %q", message, manager.ServicePath()) } data, err := os.ReadFile(manager.ServicePath()) if err != nil { t.Fatalf("reading launchd plist: %v", err) } content := string(data) if !strings.Contains(content, "com.pinchtab.pinchtab") { t.Fatalf("expected launchd label in plist: %s", content) } if !strings.Contains(content, "/Applications/Pinchtab.app/Contents/MacOS/pinchtab") { t.Fatalf("expected executable path in plist: %s", content) } if !strings.Contains(content, "/tmp/pinchtab/config.json") { t.Fatalf("expected config path in plist: %s", content) } expectedCalls := []string{ "launchctl bootout gui/501 " + manager.ServicePath(), "launchctl bootstrap gui/501 " + manager.ServicePath(), "launchctl kickstart -k gui/501/com.pinchtab.pinchtab", } if strings.Join(runner.calls, "\n") != strings.Join(expectedCalls, "\n") { t.Fatalf("launchctl calls = %v, want %v", runner.calls, expectedCalls) } } func TestSystemdUserManagerPreflightRequiresUserSession(t *testing.T) { runner := &fakeCommandRunner{ errors: map[string]error{ "systemctl --user show-environment": errors.New("exit status 1"), }, } manager := &systemdUserManager{ env: daemonEnvironment{osName: "linux"}, runner: runner, } err := manager.Preflight() if err == nil { t.Fatal("expected preflight error") } if !strings.Contains(err.Error(), "working user systemd session") { t.Fatalf("unexpected error: %v", err) } } func TestLaunchdManagerPreflightRequiresGUIDomain(t *testing.T) { runner := &fakeCommandRunner{ errors: map[string]error{ "launchctl print gui/501": errors.New("exit status 113"), }, } manager := &launchdManager{ env: daemonEnvironment{osName: "darwin", userID: "501"}, runner: runner, } err := manager.Preflight() if err == nil { t.Fatal("expected preflight error") } if !strings.Contains(err.Error(), "active launchd GUI session") { t.Fatalf("unexpected error: %v", err) } } func TestNewDaemonManagerRejectsUnsupportedOS(t *testing.T) { _, err := newDaemonManager(daemonEnvironment{osName: "windows"}, &fakeCommandRunner{}) if err == nil { t.Fatal("expected unsupported OS error") } } func TestDaemonMenuOptions(t *testing.T) { tests := []struct { name string installed bool running bool want []string }{ { name: "not installed", installed: false, running: false, want: []string{"install", "exit"}, }, { name: "installed stopped", installed: true, running: false, want: []string{"start", "uninstall", "exit"}, }, { name: "installed running", installed: true, running: true, want: []string{"stop", "restart", "uninstall", "exit"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := daemonMenuOptions(tt.installed, tt.running) if len(got) != len(tt.want) { t.Fatalf("len(daemonMenuOptions()) = %d, want %d", len(got), len(tt.want)) } for i, want := range tt.want { if got[i].value != want { t.Fatalf("daemonMenuOptions()[%d] = %q, want %q", i, got[i].value, want) } } }) } }