feat(08-07): wire repeatable bonfire flags and export client-issuing helpers

- bonfire.wrap registers a Repeatable Flag as a Cobra StringSlice so
  Input.Flags returns every repeated --name=value occurrence in order;
  scalar/bare flags are unaffected (D-19)
- wristband.IssueClientCredentials/RejectRedirectURI export the exact
  random-id/secret/hash and redirect-URI validation RFC 7591
  registration already uses, so the fonoteka:oauth-client operator
  command shares one hash/validation path with DCR (T-08-SECRET-TIMING)
This commit is contained in:
Jakub Zych
2026-09-23 21:56:00 +02:00
parent 7096a90235
commit 398353b135
3 changed files with 125 additions and 0 deletions

View File

@@ -300,6 +300,75 @@ func TestPhase8RedBonfireFlags(t *testing.T) {
}
}
// TestRepeatableFlagUnsetReturnsEmpty proves an unset Repeatable flag comes
// back as an empty/nil slice, not an error or a panic (D-19 GREEN
// companion to TestPhase8RedBonfireFlags).
func TestRepeatableFlagUnsetReturnsEmpty(t *testing.T) {
var buf bytes.Buffer
var got []string
root, err := NewRootIO("app", []Command{{
Name: "demo:unset",
Flags: []Flag{
{Name: "scope", Repeatable: true},
},
Run: func(ctx context.Context, in Input, out Output) error {
got = in.Flags("scope")
return nil
},
}}, strings.NewReader(""), &buf, &buf)
if err != nil {
t.Fatal(err)
}
root.SetArgs([]string{"demo:unset"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("unset repeatable flag = %v, want empty", got)
}
}
// TestRepeatableFlagCoexistsWithBareAndScalar proves a Repeatable flag,
// a Bare flag and a scalar flag on the same command parse independently
// (D-19 acceptance criteria: existing scalar/bare callers stay unaffected).
func TestRepeatableFlagCoexistsWithBareAndScalar(t *testing.T) {
var buf bytes.Buffer
var gotRedirect []string
var gotList, gotListOK bool
var gotName string
root, err := NewRootIO("app", []Command{{
Name: "demo:mixed",
Flags: []Flag{
{Name: "redirect-uri", Repeatable: true},
{Name: "list", Bare: true},
{Name: "name", Default: ""},
},
Run: func(ctx context.Context, in Input, out Output) error {
gotRedirect = in.Flags("redirect-uri")
listVal, ok := in.Flag("list")
gotList, gotListOK = listVal == "true", ok
gotName, _ = in.Flag("name")
return nil
},
}}, strings.NewReader(""), &buf, &buf)
if err != nil {
t.Fatal(err)
}
root.SetArgs([]string{"demo:mixed", "--redirect-uri=https://x.example", "--list", "--name=Catalog"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if !slices.Equal(gotRedirect, []string{"https://x.example"}) {
t.Fatalf("redirect-uri = %v", gotRedirect)
}
if !gotListOK || !gotList {
t.Fatalf("list = (%v, %v), want (true, true)", gotList, gotListOK)
}
if gotName != "Catalog" {
t.Fatalf("name = %q", gotName)
}
}
func TestHelpUsesSharedAdapter(t *testing.T) {
var buf bytes.Buffer
root, err := NewRoot("hello", []Command{{