diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index ee079fb3..e050dcb7 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "github.com/go-git/go-git/v5" @@ -166,15 +167,28 @@ func Create(ctx context.Context, clients *shared.ClientFactory, log *logger.Logg return appDirPath, nil } -// getAppDirName will validate and return the app's directory name +// multiDashRe matches consecutive dashes. +var multiDashRe = regexp.MustCompile(`-{2,}`) + +// nonAlphanumericRe matches any character that is not a lowercase letter, digit, or dash. +var nonAlphanumericRe = regexp.MustCompile(`[^a-z0-9-]+`) + +// getAppDirName will validate and return the app's directory name in kebab-case func getAppDirName(appName string) (string, error) { if len(appName) <= 0 { return "", fmt.Errorf("app name is required") } - // trim whitespace + // Normalize to kebab-case: lowercase, replace non-alphanumeric with dashes, collapse, and trim appName = strings.TrimSpace(appName) - appName = strings.ReplaceAll(appName, " ", "-") + appName = strings.ToLower(appName) + appName = nonAlphanumericRe.ReplaceAllString(appName, "-") + appName = multiDashRe.ReplaceAllString(appName, "-") + appName = strings.Trim(appName, "-") + + if len(appName) == 0 { + return "", fmt.Errorf("app name must contain at least one alphanumeric character") + } // name cannot be a reserved word if goutils.Contains(reserved, appName, false) { diff --git a/internal/pkg/create/create_test.go b/internal/pkg/create/create_test.go index baaa5583..dc80185b 100644 --- a/internal/pkg/create/create_test.go +++ b/internal/pkg/create/create_test.go @@ -38,33 +38,75 @@ func TestCreate(t *testing.T) { } func TestGetProjectDirectoryName(t *testing.T) { - var appName string - var err error - - // Test with empty name returns an error - appName, err = getAppDirName("") - assert.Error(t, err, "should return an error for empty name") - assert.Equal(t, "", appName) - - // Test with app name - appName, err = getAppDirName("my-app") - assert.NoError(t, err, "should not return an error") - assert.Equal(t, "my-app", appName, "should return 'my-app'") - - // Test with a dot in the app name - appName, err = getAppDirName(".my-app") - assert.NoError(t, err, "should not return an error") - assert.Equal(t, ".my-app", appName, "should return '.my-app'") - - // Spaces replaced with hyphens - appName, err = getAppDirName("my cool app") - assert.NoError(t, err) - assert.Equal(t, "my-cool-app", appName) - - // Leading/trailing spaces trimmed - appName, err = getAppDirName(" my-app ") - assert.NoError(t, err) - assert.Equal(t, "my-app", appName) + tests := map[string]struct { + input string + expected string + hasError bool + }{ + "empty name returns error": { + input: "", + hasError: true, + }, + "simple kebab-case name": { + input: "my-app", + expected: "my-app", + }, + "spaces replaced with hyphens": { + input: "my cool app", + expected: "my-cool-app", + }, + "leading and trailing spaces trimmed": { + input: " my-app ", + expected: "my-app", + }, + "uppercase converted to lowercase": { + input: "My Slack App", + expected: "my-slack-app", + }, + "mixed case normalized": { + input: "My-Slack-App", + expected: "my-slack-app", + }, + "special characters replaced with dashes": { + input: "my_app!@#test", + expected: "my-app-test", + }, + "consecutive special characters collapsed to single dash": { + input: "my---app", + expected: "my-app", + }, + "leading and trailing special characters trimmed": { + input: "---my-app---", + expected: "my-app", + }, + "dots converted to dashes": { + input: ".my-app", + expected: "my-app", + }, + "only special characters returns error": { + input: "!!!", + hasError: true, + }, + "numbers preserved": { + input: "app123", + expected: "app123", + }, + "complex mixed input": { + input: " My Cool App! (v2) ", + expected: "my-cool-app-v2", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result, err := getAppDirName(tc.input) + if tc.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + } + }) + } } func TestGetAvailableDirectory(t *testing.T) {