266 lines
6 KiB
Go
266 lines
6 KiB
Go
|
|
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
|
||
|
|
}
|