Skip to content

Commit 906346a

Browse files
committed
simplify app creation with app link cmd
1 parent aceb7a4 commit 906346a

4 files changed

Lines changed: 18 additions & 759 deletions

File tree

cmd/project/create.go

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import (
2323
"strings"
2424
"time"
2525

26+
"github.com/slackapi/slack-cli/cmd/app"
2627
"github.com/slackapi/slack-cli/internal/iostreams"
27-
"github.com/slackapi/slack-cli/internal/pkg/apps"
2828
"github.com/slackapi/slack-cli/internal/pkg/create"
2929
"github.com/slackapi/slack-cli/internal/shared"
3030
"github.com/slackapi/slack-cli/internal/shared/types"
@@ -133,7 +133,7 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
133133
WithMessage("The --subdir flag requires the --template flag")
134134
}
135135

136-
// --app requires --template (Mode 2 deferred)
136+
// --app requires --template
137137
appFlagProvided := clients.Config.AppFlag != "" && types.IsAppID(clients.Config.AppFlag)
138138
if appFlagProvided && !templateFlagProvided {
139139
return slackerror.New(slackerror.ErrMismatchedFlags).
@@ -146,23 +146,6 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
146146
WithMessage("The --environment flag requires the --app flag when used with create")
147147
}
148148

149-
// Fail fast: resolve auth and fetch manifest before creating the project
150-
var appAuth types.SlackAuth
151-
var remoteManifest types.SlackYaml
152-
if appFlagProvided {
153-
auth, err := apps.ResolveAuthForApp(ctx, clients, clients.Config.AppFlag)
154-
if err != nil {
155-
return err
156-
}
157-
appAuth = auth
158-
159-
manifest, err := apps.FetchRemoteManifest(ctx, clients, auth.Token, clients.Config.AppFlag)
160-
if err != nil {
161-
return err
162-
}
163-
remoteManifest = manifest
164-
}
165-
166149
// Collect the template URL or select a starting template
167150
template, err := promptTemplateSelection(cmd, clients, categoryShortcut)
168151
if err != nil {
@@ -183,13 +166,7 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
183166

184167
// Prompt for app name if not provided via flag or argument
185168
if appPathArg == "" {
186-
if appFlagProvided {
187-
if remoteManifest.DisplayInformation.Name != "" {
188-
appPathArg = remoteManifest.DisplayInformation.Name
189-
} else {
190-
appPathArg = generateRandomAppName()
191-
}
192-
} else if clients.IO.IsTTY() {
169+
if clients.IO.IsTTY() {
193170
defaultName := generateRandomAppName()
194171
name, err := clients.IO.InputPrompt(ctx, "Name your app:", iostreams.InputPromptConfig{
195172
Placeholder: defaultName,
@@ -230,19 +207,11 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
230207
if err != nil {
231208
return slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
232209
}
233-
if nameFlagProvided {
234-
remoteManifest.DisplayInformation.Name = displayName
235-
}
236-
if err := apps.WriteManifestToProject(clients.Fs, absProjectPath, remoteManifest); err != nil {
237-
return err
238-
}
239-
// LinkAppToProject requires the working directory to be the project
240-
// because SaveDeployed/SaveLocal use os.Getwd() to find .slack/
241210
originalDir, _ := clients.Os.Getwd()
242211
if err := os.Chdir(absProjectPath); err != nil {
243212
return slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
244213
}
245-
linkErr := apps.LinkAppToProject(ctx, clients, appAuth, clients.Config.AppFlag, remoteManifest, createEnvironmentFlag)
214+
linkErr := app.LinkExistingApp(ctx, clients, &types.App{}, false)
246215
_ = os.Chdir(originalDir)
247216
if linkErr != nil {
248217
return linkErr

cmd/project/create_test.go

Lines changed: 14 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package project
1616

1717
import (
1818
"context"
19-
"encoding/json"
2019
"os"
2120
"path/filepath"
2221
"testing"
@@ -876,239 +875,6 @@ func TestCreateCommand_AppFlag(t *testing.T) {
876875
createClientMock.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
877876
},
878877
},
879-
"app flag with template fetches manifest and links as local by default": {
880-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789"},
881-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
882-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
883-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
884-
885-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
886-
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
887-
}, nil)
888-
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
889-
Return(api.GetAppStatusResult{}, nil)
890-
891-
manifestMock := cf.AppClient().Manifest.(*app.ManifestMockObject)
892-
manifestMock.On("GetManifestRemote", mock.Anything, "xoxp-test-token", "A0123456789").
893-
Return(types.SlackYaml{
894-
AppManifest: types.AppManifest{
895-
Settings: &types.AppSettings{
896-
FunctionRuntime: types.Remote,
897-
},
898-
},
899-
}, nil)
900-
901-
appClientMock := &app.AppClientMock{}
902-
appClientMock.On("GetDeployed", mock.Anything, "T123").Return(types.NewApp(), nil)
903-
appClientMock.On("GetLocal", mock.Anything, "T123").Return(types.NewApp(), nil)
904-
appClientMock.On("SaveLocal", mock.Anything, mock.Anything).Return(nil)
905-
cf.AppClient().AppClientInterface = appClientMock
906-
907-
tmpDir := t.TempDir()
908-
projectDir := filepath.Join(tmpDir, "my-app")
909-
require.NoError(t, os.MkdirAll(projectDir, 0755))
910-
911-
createClientMock = new(CreateClientMock)
912-
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
913-
CreateFunc = createClientMock.Create
914-
},
915-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
916-
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
917-
},
918-
},
919-
"app flag with slack-hosted runtime links as deployed": {
920-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789"},
921-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
922-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
923-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
924-
925-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
926-
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
927-
}, nil)
928-
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
929-
Return(api.GetAppStatusResult{}, nil)
930-
931-
manifestMock := cf.AppClient().Manifest.(*app.ManifestMockObject)
932-
manifestMock.On("GetManifestRemote", mock.Anything, "xoxp-test-token", "A0123456789").
933-
Return(types.SlackYaml{
934-
AppManifest: types.AppManifest{
935-
Settings: &types.AppSettings{
936-
FunctionRuntime: types.SlackHosted,
937-
},
938-
},
939-
}, nil)
940-
941-
appClientMock := &app.AppClientMock{}
942-
appClientMock.On("GetDeployed", mock.Anything, "T123").Return(types.NewApp(), nil)
943-
appClientMock.On("GetLocal", mock.Anything, "T123").Return(types.NewApp(), nil)
944-
appClientMock.On("SaveDeployed", mock.Anything, mock.MatchedBy(func(a types.App) bool {
945-
return a.AppID == "A0123456789" && a.TeamID == "T123" && !a.IsDev
946-
})).Return(nil)
947-
cf.AppClient().AppClientInterface = appClientMock
948-
949-
tmpDir := t.TempDir()
950-
projectDir := filepath.Join(tmpDir, "my-app")
951-
require.NoError(t, os.MkdirAll(projectDir, 0755))
952-
953-
createClientMock = new(CreateClientMock)
954-
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
955-
CreateFunc = createClientMock.Create
956-
},
957-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
958-
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
959-
},
960-
},
961-
"app flag with local runtime links as dev app": {
962-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789"},
963-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
964-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
965-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
966-
967-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
968-
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
969-
}, nil)
970-
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
971-
Return(api.GetAppStatusResult{}, nil)
972-
973-
manifestMock := cf.AppClient().Manifest.(*app.ManifestMockObject)
974-
manifestMock.On("GetManifestRemote", mock.Anything, "xoxp-test-token", "A0123456789").
975-
Return(types.SlackYaml{
976-
AppManifest: types.AppManifest{
977-
Settings: &types.AppSettings{
978-
FunctionRuntime: types.LocallyRun,
979-
},
980-
},
981-
}, nil)
982-
983-
appClientMock := &app.AppClientMock{}
984-
appClientMock.On("GetDeployed", mock.Anything, "T123").Return(types.NewApp(), nil)
985-
appClientMock.On("GetLocal", mock.Anything, "T123").Return(types.NewApp(), nil)
986-
appClientMock.On("SaveLocal", mock.Anything, mock.Anything).Return(nil)
987-
cf.AppClient().AppClientInterface = appClientMock
988-
989-
tmpDir := t.TempDir()
990-
projectDir := filepath.Join(tmpDir, "my-app")
991-
require.NoError(t, os.MkdirAll(projectDir, 0755))
992-
993-
createClientMock = new(CreateClientMock)
994-
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
995-
CreateFunc = createClientMock.Create
996-
},
997-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
998-
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
999-
},
1000-
},
1001-
"name flag overrides manifest display name": {
1002-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--name", "Custom Name"},
1003-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
1004-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1005-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
1006-
1007-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
1008-
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
1009-
}, nil)
1010-
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
1011-
Return(api.GetAppStatusResult{}, nil)
1012-
1013-
manifestMock := cf.AppClient().Manifest.(*app.ManifestMockObject)
1014-
manifestMock.On("GetManifestRemote", mock.Anything, "xoxp-test-token", "A0123456789").
1015-
Return(types.SlackYaml{
1016-
AppManifest: types.AppManifest{
1017-
DisplayInformation: types.DisplayInformation{
1018-
Name: "Original Remote Name",
1019-
},
1020-
Settings: &types.AppSettings{
1021-
FunctionRuntime: types.Remote,
1022-
},
1023-
},
1024-
}, nil)
1025-
1026-
appClientMock := &app.AppClientMock{}
1027-
appClientMock.On("GetDeployed", mock.Anything, "T123").Return(types.NewApp(), nil)
1028-
appClientMock.On("GetLocal", mock.Anything, "T123").Return(types.NewApp(), nil)
1029-
appClientMock.On("SaveLocal", mock.Anything, mock.Anything).Return(nil)
1030-
cf.AppClient().AppClientInterface = appClientMock
1031-
1032-
tmpDir := t.TempDir()
1033-
projectDir := filepath.Join(tmpDir, "my-app")
1034-
require.NoError(t, os.MkdirAll(projectDir, 0755))
1035-
1036-
createClientMock = new(CreateClientMock)
1037-
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
1038-
CreateFunc = createClientMock.Create
1039-
},
1040-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
1041-
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
1042-
call := createClientMock.Calls[0]
1043-
projectDir := call.ReturnArguments[0].(string)
1044-
data, err := afero.ReadFile(cm.Fs, filepath.Join(projectDir, "manifest.json"))
1045-
require.NoError(t, err)
1046-
var result map[string]any
1047-
require.NoError(t, json.Unmarshal(data, &result))
1048-
displayInfo := result["display_information"].(map[string]any)
1049-
assert.Equal(t, "Custom Name", displayInfo["name"])
1050-
},
1051-
},
1052-
"app flag with no authenticated workspace returns error": {
1053-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789"},
1054-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
1055-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1056-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
1057-
1058-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{}, nil)
1059-
1060-
createClientMock = new(CreateClientMock)
1061-
CreateFunc = createClientMock.Create
1062-
},
1063-
ExpectedErrorStrings: []string{"No workspaces connected"},
1064-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
1065-
createClientMock.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
1066-
},
1067-
},
1068-
"app flag with inaccessible app returns error": {
1069-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789"},
1070-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
1071-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1072-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
1073-
1074-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
1075-
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
1076-
}, nil)
1077-
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
1078-
Return(api.GetAppStatusResult{}, assert.AnError)
1079-
1080-
createClientMock = new(CreateClientMock)
1081-
CreateFunc = createClientMock.Create
1082-
},
1083-
ExpectedErrorStrings: []string{"No authenticated workspace has access to app A0123456789"},
1084-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
1085-
createClientMock.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
1086-
},
1087-
},
1088-
"app flag with manifest export failure surfaces original error": {
1089-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789"},
1090-
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
1091-
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1092-
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
1093-
1094-
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
1095-
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
1096-
}, nil)
1097-
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
1098-
Return(api.GetAppStatusResult{}, nil)
1099-
1100-
manifestMock := cf.AppClient().Manifest.(*app.ManifestMockObject)
1101-
manifestMock.On("GetManifestRemote", mock.Anything, "xoxp-test-token", "A0123456789").
1102-
Return(types.SlackYaml{}, assert.AnError)
1103-
1104-
createClientMock = new(CreateClientMock)
1105-
CreateFunc = createClientMock.Create
1106-
},
1107-
ExpectedErrorStrings: []string{assert.AnError.Error()},
1108-
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
1109-
createClientMock.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
1110-
},
1111-
},
1112878
"environment flag without app flag returns error": {
1113879
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--environment", "deployed"},
1114880
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
@@ -1120,46 +886,45 @@ func TestCreateCommand_AppFlag(t *testing.T) {
1120886
createClientMock.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
1121887
},
1122888
},
1123-
"environment flag deployed overrides manifest inference": {
1124-
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "deployed"},
889+
"app flag with template creates project then runs link": {
890+
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
1125891
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
1126892
cm.IO.On("SelectPrompt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
1127893
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil).Maybe()
1128894

895+
// LinkExistingApp calls PromptTeamSlackAuth which calls Auths
1129896
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{
1130897
{Token: "xoxp-test-token", TeamID: "T123", TeamDomain: "test-team", UserID: "U123"},
1131898
}, nil)
899+
// LinkExistingApp validates app access
1132900
cm.API.On("GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123").
1133901
Return(api.GetAppStatusResult{}, nil)
1134902

1135-
manifestMock := cf.AppClient().Manifest.(*app.ManifestMockObject)
1136-
manifestMock.On("GetManifestRemote", mock.Anything, "xoxp-test-token", "A0123456789").
1137-
Return(types.SlackYaml{
1138-
AppManifest: types.AppManifest{
1139-
Settings: &types.AppSettings{
1140-
FunctionRuntime: types.Remote,
1141-
},
1142-
},
1143-
}, nil)
1144-
903+
// LinkExistingApp calls saveAppToJSON
1145904
appClientMock := &app.AppClientMock{}
1146905
appClientMock.On("GetDeployed", mock.Anything, "T123").Return(types.NewApp(), nil)
1147906
appClientMock.On("GetLocal", mock.Anything, "T123").Return(types.NewApp(), nil)
1148-
appClientMock.On("SaveDeployed", mock.Anything, mock.MatchedBy(func(a types.App) bool {
1149-
return a.AppID == "A0123456789" && !a.IsDev
1150-
})).Return(nil)
907+
appClientMock.On("SaveLocal", mock.Anything, mock.Anything).Return(nil)
1151908
cf.AppClient().AppClientInterface = appClientMock
1152909

910+
// Create project directory on real filesystem for os.Chdir
1153911
tmpDir := t.TempDir()
1154912
projectDir := filepath.Join(tmpDir, "my-app")
1155913
require.NoError(t, os.MkdirAll(projectDir, 0755))
1156914

915+
// Set up mock Os.Getwd to return the project dir (used by ProjectConfig)
916+
cm.Os.On("Getwd").Return(projectDir, nil)
917+
// Create .slack/hooks.json on mock filesystem so GetProjectDirPath succeeds
918+
require.NoError(t, cm.Fs.MkdirAll(filepath.Join(projectDir, ".slack"), 0755))
919+
require.NoError(t, afero.WriteFile(cm.Fs, filepath.Join(projectDir, ".slack", "hooks.json"), []byte("{}"), 0644))
920+
1157921
createClientMock = new(CreateClientMock)
1158922
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
1159923
CreateFunc = createClientMock.Create
1160924
},
1161925
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
1162926
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
927+
cm.API.AssertCalled(t, "GetAppStatus", mock.Anything, "xoxp-test-token", []string{"A0123456789"}, "T123")
1163928
},
1164929
},
1165930
}, func(cf *shared.ClientFactory) *cobra.Command {

0 commit comments

Comments
 (0)