Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions internal/pkg/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat
return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
}

// Get the app selection and accompanying app directory name (this may change when we find the unique directory name)
appDirName, err := getAppDirName(createArgs.AppName)
// Parse the app name input into a directory path and display name
appPath, displayName, err := parseAppPath(createArgs.AppName)
if err != nil {
return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
}

// Get the project's full directory path
projectDirPath := ""
if filepath.IsLocal(appDirName) {
projectDirPath = filepath.Join(workingDirPath, appDirName)
if filepath.IsLocal(appPath) {
projectDirPath = filepath.Join(workingDirPath, appPath)
projectDirPath, err = getAvailableDir(ctx, projectDirPath)
if err != nil {
return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
Expand All @@ -86,7 +86,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat
return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
}
} else {
projectDirPath = filepath.Join(appDirName)
projectDirPath = filepath.Join(appPath)
projectDirPath, err = getAvailableDir(ctx, projectDirPath)
if err != nil {
return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
Expand All @@ -98,7 +98,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat
}

// Update the app's directory name now that the unique directory is created
appDirName = filepath.Base(projectDirPath)
appDirName := filepath.Base(projectDirPath)

// Print a bunch of information about the progress of the command to traces
// and debugs and the standard output here
Expand Down Expand Up @@ -150,7 +150,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat
}()

// Update default project files' app name, bot name, etc
if err := app.UpdateDefaultProjectFiles(clients.Fs, projectDirPath, appDirName, createArgs.AppName); err != nil {
if err := app.UpdateDefaultProjectFiles(clients.Fs, projectDirPath, appDirName, displayName); err != nil {
return "", slackerror.Wrap(err, slackerror.ErrProjectFileUpdate)
}

Expand Down Expand Up @@ -192,6 +192,29 @@ func getAppDirName(appName string) (string, error) {
return appName, nil
}

// parseAppPath splits user input into a directory path (with kebab-cased basename)
// and a display name (the raw basename preserving original casing/spacing).
func parseAppPath(input string) (appPath string, displayName string, err error) {
input = strings.TrimSpace(input)
if input == "" {
return "", "", fmt.Errorf("app name is required")
}

input = filepath.Clean(input)
displayName = filepath.Base(input)
pathPrefix := filepath.Dir(input)

dirName, err := getAppDirName(displayName)
if err != nil {
return "", "", err
}

if pathPrefix == "." {
return dirName, displayName, nil
}
return filepath.Join(pathPrefix, dirName), displayName, nil
}

// getAvailableDir will return a unique directory path.
// If dirPath already exists, then a unique numbered path will be appended to the path.
func getAvailableDir(ctx context.Context, dirPath string) (string, error) {
Expand Down
79 changes: 79 additions & 0 deletions internal/pkg/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,85 @@ func TestGetProjectDirectoryName(t *testing.T) {
}
}

func TestParseAppPath(t *testing.T) {
tests := map[string]struct {
input string
expectedPath string
expectedDisplay string
hasError bool
}{
"simple kebab-case name": {
input: "my-app",
expectedPath: "my-app",
expectedDisplay: "my-app",
},
"name with spaces": {
input: "My Cool App",
expectedPath: "my-cool-app",
expectedDisplay: "My Cool App",
},
"relative path with simple name": {
input: "path/to/my-app",
expectedPath: filepath.Join("path", "to", "my-app"),
expectedDisplay: "my-app",
},
"relative path with spaced name": {
input: "path/to/My App",
expectedPath: filepath.Join("path", "to", "my-app"),
expectedDisplay: "My App",
},
"dot-prefixed path": {
input: "./my-app",
expectedPath: "my-app",
expectedDisplay: "my-app",
},
"absolute path": {
input: "/abs/path/app",
expectedPath: filepath.Join("/abs", "path", "app"),
expectedDisplay: "app",
},
"single directory depth": {
input: "projects/my-app",
expectedPath: filepath.Join("projects", "my-app"),
expectedDisplay: "my-app",
},
"uppercase in nested path": {
input: "projects/My Slack App",
expectedPath: filepath.Join("projects", "my-slack-app"),
expectedDisplay: "My Slack App",
},
"trailing slash is trimmed": {
input: "path/to/my-app/",
expectedPath: filepath.Join("path", "to", "my-app"),
expectedDisplay: "my-app",
},
"empty string returns error": {
input: "",
hasError: true,
},
"whitespace only returns error": {
input: " ",
hasError: true,
},
"basename with only special chars returns error": {
input: "path/to/!!!",
hasError: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
appPath, displayName, err := parseAppPath(tc.input)
if tc.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedPath, appPath)
assert.Equal(t, tc.expectedDisplay, displayName)
}
})
}
}

func TestGetAvailableDirectory(t *testing.T) {
var exists bool
var err error
Expand Down
Loading