From d6f31dbe5749e01a989c6327de726b83241fc50f Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 20:22:48 +0000 Subject: [PATCH] fix(cli): tighten chain command validation Co-Authored-By: Charon --- chain_commands.go | 15 +++++++++++ commands_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++ explorer_command.go | 4 +++ sync_command.go | 7 +++++ 4 files changed, 91 insertions(+) diff --git a/chain_commands.go b/chain_commands.go index 08cd8ce..641f262 100644 --- a/chain_commands.go +++ b/chain_commands.go @@ -6,6 +6,8 @@ package blockchain import ( + "fmt" + "net" "os" "path/filepath" @@ -75,3 +77,16 @@ func ensureChainDataDirExists(dataDir string) error { } return nil } + +func validateChainOptions(dataDir, seed string) error { + if dataDir == "" { + return coreerr.E("validateChainOptions", "data dir is required", nil) + } + if seed == "" { + return coreerr.E("validateChainOptions", "seed is required", nil) + } + if _, _, err := net.SplitHostPort(seed); err != nil { + return coreerr.E("validateChainOptions", fmt.Sprintf("seed %q must be host:port", seed), err) + } + return nil +} diff --git a/commands_test.go b/commands_test.go index 6ef84a5..1d99a77 100644 --- a/commands_test.go +++ b/commands_test.go @@ -46,3 +46,68 @@ func TestAddChainCommands_Good_PersistentFlags(t *testing.T) { assert.NotNil(t, chainCmd.PersistentFlags().Lookup("seed")) assert.NotNil(t, chainCmd.PersistentFlags().Lookup("testnet")) } + +func TestValidateChainOptions_Good(t *testing.T) { + err := validateChainOptions("/tmp/lethean", "seed.example:36942") + require.NoError(t, err) +} + +func TestValidateChainOptions_Bad(t *testing.T) { + tests := []struct { + name string + dataDir string + seed string + want string + }{ + {name: "missing data dir", dataDir: "", seed: "seed.example:36942", want: "data dir is required"}, + {name: "missing seed", dataDir: "/tmp/lethean", seed: "", want: "seed is required"}, + {name: "malformed seed", dataDir: "/tmp/lethean", seed: "seed.example", want: "must be host:port"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateChainOptions(tt.dataDir, tt.seed) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.want) + }) + } +} + +func TestChainSyncCommand_BadMutuallyExclusiveFlags(t *testing.T) { + dataDir := t.TempDir() + seed := "seed.example:36942" + testnet := false + + cmd := newChainSyncCommand(&dataDir, &seed, &testnet) + cmd.SetArgs([]string{"--daemon", "--stop"}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot be combined") +} + +func TestChainSyncCommand_BadArgsRejected(t *testing.T) { + dataDir := t.TempDir() + seed := "seed.example:36942" + testnet := false + + cmd := newChainSyncCommand(&dataDir, &seed, &testnet) + cmd.SetArgs([]string{"extra"}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown command") +} + +func TestChainExplorerCommand_BadSeedRejected(t *testing.T) { + dataDir := t.TempDir() + seed := "bad-seed" + testnet := false + + cmd := newChainExplorerCommand(&dataDir, &seed, &testnet) + cmd.SetArgs(nil) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "must be host:port") +} diff --git a/explorer_command.go b/explorer_command.go index e6f9cca..164ae5b 100644 --- a/explorer_command.go +++ b/explorer_command.go @@ -34,6 +34,10 @@ func newChainExplorerCommand(dataDir, seed *string, testnet *bool) *cobra.Comman Use: "explorer", Short: "TUI block explorer", Long: "Interactive terminal block explorer with live sync status.", + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + return validateChainOptions(*dataDir, *seed) + }, RunE: func(cmd *cobra.Command, args []string) error { return runChainExplorer(*dataDir, *seed, *testnet) }, diff --git a/sync_command.go b/sync_command.go index 1a2e3ce..0dcaf26 100644 --- a/sync_command.go +++ b/sync_command.go @@ -42,6 +42,13 @@ func newChainSyncCommand(dataDir, seed *string, testnet *bool) *cobra.Command { Use: "sync", Short: "Headless P2P chain sync", Long: "Sync the blockchain from P2P peers without the TUI explorer.", + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + if daemon && stop { + return coreerr.E("newChainSyncCommand", "flags --daemon and --stop cannot be combined", nil) + } + return validateChainOptions(*dataDir, *seed) + }, RunE: func(cmd *cobra.Command, args []string) error { if stop { return stopChainSyncDaemon(*dataDir)