Skip to content

Commit 7478657

Browse files
authored
fix(cli): wrap fs.Parse with reorderFlagsBeforePositional in remaining commands (#25)
Seven CLI subcommands called fs.Parse(args) directly instead of going through reorderFlagsBeforePositional, so any positional argument before a flag stopped flag parsing and silently fell through to flag defaults. For commands that take a --db flag this is a production hazard. Running sluice binding remove 2 --db /custom/path stopped at "2", missed --db, and operated on the default data/sluice.db instead of the requested path. The same shape applies to cred remove, policy remove, policy import, mcp remove, channel update, and channel remove. Wrap each fs.Parse call with reorderFlagsBeforePositional like the other sibling subcommands already do, and add a regression test per command that exercises the positional-before-flags ordering. Also fix the cred dispatcher's usage line to mention the "update" subcommand that was added in v0.8.0.
1 parent 32eaa23 commit 7478657

10 files changed

Lines changed: 240 additions & 8 deletions

File tree

cmd/sluice/binding.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ func handleBindingUpdate(args []string) error {
277277
func handleBindingRemove(args []string) error {
278278
fs := flag.NewFlagSet("binding remove", flag.ContinueOnError)
279279
dbPath := fs.String("db", "data/sluice.db", "path to SQLite database")
280-
if err := fs.Parse(args); err != nil {
280+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
281281
return err
282282
}
283283

cmd/sluice/binding_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,53 @@ func TestHandleBindingRemoveMissingArg(t *testing.T) {
582582
}
583583
}
584584

585+
// TestHandleBindingRemoveIDBeforeFlags is a regression for v0.8.0: the
586+
// positional id is allowed to appear before flags. The original
587+
// handleBindingRemove called fs.Parse(args) without
588+
// reorderFlagsBeforePositional, so an invocation like
589+
//
590+
// sluice binding remove 1 --db /custom/path
591+
//
592+
// stopped flag parsing at "1" and silently fell through to the default
593+
// "data/sluice.db", deleting the wrong row in production. The fix is the
594+
// same reorderFlagsBeforePositional wrapper used by every other binding
595+
// subcommand. This test guards against the regression by passing the id
596+
// BEFORE --db and asserting the correct DB was modified.
597+
func TestHandleBindingRemoveIDBeforeFlags(t *testing.T) {
598+
dbPath := setupBindingDB(t)
599+
600+
db, err := store.New(dbPath)
601+
if err != nil {
602+
t.Fatal(err)
603+
}
604+
if _, err := db.AddBinding("api.example.com", "mycred", store.BindingOpts{}); err != nil {
605+
t.Fatal(err)
606+
}
607+
_ = db.Close()
608+
609+
_ = captureStdout(t, func() {
610+
if err := handleBindingCommand([]string{
611+
"remove", "1", "--db", dbPath,
612+
}); err != nil {
613+
t.Fatalf("remove with id-before-flags: %v", err)
614+
}
615+
})
616+
617+
db, err = store.New(dbPath)
618+
if err != nil {
619+
t.Fatal(err)
620+
}
621+
defer func() { _ = db.Close() }()
622+
623+
bindings, err := db.ListBindings()
624+
if err != nil {
625+
t.Fatal(err)
626+
}
627+
if len(bindings) != 0 {
628+
t.Errorf("expected 0 bindings after remove with id-before-flags, got %d", len(bindings))
629+
}
630+
}
631+
585632
// TestHandleBindingAddWithEnvVar verifies that --env-var is stored on the
586633
// new binding when add is invoked.
587634
func TestHandleBindingAddWithEnvVar(t *testing.T) {

cmd/sluice/channel.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func handleChannelUpdate(args []string) error {
121121
enabled := fs.String("enabled", "", "set enabled state (true or false)")
122122
url := fs.String("url", "", "update webhook URL")
123123
secret := fs.String("secret", "", "update webhook HMAC secret")
124-
if err := fs.Parse(args); err != nil {
124+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
125125
return err
126126
}
127127

@@ -174,7 +174,7 @@ func handleChannelUpdate(args []string) error {
174174
func handleChannelRemove(args []string) error {
175175
fs := flag.NewFlagSet("channel remove", flag.ContinueOnError)
176176
dbPath := fs.String("db", "data/sluice.db", "path to SQLite database")
177-
if err := fs.Parse(args); err != nil {
177+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
178178
return err
179179
}
180180

cmd/sluice/channel_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,49 @@ func TestHandleChannelRemoveNonExistent(t *testing.T) {
413413
}
414414
}
415415

416+
// TestHandleChannelUpdateIDBeforeFlags is a regression for the v0.8.0
417+
// flag ordering bug: handleChannelUpdate called fs.Parse(args) directly,
418+
// so passing the channel id before --db caused the parser to stop and
419+
// silently fall through to the default "data/sluice.db", updating the
420+
// wrong channel. The fix wraps args with reorderFlagsBeforePositional.
421+
func TestHandleChannelUpdateIDBeforeFlags(t *testing.T) {
422+
dbPath := setupChannelDB(t)
423+
424+
_ = captureChannelOutput(t, func() {
425+
if err := handleChannelAdd([]string{
426+
"--db", dbPath, "--type", "http", "--url", "https://example.com/hook",
427+
}); err != nil {
428+
t.Fatalf("add: %v", err)
429+
}
430+
})
431+
432+
_ = captureChannelOutput(t, func() {
433+
if err := handleChannelUpdate([]string{"2", "--db", dbPath, "--enabled", "false"}); err != nil {
434+
t.Fatalf("channel update with id-before-flags: %v", err)
435+
}
436+
})
437+
}
438+
439+
// TestHandleChannelRemoveIDBeforeFlags is a regression for the same v0.8.0
440+
// flag ordering bug, applied to channel remove.
441+
func TestHandleChannelRemoveIDBeforeFlags(t *testing.T) {
442+
dbPath := setupChannelDB(t)
443+
444+
_ = captureChannelOutput(t, func() {
445+
if err := handleChannelAdd([]string{
446+
"--db", dbPath, "--type", "http", "--url", "https://example.com/hook",
447+
}); err != nil {
448+
t.Fatalf("add: %v", err)
449+
}
450+
})
451+
452+
_ = captureChannelOutput(t, func() {
453+
if err := handleChannelRemove([]string{"2", "--db", dbPath}); err != nil {
454+
t.Fatalf("channel remove with id-before-flags: %v", err)
455+
}
456+
})
457+
}
458+
416459
func TestChannelTypeName(t *testing.T) {
417460
if got := channelTypeName(int(channel.ChannelTelegram)); got != "telegram" {
418461
t.Errorf("telegram: got %q", got)

cmd/sluice/cred.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
func handleCredCommand(args []string) error {
2121
if len(args) == 0 {
22-
return fmt.Errorf("usage: sluice cred [add|list|remove]")
22+
return fmt.Errorf("usage: sluice cred [add|list|update|remove]")
2323
}
2424

2525
switch args[0] {
@@ -544,7 +544,7 @@ func handleCredList(args []string) error {
544544
func handleCredRemove(args []string) error {
545545
fs := flag.NewFlagSet("cred remove", flag.ContinueOnError)
546546
dbPath := fs.String("db", "data/sluice.db", "path to SQLite database")
547-
if err := fs.Parse(args); err != nil {
547+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
548548
return err
549549
}
550550

cmd/sluice/cred_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,43 @@ func TestHandleCredRemove(t *testing.T) {
275275
}
276276
}
277277

278+
// TestHandleCredRemoveNameBeforeFlags is a regression for the v0.8.0 flag
279+
// ordering bug: handleCredRemove called fs.Parse(args) directly, so an
280+
// invocation like
281+
//
282+
// sluice cred remove mycred --db /custom/path
283+
//
284+
// stopped flag parsing at "mycred" and silently fell through to the default
285+
// "data/sluice.db", removing the wrong credential. The fix wraps args with
286+
// reorderFlagsBeforePositional like every other CLI subcommand. This test
287+
// guards against the regression by passing the name BEFORE --db.
288+
func TestHandleCredRemoveNameBeforeFlags(t *testing.T) {
289+
dir := t.TempDir()
290+
dbPath := setupVaultDB(t, dir)
291+
292+
vs, err := vault.NewStore(dir)
293+
if err != nil {
294+
t.Fatal(err)
295+
}
296+
if _, err := vs.Add("to_remove", "secret"); err != nil {
297+
t.Fatal(err)
298+
}
299+
300+
if err := handleCredCommand([]string{"remove", "to_remove", "--db", dbPath}); err != nil {
301+
t.Fatalf("cred remove with name-before-flags: %v", err)
302+
}
303+
304+
names, err := vs.List()
305+
if err != nil {
306+
t.Fatal(err)
307+
}
308+
for _, n := range names {
309+
if n == "to_remove" {
310+
t.Error("credential should have been removed via name-before-flags ordering")
311+
}
312+
}
313+
}
314+
278315
// TestHandleCredListEmpty tests listing when no credentials exist.
279316
func TestHandleCredListEmpty(t *testing.T) {
280317
dir := t.TempDir()

cmd/sluice/mcp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ func handleMCPList(args []string) error {
392392
func handleMCPRemove(args []string) error {
393393
fs := flag.NewFlagSet("mcp remove", flag.ContinueOnError)
394394
dbPath := fs.String("db", "data/sluice.db", "path to SQLite database")
395-
if err := fs.Parse(args); err != nil {
395+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
396396
return err
397397
}
398398

cmd/sluice/mcp_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,42 @@ func TestHandleMCPRemove(t *testing.T) {
414414
}
415415
}
416416

417+
// TestHandleMCPRemoveNameBeforeFlags is a regression for the v0.8.0 flag
418+
// ordering bug: handleMCPRemove called fs.Parse(args) directly, so the
419+
// positional name appearing before --db caused the parser to stop and
420+
// silently fall through to the default "data/sluice.db", removing the
421+
// wrong upstream.
422+
func TestHandleMCPRemoveNameBeforeFlags(t *testing.T) {
423+
dir := t.TempDir()
424+
dbPath := filepath.Join(dir, "test.db")
425+
426+
if err := handleMCPAdd([]string{
427+
"--db", dbPath,
428+
"--command", "server",
429+
"myserver",
430+
}); err != nil {
431+
t.Fatal(err)
432+
}
433+
434+
if err := handleMCPRemove([]string{"myserver", "--db", dbPath}); err != nil {
435+
t.Fatalf("mcp remove with name-before-flags: %v", err)
436+
}
437+
438+
db, err := store.New(dbPath)
439+
if err != nil {
440+
t.Fatal(err)
441+
}
442+
defer func() { _ = db.Close() }()
443+
444+
upstreams, err := db.ListMCPUpstreams()
445+
if err != nil {
446+
t.Fatal(err)
447+
}
448+
if len(upstreams) != 0 {
449+
t.Errorf("expected 0 upstreams after name-before-flags removal, got %d", len(upstreams))
450+
}
451+
}
452+
417453
// TestHandleMCPList verifies the list subcommand output.
418454
func TestHandleMCPList(t *testing.T) {
419455
dir := t.TempDir()

cmd/sluice/policy.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func handlePolicyAdd(args []string) error {
136136
func handlePolicyRemove(args []string) error {
137137
fs := flag.NewFlagSet("policy remove", flag.ContinueOnError)
138138
dbPath := fs.String("db", "data/sluice.db", "path to SQLite database")
139-
if err := fs.Parse(args); err != nil {
139+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
140140
return err
141141
}
142142

@@ -169,7 +169,7 @@ func handlePolicyRemove(args []string) error {
169169
func handlePolicyImport(args []string) error {
170170
fs := flag.NewFlagSet("policy import", flag.ContinueOnError)
171171
dbPath := fs.String("db", "data/sluice.db", "path to SQLite database")
172-
if err := fs.Parse(args); err != nil {
172+
if err := fs.Parse(reorderFlagsBeforePositional(args, fs)); err != nil {
173173
return err
174174
}
175175

cmd/sluice/policy_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,41 @@ func TestHandlePolicyRemoveNoArgs(t *testing.T) {
374374
}
375375
}
376376

377+
// TestHandlePolicyRemoveIDBeforeFlags is a regression for the v0.8.0 flag
378+
// ordering bug: handlePolicyRemove called fs.Parse(args) directly, so
379+
// passing the id before --db caused the parser to stop and silently fall
380+
// through to the default "data/sluice.db", removing the wrong rule. The fix
381+
// wraps args with reorderFlagsBeforePositional. This test guards against
382+
// the regression.
383+
func TestHandlePolicyRemoveIDBeforeFlags(t *testing.T) {
384+
dir := t.TempDir()
385+
dbPath := filepath.Join(dir, "test.db")
386+
387+
db, err := store.New(dbPath)
388+
if err != nil {
389+
t.Fatal(err)
390+
}
391+
if _, err := db.AddRule("allow", store.RuleOpts{Destination: "example.com"}); err != nil {
392+
t.Fatal(err)
393+
}
394+
_ = db.Close()
395+
396+
if err := handlePolicyRemove([]string{"1", "--db", dbPath}); err != nil {
397+
t.Fatalf("policy remove with id-before-flags: %v", err)
398+
}
399+
400+
db, err = store.New(dbPath)
401+
if err != nil {
402+
t.Fatal(err)
403+
}
404+
defer func() { _ = db.Close() }()
405+
406+
rules, _ := db.ListRules(store.RuleFilter{})
407+
if len(rules) != 0 {
408+
t.Errorf("expected 0 rules after id-before-flags removal, got %d", len(rules))
409+
}
410+
}
411+
377412
// --- handlePolicyImport tests ---
378413

379414
func TestHandlePolicyImportValid(t *testing.T) {
@@ -454,6 +489,40 @@ func TestHandlePolicyImportNoArgs(t *testing.T) {
454489
}
455490
}
456491

492+
// TestHandlePolicyImportPathBeforeFlags is a regression for the v0.8.0 flag
493+
// ordering bug: handlePolicyImport called fs.Parse(args) directly, so
494+
// passing the TOML path before --db caused the import to silently fall
495+
// through to the default "data/sluice.db". The fix wraps args with
496+
// reorderFlagsBeforePositional.
497+
func TestHandlePolicyImportPathBeforeFlags(t *testing.T) {
498+
dir := t.TempDir()
499+
dbPath := filepath.Join(dir, "test.db")
500+
tomlPath := filepath.Join(dir, "config.toml")
501+
502+
tomlData := `[[allow]]
503+
destination = "api.example.com"
504+
ports = [443]
505+
`
506+
if err := os.WriteFile(tomlPath, []byte(tomlData), 0o644); err != nil {
507+
t.Fatal(err)
508+
}
509+
510+
if err := handlePolicyImport([]string{tomlPath, "--db", dbPath}); err != nil {
511+
t.Fatalf("policy import with path-before-flags: %v", err)
512+
}
513+
514+
db, err := store.New(dbPath)
515+
if err != nil {
516+
t.Fatal(err)
517+
}
518+
defer func() { _ = db.Close() }()
519+
520+
rules, _ := db.ListRules(store.RuleFilter{})
521+
if len(rules) != 1 {
522+
t.Errorf("expected 1 rule imported into custom db, got %d", len(rules))
523+
}
524+
}
525+
457526
// --- handlePolicyExport tests ---
458527

459528
func TestHandlePolicyExportMatchesStore(t *testing.T) {

0 commit comments

Comments
 (0)