diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8cfb152..00ae01e 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -30,7 +30,7 @@ universal_binaries: - replace: true archives: - - format: tar.gz + - formats: ["tar.gz"] # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ @@ -42,7 +42,7 @@ archives: # use zip for windows archives format_overrides: - goos: windows - format: zip + formats: ["zip"] report_sizes: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73ece76..d7494d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ 2. Install goreleaser ``` -brew install goreleaser/tap/goreleaser +brew install --cask goreleaser/tap/goreleaser ``` 3. Build the CLI diff --git a/go.mod b/go.mod index d871e2b..31e2e70 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/pinecone-io/go-pinecone/v4 v4.1.4 github.com/rs/zerolog v1.32.0 github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 golang.org/x/oauth2 v0.30.0 golang.org/x/term v0.33.0 @@ -49,7 +50,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect diff --git a/internal/pkg/cli/command/index/configure.go b/internal/pkg/cli/command/index/configure.go index d148486..c06df3c 100644 --- a/internal/pkg/cli/command/index/configure.go +++ b/internal/pkg/cli/command/index/configure.go @@ -4,6 +4,7 @@ import ( "context" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/index" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" @@ -19,29 +20,28 @@ type configureIndexOptions struct { podType string replicas int32 deletionProtection string - - json bool + json bool } func NewConfigureIndexCmd() *cobra.Command { options := configureIndexOptions{} cmd := &cobra.Command{ - Use: "configure", + Use: "configure ", Short: "Configure an existing index with the specified configuration", Example: "", + Args: index.ValidateIndexNameArgs, Run: func(cmd *cobra.Command, args []string) { + options.name = args[0] runConfigureIndexCmd(options) }, } - // Required flags - cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to configure") - // Optional flags cmd.Flags().StringVarP(&options.podType, "pod_type", "t", "", "type of pod to use, can only upgrade when configuring") cmd.Flags().Int32VarP(&options.replicas, "replicas", "r", 0, "replicas of the index to configure") cmd.Flags().StringVarP(&options.deletionProtection, "deletion_protection", "p", "", "enable or disable deletion protection for the index") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") return cmd } @@ -59,13 +59,14 @@ func runConfigureIndexCmd(options configureIndexOptions) { msg.FailMsg("Failed to configure index %s: %+v\n", style.Emphasis(options.name), err) exit.Error(err) } + if options.json { json := text.IndentJSON(idx) pcio.Println(json) return } - describeCommand := pcio.Sprintf("pc index describe --name %s", idx.Name) + describeCommand := pcio.Sprintf("pc index describe %s", idx.Name) msg.SuccessMsg("Index %s configured successfully. Run %s to check status. \n\n", style.Emphasis(idx.Name), style.Code(describeCommand)) presenters.PrintDescribeIndexTable(idx) } diff --git a/internal/pkg/cli/command/index/configure_test.go b/internal/pkg/cli/command/index/configure_test.go new file mode 100644 index 0000000..31e05f4 --- /dev/null +++ b/internal/pkg/cli/command/index/configure_test.go @@ -0,0 +1,82 @@ +package index + +import ( + "testing" + + "github.com/pinecone-io/cli/internal/pkg/utils/testutils" +) + +func TestConfigureCmd_ArgsValidation(t *testing.T) { + cmd := NewConfigureIndexCmd() + + // Get preset index name validation tests + tests := testutils.GetIndexNameValidationTests() + + // Add custom tests for this command (configure-specific flags) + customTests := []testutils.CommandTestConfig{ + { + Name: "valid - positional arg with --json flag", + Args: []string{"my-index"}, + Flags: map[string]string{"json": "true"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --json=false", + Args: []string{"my-index"}, + Flags: map[string]string{"json": "false"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --pod_type flag", + Args: []string{"my-index"}, + Flags: map[string]string{"pod_type": "p1.x1"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --replicas flag", + Args: []string{"my-index"}, + Flags: map[string]string{"replicas": "2"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --deletion_protection flag", + Args: []string{"my-index"}, + Flags: map[string]string{"deletion_protection": "enabled"}, + ExpectError: false, + }, + { + Name: "error - no arguments but with --json flag", + Args: []string{}, + Flags: map[string]string{"json": "true"}, + ExpectError: true, + ErrorSubstr: "please provide an index name", + }, + { + Name: "error - multiple arguments with --json flag", + Args: []string{"index1", "index2"}, + Flags: map[string]string{"json": "true"}, + ExpectError: true, + ErrorSubstr: "please provide only one index name", + }, + } + + // Combine preset tests with custom tests + allTests := append(tests, customTests...) + + // Use the generic test utility + testutils.TestCommandArgsAndFlags(t, cmd, allTests) +} + +func TestConfigureCmd_Flags(t *testing.T) { + cmd := NewConfigureIndexCmd() + + // Test that the command has a properly configured --json flag + testutils.AssertJSONFlag(t, cmd) +} + +func TestConfigureCmd_Usage(t *testing.T) { + cmd := NewConfigureIndexCmd() + + // Test that the command has proper usage metadata + testutils.AssertCommandUsage(t, cmd, "configure ", "index") +} diff --git a/internal/pkg/cli/command/index/create.go b/internal/pkg/cli/command/index/create.go index 1f4d67b..600c45b 100644 --- a/internal/pkg/cli/command/index/create.go +++ b/internal/pkg/cli/command/index/create.go @@ -6,6 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/pinecone-io/cli/internal/pkg/utils/docslinks" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/index" "github.com/pinecone-io/cli/internal/pkg/utils/log" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" @@ -65,7 +66,7 @@ func NewCreateIndexCmd() *cobra.Command { options := createIndexOptions{} cmd := &cobra.Command{ - Use: "create", + Use: "create ", Short: "Create a new index with the specified configuration", Long: heredoc.Docf(` The %s command creates a new index with the specified configuration. There are several different types of indexes @@ -80,23 +81,21 @@ func NewCreateIndexCmd() *cobra.Command { `, style.Code("pc index create"), style.URL(docslinks.DocsIndexCreate)), Example: heredoc.Doc(` # create a serverless index - $ pc index create --name my-index --dimension 1536 --metric cosine --cloud aws --region us-east-1 + $ pc index create my-index --dimension 1536 --metric cosine --cloud aws --region us-east-1 # create a pod index - $ pc index create --name my-index --dimension 1536 --metric cosine --environment us-east-1-aws --pod-type p1.x1 --shards 2 --replicas 2 + $ pc index create my-index --dimension 1536 --metric cosine --environment us-east-1-aws --pod-type p1.x1 --shards 2 --replicas 2 # create an integrated index - $ pc index create --name my-index --dimension 1536 --metric cosine --cloud aws --region us-east-1 --model multilingual-e5-large --field_map text=chunk_text + $ pc index create my-index --dimension 1536 --metric cosine --cloud aws --region us-east-1 --model multilingual-e5-large --field_map text=chunk_text `), + Args: index.ValidateIndexNameArgs, Run: func(cmd *cobra.Command, args []string) { + options.name = args[0] runCreateIndexCmd(options) }, } - // Required flags - cmd.Flags().StringVarP(&options.name, "name", "n", "", "Name of index to create") - _ = cmd.MarkFlagRequired("name") - // Serverless & Pods cmd.Flags().StringVar(&options.sourceCollection, "source_collection", "", "When creating an index from a collection") @@ -243,7 +242,7 @@ func renderSuccessOutput(idx *pinecone.Index, options createIndexOptions) { return } - describeCommand := pcio.Sprintf("pc index describe --name %s", idx.Name) + describeCommand := pcio.Sprintf("pc index describe %s", idx.Name) msg.SuccessMsg("Index %s created successfully. Run %s to check status. \n\n", style.Emphasis(idx.Name), style.Code(describeCommand)) presenters.PrintDescribeIndexTable(idx) } diff --git a/internal/pkg/cli/command/index/create_test.go b/internal/pkg/cli/command/index/create_test.go new file mode 100644 index 0000000..7b45a14 --- /dev/null +++ b/internal/pkg/cli/command/index/create_test.go @@ -0,0 +1,100 @@ +package index + +import ( + "testing" + + "github.com/pinecone-io/cli/internal/pkg/utils/testutils" +) + +func TestCreateCmd_ArgsValidation(t *testing.T) { + cmd := NewCreateIndexCmd() + + // Get preset index name validation tests + tests := testutils.GetIndexNameValidationTests() + + // Add custom tests for this command (create-specific flags) + customTests := []testutils.CommandTestConfig{ + { + Name: "valid - positional arg with --json flag", + Args: []string{"my-index"}, + Flags: map[string]string{"json": "true"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --json=false", + Args: []string{"my-index"}, + Flags: map[string]string{"json": "false"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --dimension flag", + Args: []string{"my-index"}, + Flags: map[string]string{"dimension": "1536"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --metric flag", + Args: []string{"my-index"}, + Flags: map[string]string{"metric": "cosine"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --cloud and --region flags", + Args: []string{"my-index"}, + Flags: map[string]string{"cloud": "aws", "region": "us-east-1"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --environment flag", + Args: []string{"my-index"}, + Flags: map[string]string{"environment": "us-east-1-aws"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --pod_type flag", + Args: []string{"my-index"}, + Flags: map[string]string{"pod_type": "p1.x1"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --model flag", + Args: []string{"my-index"}, + Flags: map[string]string{"model": "multilingual-e5-large"}, + ExpectError: false, + }, + { + Name: "error - no arguments but with --json flag", + Args: []string{}, + Flags: map[string]string{"json": "true"}, + ExpectError: true, + ErrorSubstr: "please provide an index name", + }, + { + Name: "error - multiple arguments with --json flag", + Args: []string{"index1", "index2"}, + Flags: map[string]string{"json": "true"}, + ExpectError: true, + ErrorSubstr: "please provide only one index name", + }, + } + + // Combine preset tests with custom tests + allTests := append(tests, customTests...) + + // Use the generic test utility + testutils.TestCommandArgsAndFlags(t, cmd, allTests) +} + +func TestCreateCmd_Flags(t *testing.T) { + cmd := NewCreateIndexCmd() + + // Test that the command has a properly configured --json flag + testutils.AssertJSONFlag(t, cmd) +} + +func TestCreateCmd_Usage(t *testing.T) { + cmd := NewCreateIndexCmd() + + // Test that the command has proper usage metadata + testutils.AssertCommandUsage(t, cmd, "create ", "index") +} diff --git a/internal/pkg/cli/command/index/delete.go b/internal/pkg/cli/command/index/delete.go index 65910fe..243ce85 100644 --- a/internal/pkg/cli/command/index/delete.go +++ b/internal/pkg/cli/command/index/delete.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/index" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" "github.com/pinecone-io/cli/internal/pkg/utils/style" @@ -19,9 +20,11 @@ func NewDeleteCmd() *cobra.Command { options := DeleteCmdOptions{} cmd := &cobra.Command{ - Use: "delete", + Use: "delete ", Short: "Delete an index", + Args: index.ValidateIndexNameArgs, Run: func(cmd *cobra.Command, args []string) { + options.name = args[0] ctx := context.Background() pc := sdk.NewPineconeClient() @@ -39,9 +42,5 @@ func NewDeleteCmd() *cobra.Command { }, } - // required flags - cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to delete") - _ = cmd.MarkFlagRequired("name") - return cmd } diff --git a/internal/pkg/cli/command/index/delete_test.go b/internal/pkg/cli/command/index/delete_test.go new file mode 100644 index 0000000..9eedecb --- /dev/null +++ b/internal/pkg/cli/command/index/delete_test.go @@ -0,0 +1,24 @@ +package index + +import ( + "testing" + + "github.com/pinecone-io/cli/internal/pkg/utils/testutils" +) + +func TestDeleteCmd_ArgsValidation(t *testing.T) { + cmd := NewDeleteCmd() + + // Get preset index name validation tests + tests := testutils.GetIndexNameValidationTests() + + // Use the generic test utility + testutils.TestCommandArgsAndFlags(t, cmd, tests) +} + +func TestDeleteCmd_Usage(t *testing.T) { + cmd := NewDeleteCmd() + + // Test that the command has proper usage metadata + testutils.AssertCommandUsage(t, cmd, "delete ", "index") +} diff --git a/internal/pkg/cli/command/index/describe.go b/internal/pkg/cli/command/index/describe.go index 8278b17..1765f66 100644 --- a/internal/pkg/cli/command/index/describe.go +++ b/internal/pkg/cli/command/index/describe.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/index" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" @@ -22,9 +23,11 @@ func NewDescribeCmd() *cobra.Command { options := DescribeCmdOptions{} cmd := &cobra.Command{ - Use: "describe", + Use: "describe ", Short: "Get configuration and status information for an index", + Args: index.ValidateIndexNameArgs, Run: func(cmd *cobra.Command, args []string) { + options.name = args[0] pc := sdk.NewPineconeClient() idx, err := pc.DescribeIndex(cmd.Context(), options.name) @@ -46,10 +49,6 @@ func NewDescribeCmd() *cobra.Command { }, } - // required flags - cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to describe") - _ = cmd.MarkFlagRequired("name") - // optional flags cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") diff --git a/internal/pkg/cli/command/index/describe_test.go b/internal/pkg/cli/command/index/describe_test.go new file mode 100644 index 0000000..1388ab0 --- /dev/null +++ b/internal/pkg/cli/command/index/describe_test.go @@ -0,0 +1,64 @@ +package index + +import ( + "testing" + + "github.com/pinecone-io/cli/internal/pkg/utils/testutils" +) + +func TestDescribeCmd_ArgsValidation(t *testing.T) { + cmd := NewDescribeCmd() + + // Get preset index name validation tests + tests := testutils.GetIndexNameValidationTests() + + // Add custom tests for this command (e.g., with --json flag) + customTests := []testutils.CommandTestConfig{ + { + Name: "valid - positional arg with --json flag", + Args: []string{"my-index"}, + Flags: map[string]string{"json": "true"}, + ExpectError: false, + }, + { + Name: "valid - positional arg with --json=false", + Args: []string{"my-index"}, + Flags: map[string]string{"json": "false"}, + ExpectError: false, + }, + { + Name: "error - no arguments but with --json flag", + Args: []string{}, + Flags: map[string]string{"json": "true"}, + ExpectError: true, + ErrorSubstr: "please provide an index name", + }, + { + Name: "error - multiple arguments with --json flag", + Args: []string{"index1", "index2"}, + Flags: map[string]string{"json": "true"}, + ExpectError: true, + ErrorSubstr: "please provide only one index name", + }, + } + + // Combine preset tests with custom tests + allTests := append(tests, customTests...) + + // Use the generic test utility + testutils.TestCommandArgsAndFlags(t, cmd, allTests) +} + +func TestDescribeCmd_Flags(t *testing.T) { + cmd := NewDescribeCmd() + + // Test that the command has a properly configured --json flag + testutils.AssertJSONFlag(t, cmd) +} + +func TestDescribeCmd_Usage(t *testing.T) { + cmd := NewDescribeCmd() + + // Test that the command has proper usage metadata + testutils.AssertCommandUsage(t, cmd, "describe ", "index") +} diff --git a/internal/pkg/utils/index/validation.go b/internal/pkg/utils/index/validation.go new file mode 100644 index 0000000..7ac7399 --- /dev/null +++ b/internal/pkg/utils/index/validation.go @@ -0,0 +1,23 @@ +package index + +import ( + "errors" + "strings" + + "github.com/spf13/cobra" +) + +// ValidateIndexNameArgs validates that exactly one non-empty index name is provided as a positional argument. +// This is the standard validation used across all index commands (create, describe, delete, configure). +func ValidateIndexNameArgs(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("please provide an index name") + } + if len(args) > 1 { + return errors.New("please provide only one index name") + } + if strings.TrimSpace(args[0]) == "" { + return errors.New("index name cannot be empty") + } + return nil +} diff --git a/internal/pkg/utils/testutils/README.md b/internal/pkg/utils/testutils/README.md new file mode 100644 index 0000000..049ffba --- /dev/null +++ b/internal/pkg/utils/testutils/README.md @@ -0,0 +1,228 @@ +# Test Utilities + +This package provides reusable test utilities for testing CLI commands, particularly for common patterns like the `--json` flag and argument validation. + +## File Organization + +- `testutils.go` - Complex testing utilities (`TestCommandArgsAndFlags`) +- `assertions.go` - Simple assertion utilities (`AssertCommandUsage`, `AssertJSONFlag`) +- `index_validation.go` - Index name validation utilities (`GetIndexNameValidationTests`) + +## Generic Command Testing + +The most powerful utility is `TestCommandArgsAndFlags` which provides a generic way to test any command's argument validation and flag handling: + +```go +func TestMyCommand_ArgsValidation(t *testing.T) { + cmd := NewMyCommand() + + tests := []testutils.CommandTestConfig{ + { + Name: "valid - single argument with flag", + Args: []string{"my-arg"}, + Flags: map[string]string{"json": "true"}, + ExpectError: false, + ExpectedArgs: []string{"my-arg"}, + ExpectedFlags: map[string]interface{}{ + "json": true, + }, + }, + { + Name: "error - no arguments", + Args: []string{}, + Flags: map[string]string{}, + ExpectError: true, + ErrorSubstr: "please provide an argument", + }, + } + + testutils.TestCommandArgsAndFlags(t, cmd, tests) +} +``` + +## JSON Flag Testing + +The `--json` flag is used across many commands in the CLI. The JSON utility answers one simple question: **"Does my command have a properly configured `--json` flag?"** + +### JSON Flag Test + +```go +func TestMyCommand_Flags(t *testing.T) { + cmd := NewMyCommand() + + // Test that the command has a properly configured --json flag + testutils.AssertJSONFlag(t, cmd) +} +``` + +This single function verifies that the `--json` flag is: + +- Properly defined +- Boolean type +- Optional (not required) +- Has a description mentioning "json" +- Can be set to true/false + +## Command Usage Testing + +The `AssertCommandUsage` utility tests that a command has proper usage metadata: + +```go +func TestMyCommand_Usage(t *testing.T) { + cmd := NewMyCommand() + + // Test that the command has proper usage metadata + testutils.AssertCommandUsage(t, cmd, "my-command ", "domain") +} +``` + +This function verifies that the command has: + +- Correct usage string format +- Non-empty short description +- Description mentions the expected domain + +## Index Name Validation + +For commands that take an index name as a positional argument (like `describe`, `delete`, etc.), there are specialized utilities: + +### Index Name Validator + +**Basic approach (preset tests only):** + +```go +func TestMyIndexCommand_ArgsValidation(t *testing.T) { + cmd := NewMyIndexCommand() + + // Get preset index name validation tests + tests := testutils.GetIndexNameValidationTests() + + // Use the generic test utility + testutils.TestCommandArgsAndFlags(t, cmd, tests) +} +``` + +**Advanced approach (preset + custom tests):** + +```go +func TestMyIndexCommand_ArgsValidation(t *testing.T) { + cmd := NewMyIndexCommand() + + // Get preset index name validation tests + tests := testutils.GetIndexNameValidationTests() + + // Add custom tests for this specific command + customTests := []testutils.CommandTestConfig{ + { + Name: "valid - with custom flag", + Args: []string{"my-index"}, + Flags: map[string]string{"custom-flag": "value"}, + ExpectError: false, + }, + } + + // Combine preset tests with custom tests + allTests := append(tests, customTests...) + + // Use the generic test utility + testutils.TestCommandArgsAndFlags(t, cmd, allTests) +} +``` + +### Testing Flags Separately + +**For commands with --json flag:** + +```go +func TestMyIndexCommand_Flags(t *testing.T) { + cmd := NewMyIndexCommand() + + // Test the --json flag specifically + testutils.AssertJSONFlag(t, cmd) +} +``` + +**For commands with custom flags:** + +```go +func TestMyIndexCommand_Flags(t *testing.T) { + cmd := NewMyIndexCommand() + + // Test custom flags using the generic utility + tests := []testutils.CommandTestConfig{ + { + Name: "valid - with custom flag", + Args: []string{"my-index"}, + Flags: map[string]string{"custom-flag": "value"}, + ExpectError: false, + ExpectedArgs: []string{"my-index"}, + ExpectedFlags: map[string]interface{}{ + "custom-flag": "value", + }, + }, + } + + testutils.TestCommandArgsAndFlags(t, cmd, tests) +} +``` + +### Index Name Validator Function + +```go +func NewMyIndexCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "my-command ", + Short: "Description of my command", + Args: testutils.CreateIndexNameValidator(), // Reusable validator + Run: func(cmd *cobra.Command, args []string) { + // Command logic here + }, + } + return cmd +} +``` + +The index name validator handles: + +- Empty string validation +- Whitespace-only validation +- Multiple argument validation +- No argument validation + +## Available Functions + +### Generic Command Testing + +- `TestCommandArgsAndFlags(t, cmd, tests)` - Generic utility to test any command's argument validation and flag handling +- `CommandTestConfig` - Configuration struct for defining test cases +- `AssertCommandUsage(t, cmd, expectedUsage, expectedDomain)` - Tests that a command has proper usage metadata + +### Index Name Validation + +- `GetIndexNameValidationTests()` - Returns preset test cases for index name validation + +### JSON Flag Specific + +- `AssertJSONFlag(t, cmd)` - Verifies that the command has a properly configured `--json` flag (definition, type, optional, description, functionality) + +## Supported Flag Types + +The generic utility supports all common flag types: + +- `bool` - Boolean flags +- `string` - String flags +- `int`, `int64` - Integer flags +- `float64` - Float flags +- `stringSlice`, `intSlice` - Slice flags + +## Usage in Commands + +Any command that follows the standard cobra pattern can use these utilities. The generic utilities are particularly useful for commands with: + +- Positional arguments +- Multiple flags of different types +- Custom argument validation logic + +## Example + +See `internal/pkg/cli/command/index/describe_test.go` for a complete example of how to use these utilities. diff --git a/internal/pkg/utils/testutils/assertions.go b/internal/pkg/utils/testutils/assertions.go new file mode 100644 index 0000000..104dc98 --- /dev/null +++ b/internal/pkg/utils/testutils/assertions.go @@ -0,0 +1,91 @@ +package testutils + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" +) + +// AssertCommandUsage tests that a command has proper usage metadata +// This function can be reused by any command to test its usage information +func AssertCommandUsage(t *testing.T, cmd *cobra.Command, expectedUsage string, expectedDomain string) { + t.Helper() + + // Test usage string + if cmd.Use != expectedUsage { + t.Errorf("expected Use to be %q, got %q", expectedUsage, cmd.Use) + } + + // Test short description exists + if cmd.Short == "" { + t.Error("expected command to have a short description") + } + + // Test description mentions domain + if !strings.Contains(strings.ToLower(cmd.Short), expectedDomain) { + t.Errorf("expected short description to mention %q, got %q", expectedDomain, cmd.Short) + } +} + +// AssertJSONFlag tests the common --json flag pattern used across commands +// This function comprehensively tests flag definition, properties, and functionality +// This function can be reused by any command that has a --json flag +func AssertJSONFlag(t *testing.T, cmd *cobra.Command) { + t.Helper() + + // Test that the json flag is properly defined + jsonFlag := cmd.Flags().Lookup("json") + if jsonFlag == nil { + t.Error("expected --json flag to be defined") + return + } + + // Test that it's a boolean flag + if jsonFlag.Value.Type() != "bool" { + t.Errorf("expected --json flag to be bool type, got %s", jsonFlag.Value.Type()) + } + + // Test that the flag is optional (not required) + if jsonFlag.Annotations[cobra.BashCompOneRequiredFlag] != nil { + t.Error("expected --json flag to be optional") + } + + // Test that the flag has a description + if jsonFlag.Usage == "" { + t.Error("expected --json flag to have a usage description") + } + + // Test that the description mentions JSON + if !strings.Contains(strings.ToLower(jsonFlag.Usage), "json") { + t.Errorf("expected --json flag description to mention 'json', got %q", jsonFlag.Usage) + } + + // Test setting json flag to true + err := cmd.Flags().Set("json", "true") + if err != nil { + t.Errorf("failed to set --json flag to true: %v", err) + } + + jsonValue, err := cmd.Flags().GetBool("json") + if err != nil { + t.Errorf("failed to get --json flag value: %v", err) + } + if !jsonValue { + t.Error("expected --json flag to be true after setting it") + } + + // Test setting json flag to false + err = cmd.Flags().Set("json", "false") + if err != nil { + t.Errorf("failed to set --json flag to false: %v", err) + } + + jsonValue, err = cmd.Flags().GetBool("json") + if err != nil { + t.Errorf("failed to get --json flag value: %v", err) + } + if jsonValue { + t.Error("expected --json flag to be false after setting it") + } +} diff --git a/internal/pkg/utils/testutils/index_validation.go b/internal/pkg/utils/testutils/index_validation.go new file mode 100644 index 0000000..038c305 --- /dev/null +++ b/internal/pkg/utils/testutils/index_validation.go @@ -0,0 +1,62 @@ +package testutils + +// GetIndexNameValidationTests returns standard index name validation test cases +// These tests focus ONLY on index name validation, without any flag assumptions +// Flags should be tested separately using the generic TestCommandArgsAndFlags utility +func GetIndexNameValidationTests() []CommandTestConfig { + return []CommandTestConfig{ + { + Name: "valid - single index name", + Args: []string{"my-index"}, + Flags: map[string]string{}, + ExpectError: false, + }, + { + Name: "valid - index name with special characters", + Args: []string{"my-index-123"}, + Flags: map[string]string{}, + ExpectError: false, + }, + { + Name: "valid - index name with underscores", + Args: []string{"my_index_123"}, + Flags: map[string]string{}, + ExpectError: false, + }, + { + Name: "error - no arguments", + Args: []string{}, + Flags: map[string]string{}, + ExpectError: true, + ErrorSubstr: "please provide an index name", + }, + { + Name: "error - multiple arguments", + Args: []string{"index1", "index2"}, + Flags: map[string]string{}, + ExpectError: true, + ErrorSubstr: "please provide only one index name", + }, + { + Name: "error - three arguments", + Args: []string{"index1", "index2", "index3"}, + Flags: map[string]string{}, + ExpectError: true, + ErrorSubstr: "please provide only one index name", + }, + { + Name: "error - empty string argument", + Args: []string{""}, + Flags: map[string]string{}, + ExpectError: true, + ErrorSubstr: "index name cannot be empty", + }, + { + Name: "error - whitespace only argument", + Args: []string{" "}, + Flags: map[string]string{}, + ExpectError: true, + ErrorSubstr: "index name cannot be empty", + }, + } +} diff --git a/internal/pkg/utils/testutils/testutils.go b/internal/pkg/utils/testutils/testutils.go new file mode 100644 index 0000000..591c089 --- /dev/null +++ b/internal/pkg/utils/testutils/testutils.go @@ -0,0 +1,114 @@ +package testutils + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// CommandTestConfig represents the configuration for testing a command's arguments and flags +type CommandTestConfig struct { + Name string + Args []string + Flags map[string]string + ExpectError bool + ErrorSubstr string + ExpectedArgs []string // Expected positional arguments after processing + ExpectedFlags map[string]interface{} // Expected flag values after processing +} + +// TestCommandArgsAndFlags provides a generic way to test any command's argument validation and flag handling +// This can be used for any command that follows the standard cobra pattern +func TestCommandArgsAndFlags(t *testing.T, cmd *cobra.Command, tests []CommandTestConfig) { + t.Helper() + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + // Create a fresh command instance for each test + cmdCopy := *cmd + cmdCopy.Flags().SortFlags = false + + // Reset all flags to their default values + cmdCopy.Flags().VisitAll(func(flag *pflag.Flag) { + flag.Value.Set(flag.DefValue) + }) + + // Set flags if provided + for flagName, flagValue := range tt.Flags { + err := cmdCopy.Flags().Set(flagName, flagValue) + if err != nil { + t.Fatalf("failed to set flag %s=%s: %v", flagName, flagValue, err) + } + } + + // Test the Args validation function + err := cmdCopy.Args(&cmdCopy, tt.Args) + + if tt.ExpectError { + if err == nil { + t.Errorf("expected error but got nil") + } else if tt.ErrorSubstr != "" && !strings.Contains(err.Error(), tt.ErrorSubstr) { + t.Errorf("expected error to contain %q, got %q", tt.ErrorSubstr, err.Error()) + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // If validation passed, test that the command would be configured correctly + if len(tt.Args) > 0 && len(tt.ExpectedArgs) > 0 { + // Verify positional arguments + for i, expectedArg := range tt.ExpectedArgs { + if i < len(tt.Args) && tt.Args[i] != expectedArg { + t.Errorf("expected arg[%d] to be %q, got %q", i, expectedArg, tt.Args[i]) + } + } + } + + // Verify flag values + for flagName, expectedValue := range tt.ExpectedFlags { + flag := cmdCopy.Flags().Lookup(flagName) + if flag == nil { + t.Errorf("expected flag %s to exist", flagName) + continue + } + + // Get the actual flag value based on its type + actualValue, err := getFlagValue(&cmdCopy, flagName, flag.Value.Type()) + if err != nil { + t.Errorf("failed to get flag %s value: %v", flagName, err) + continue + } + + if actualValue != expectedValue { + t.Errorf("expected flag %s to be %v, got %v", flagName, expectedValue, actualValue) + } + } + } + }) + } +} + +// getFlagValue retrieves the value of a flag based on its type +func getFlagValue(cmd *cobra.Command, flagName, flagType string) (interface{}, error) { + switch flagType { + case "bool": + return cmd.Flags().GetBool(flagName) + case "string": + return cmd.Flags().GetString(flagName) + case "int": + return cmd.Flags().GetInt(flagName) + case "int64": + return cmd.Flags().GetInt64(flagName) + case "float64": + return cmd.Flags().GetFloat64(flagName) + case "stringSlice": + return cmd.Flags().GetStringSlice(flagName) + case "intSlice": + return cmd.Flags().GetIntSlice(flagName) + default: + return cmd.Flags().GetString(flagName) // fallback to string + } +}