From 96d75d9f1fab87365d7e6b5070eed3a5757c3484 Mon Sep 17 00:00:00 2001 From: James O'Doherty Date: Fri, 22 May 2026 09:18:55 -0400 Subject: Refactor CLI for testability and implement hermetic config path injection --- internal/cli/cli.go | 114 +++++++++++++++++++++++++++++++++++++++++++++++ internal/cli/cli_test.go | 34 ++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 internal/cli/cli.go create mode 100644 internal/cli/cli_test.go (limited to 'internal/cli') diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..cb95202 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,114 @@ +package cli + +import ( + "flag" + "fmt" + + "git.theodohertyfamily.com/tools/wg-wrap/internal/config" +) + +type App struct { + Args []string + ConfigDir string // Optional override for profile storage location +} + +func NewApp(args []string) *App { + return &App{Args: args} +} + +func (a *App) Run() error { + // Handle subcommands first (profile list, import, configure, delete, stop) + if len(a.Args) > 1 && a.Args[1] == "profile" { + return a.handleProfileCmd() + } + + cfg := &config.Config{} + + fs := flag.NewFlagSet("wg-wrap", flag.ExitOnError) + fs.StringVar(&cfg.Profile, "profile", "", "WireGuard profile to use (filename without extension in ~/.config/wg-wrap/profiles/)") + fs.StringVar(&cfg.DNSServer, "dns-server", "", "Override DNS server to use") + + args := a.Args[1:] + sepIdx := -1 + for i, arg := range args { + if arg == "--" { + sepIdx = i + break + } + } + + var flagsToParse []string + if sepIdx != -1 { + flagsToParse = args[:sepIdx] + cfg.Command = args[sepIdx+1:] + } else { + flagsToParse = args + } + + err := fs.Parse(flagsToParse) + if err != nil { + return fmt.Errorf("error parsing flags: %w", err) + } + + if sepIdx == -1 { + cfg.Command = fs.Args() + } + + if len(cfg.Command) == 0 { + return fmt.Errorf("no command provided. use --help for usage") + } + + if cfg.Profile == "" { + cfg.Profile = "default" + } + + profilesDir := a.ConfigDir + if profilesDir == "" { + profilesDir = config.GetDefaultProfilesDir() + } + + fmt.Printf("Profile: %s\n", cfg.Profile) + fmt.Printf("Profiles Directory: %s\n", profilesDir) + fmt.Printf("DNS Server: %s\n", cfg.DNSServer) + fmt.Printf("Command: %v\n", cfg.Command) + return nil +} + +func (a *App) handleProfileCmd() error { + if len(a.Args) < 3 { + return fmt.Errorf("usage: wg-wrap profile [args]") + } + + subCmd := a.Args[2] + switch subCmd { + case "list": + fmt.Println("Listing profiles...") + return fmt.Errorf("profile list not yet implemented") + case "import": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile import ") + } + fmt.Printf("Importing profile from %s...\n", a.Args[3]) + return fmt.Errorf("profile import not yet implemented") + case "configure": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile configure ") + } + fmt.Printf("Configuring profile %s...\n", a.Args[3]) + return fmt.Errorf("profile configure not yet implemented") + case "delete": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile delete ") + } + fmt.Printf("Deleting profile %s...\n", a.Args[3]) + return fmt.Errorf("profile delete not yet implemented") + case "stop": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile stop ") + } + fmt.Printf("Stopping profile %s and unpinning namespace...\n", a.Args[3]) + return fmt.Errorf("profile stop not yet implemented") + default: + return fmt.Errorf("unknown profile subcommand: %s", subCmd) + } +} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..71ff6cb --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,34 @@ +package cli + +import ( + "testing" +) + +func TestAppRun_ProfileDirInjection(t *testing.T) { + // Set up a temporary directory to simulate XDG_CONFIG_HOME/wg-wrap/profiles + tmpDir := t.TempDir() + + tests := []struct { + name string + args []string + wantErr bool + }{ + { + name: "valid profile with injected dir", + args: []string{"wg-wrap", "--profile", "test-vpn", "curl", "google.com"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := NewApp(tt.args) + app.ConfigDir = tmpDir // Inject temporary directory + + err := app.Run() + if (err != nil) != tt.wantErr { + t.Errorf("App.Run() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} -- cgit v1.2.3