package cli import ( "bytes" "errors" "os" "slices" "strings" "testing" ) func TestRunSetupParsesTypedFieldsWithDefaultsAndNormalization(t *testing.T) { stdin := setupTestInputFile(t, strings.Join([]string{ " https://api.example.com ", "", "", "Read, WRITE, admin", " eu-west ", }, "\n")+"\n") var stdout bytes.Buffer result, err := RunSetup(SetupOptions{ Stdin: stdin, Stdout: &stdout, Fields: []SetupField{ { Name: "base_url", Label: "Base URL", Type: SetupFieldURL, Required: true, }, { Name: "api_token", Label: "API token", Type: SetupFieldSecret, Required: true, ExistingSecret: "stored-token", }, { Name: "enabled", Label: "Enabled", Type: SetupFieldBool, Default: "true", }, { Name: "scopes", Label: "Scopes", Type: SetupFieldList, Default: "read,write", Normalize: func(raw string) string { return strings.ToLower(strings.TrimSpace(raw)) }, }, { Name: "region", Label: "Region", Type: SetupFieldString, Default: "eu-central", Normalize: strings.TrimSpace, }, }, }) if err != nil { t.Fatalf("RunSetup returned error: %v", err) } baseURL, ok := result.Get("base_url") if !ok { t.Fatalf("missing base_url field") } if !baseURL.Set || baseURL.String != "https://api.example.com" { t.Fatalf("base_url = %#v, want normalized URL", baseURL) } token, ok := result.Get("api_token") if !ok { t.Fatalf("missing api_token field") } if token.String != "stored-token" { t.Fatalf("token.String = %q, want stored-token", token.String) } if !token.KeptStoredSecret { t.Fatalf("token should keep stored secret when blank") } enabled, ok := result.Get("enabled") if !ok { t.Fatalf("missing enabled field") } if !enabled.Bool || !enabled.Set { t.Fatalf("enabled = %#v, want true from default", enabled) } scopes, ok := result.Get("scopes") if !ok { t.Fatalf("missing scopes field") } wantScopes := []string{"read", "write", "admin"} if !slices.Equal(scopes.List, wantScopes) { t.Fatalf("scopes.List = %v, want %v", scopes.List, wantScopes) } region, ok := result.Get("region") if !ok { t.Fatalf("missing region field") } if region.String != "eu-west" { t.Fatalf("region.String = %q, want eu-west", region.String) } promptOutput := stdout.String() for _, needle := range []string{ "Base URL:", "API token [stored, leave blank to keep]:", "Enabled [Y/n]:", "Scopes [read,write]:", "Region [eu-central]:", } { if !strings.Contains(promptOutput, needle) { t.Fatalf("prompt output = %q, want substring %q", promptOutput, needle) } } } func TestRunSetupReturnsReadableValidationErrorInNonInteractiveMode(t *testing.T) { stdin := setupTestInputFile(t, "maybe\n") var stdout bytes.Buffer _, err := RunSetup(SetupOptions{ Stdin: stdin, Stdout: &stdout, Fields: []SetupField{ { Name: "enabled", Label: "Enabled", Type: SetupFieldBool, }, }, }) var validationErr *SetupValidationError if !errors.As(err, &validationErr) { t.Fatalf("RunSetup error = %v, want SetupValidationError", err) } if validationErr.Field != "enabled" { t.Fatalf("validationErr.Field = %q, want enabled", validationErr.Field) } if !strings.Contains(validationErr.Error(), "must be one of") { t.Fatalf("validationErr = %v, want readable bool error", validationErr) } } func TestRunSetupSupportsValidationHooks(t *testing.T) { stdin := setupTestInputFile(t, "https://example.com\nalpha,beta\n") _, err := RunSetup(SetupOptions{ Stdin: stdin, Fields: []SetupField{ { Name: "base_url", Label: "Base URL", Type: SetupFieldURL, Validate: func(value string) error { if !strings.HasSuffix(value, "/v1") { return errors.New("must end with /v1") } return nil }, }, { Name: "scopes", Type: SetupFieldList, ValidateList: func(values []string) error { if len(values) < 3 { return errors.New("at least 3 scopes are required") } return nil }, }, }, }) var validationErr *SetupValidationError if !errors.As(err, &validationErr) { t.Fatalf("RunSetup error = %v, want SetupValidationError", err) } if validationErr.Field != "base_url" { t.Fatalf("validationErr.Field = %q, want base_url", validationErr.Field) } if !strings.Contains(validationErr.Error(), "must end with /v1") { t.Fatalf("validationErr = %v", validationErr) } } func TestRunSetupRejectsInvalidDefinitions(t *testing.T) { tests := []struct { name string fields []SetupField }{ { name: "empty field name", fields: []SetupField{ {Name: " ", Type: SetupFieldString}, }, }, { name: "duplicate field names", fields: []SetupField{ {Name: "base_url", Type: SetupFieldString}, {Name: "base_url", Type: SetupFieldURL}, }, }, { name: "unknown type", fields: []SetupField{ {Name: "base_url", Type: SetupFieldType("json")}, }, }, { name: "invalid bool default", fields: []SetupField{ {Name: "enabled", Type: SetupFieldBool, Default: "sometimes"}, }, }, { name: "invalid url default", fields: []SetupField{ {Name: "base_url", Type: SetupFieldURL, Default: "localhost"}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := RunSetup(SetupOptions{ Stdin: setupTestInputFile(t, ""), Fields: tc.fields, }) if !errors.Is(err, ErrInvalidSetupDefinition) { t.Fatalf("RunSetup error = %v, want ErrInvalidSetupDefinition", err) } }) } } func setupTestInputFile(t *testing.T, content string) *os.File { t.Helper() file, err := os.CreateTemp(t.TempDir(), "setup-input-*.txt") if err != nil { t.Fatalf("CreateTemp returned error: %v", err) } t.Cleanup(func() { _ = file.Close() }) if _, err := file.WriteString(content); err != nil { t.Fatalf("WriteString returned error: %v", err) } if _, err := file.Seek(0, 0); err != nil { t.Fatalf("Seek returned error: %v", err) } return file }