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
18 changes: 9 additions & 9 deletions cmd/app/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *ty
// Add empty line between executed command and first output
clients.IO.PrintInfo(ctx, false, "")

err = LinkExistingApp(ctx, clients, app, false)
_, err = LinkExistingApp(ctx, clients, app, false)
if err != nil {
return err
}
Expand Down Expand Up @@ -130,7 +130,7 @@ func LinkAppHeaderSection(ctx context.Context, clients *shared.ClientFactory, sh
// When shouldConfirm is true, a confirmation prompt will ask the user if they want to
// link an existing app and additional information is included in the header.
// The shouldConfirm option is encouraged for third-party callers.
func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool) (err error) {
func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool) (_ *types.SlackAuth, err error) {
// Header section
LinkAppHeaderSection(ctx, clients, shouldConfirm)

Expand All @@ -139,21 +139,21 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty
proceed, err := clients.IO.ConfirmPrompt(ctx, LinkAppConfirmPromptText, true)
if err != nil {
clients.IO.PrintDebug(ctx, "Error prompting to add an existing app: %s", err)
return err
return nil, err
}

// Add newline to match the trailing newline inserted from the footer section
clients.IO.PrintInfo(ctx, false, "")

if !proceed {
return nil
return nil, nil
}
}

// App Manifest section
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
if err != nil {
return err
return nil, err
}

configPath := filepath.Join(config.ProjectConfigDirName, config.ProjectConfigJSONFilename)
Expand All @@ -170,26 +170,26 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty
var auth *types.SlackAuth
*app, auth, err = promptExistingApp(ctx, clients)
if err != nil {
return err
return nil, err
}

appIDs := []string{app.AppID}
_, err = clients.API().GetAppStatus(ctx, auth.Token, appIDs, app.TeamID)
if err != nil {
return err
return nil, err
}

// Save the app to the project
err = saveAppToJSON(ctx, clients, *app)
if err != nil {
clients.IO.PrintDebug(ctx, "Error saving app to file when linking existing app: %s", err)
return err
return nil, err
}

// Footer section
LinkAppFooterSection(ctx, clients, app)

return nil
return auth, nil
}

// LinkAppFooterSection displays the details of app that was added to the project.
Expand Down
37 changes: 35 additions & 2 deletions cmd/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package project

import (
"context"
"encoding/json"
"fmt"
"math/rand"
"os"
Expand All @@ -31,6 +32,7 @@ import (
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/slacktrace"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -229,8 +231,24 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
defer func() {
_ = os.Chdir(originalDir)
}()
if err := app.LinkExistingApp(ctx, clients, &types.App{}, false); err != nil {
return err

linkedApp := &types.App{}
auth, linkErr := app.LinkExistingApp(ctx, clients, linkedApp, false)
if linkErr != nil {
return linkErr
}

if auth != nil && linkedApp.AppID != "" {
fetchErr := fetchAndWriteRemoteManifest(ctx, clients, auth.Token, linkedApp.AppID, absProjectPath)
if fetchErr != nil {
clients.IO.PrintWarning(ctx, "%s", style.Sectionf(style.TextSection{
Text: "Could not fetch the remote app manifest",
Secondary: []string{
fetchErr.Error(),
"The template manifest was kept unchanged",
},
}))
}
}
}

Expand Down Expand Up @@ -289,6 +307,21 @@ func printCreateSuccess(ctx context.Context, clients *shared.ClientFactory, appP
clients.IO.PrintTrace(ctx, slacktrace.CreateSuccess)
}

// fetchAndWriteRemoteManifest fetches the app manifest from remote settings and writes it to the project.
func fetchAndWriteRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token, appID, projectPath string) error {
slackYaml, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
if err != nil {
return err
}
data, err := json.MarshalIndent(slackYaml.AppManifest, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
manifestPath := filepath.Join(projectPath, "manifest.json")
return afero.WriteFile(clients.Fs, manifestPath, data, 0644)
}

// generateRandomAppName will create a random app name based on two words and a number
func generateRandomAppName() string {
rand.New(rand.NewSource(time.Now().UnixNano()))
Expand Down
100 changes: 96 additions & 4 deletions cmd/project/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"testing"

"github.com/slackapi/slack-cli/internal/api"
"github.com/slackapi/slack-cli/internal/app"
internalApp "github.com/slackapi/slack-cli/internal/app"
"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/iostreams"
"github.com/slackapi/slack-cli/internal/pkg/create"
Expand All @@ -29,6 +29,7 @@ import (
"github.com/slackapi/slack-cli/internal/slackdeps"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/test/testutil"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -983,6 +984,97 @@ func TestCreateCommand_AppFlag(t *testing.T) {
})
}

func TestCreateCommand_AppFlag_FetchesRemoteManifest(t *testing.T) {
var createClientMock *CreateClientMock

mockAuth := types.SlackAuth{
Token: "xoxp-test-token",
TeamDomain: "test-team",
TeamID: "T001",
UserID: "U001",
}
mockManifest := types.SlackYaml{
AppManifest: types.AppManifest{
DisplayInformation: types.DisplayInformation{
Name: "My Remote App",
Description: "An app from remote settings",
},
},
}

setupAppFlagMocks := func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) string {
projectDir := t.TempDir()
createClientMock = new(CreateClientMock)
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
CreateFunc = createClientMock.Create

cm.Os.On("Getwd").Return(projectDir, nil)

err := cm.Fs.MkdirAll(projectDir+"/.slack", 0755)
require.NoError(t, err)
err = afero.WriteFile(cm.Fs, projectDir+"/.slack/hooks.json", []byte("{}"), 0644)
require.NoError(t, err)

cm.IO.On("SelectPrompt", mock.Anything, "Select a category:", mock.Anything, mock.Anything).
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil)

cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{mockAuth}, nil)
cm.IO.On("SelectPrompt", mock.Anything, "Select the existing app team", mock.Anything, mock.Anything, mock.Anything).
Return(iostreams.SelectPromptResponse{Prompt: true, Index: 0, Option: mockAuth.TeamDomain}, nil)
cm.IO.On("SelectPrompt", mock.Anything, "Choose the app environment", mock.Anything, mock.Anything, mock.Anything).
Return(iostreams.SelectPromptResponse{Prompt: true, Option: "local"}, nil)

cm.API.On("GetAppStatus", mock.Anything, mockAuth.Token, []string{"A0123456789"}, mockAuth.TeamID).
Return(api.GetAppStatusResult{}, nil)

return projectDir
}

var projectDir string

testutil.TableTestCommand(t, testutil.CommandTests{
"fetches remote manifest after linking app": {
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
projectDir = setupAppFlagMocks(t, ctx, cm, cf)

manifestMock := &internalApp.ManifestMockObject{}
manifestMock.On("GetManifestRemote", mock.Anything, mockAuth.Token, "A0123456789").
Return(mockManifest, nil)
cf.AppClient().Manifest = manifestMock
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)

manifestData, err := afero.ReadFile(cm.Fs, projectDir+"/manifest.json")
require.NoError(t, err)
assert.Contains(t, string(manifestData), `"name": "My Remote App"`)
assert.Contains(t, string(manifestData), `"description": "An app from remote settings"`)
},
},
"warns on manifest fetch failure": {
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
projectDir = setupAppFlagMocks(t, ctx, cm, cf)

manifestMock := &internalApp.ManifestMockObject{}
manifestMock.On("GetManifestRemote", mock.Anything, mockAuth.Token, "A0123456789").
Return(types.SlackYaml{}, slackerror.New("network error"))
cf.AppClient().Manifest = manifestMock
},
ExpectedStdoutOutputs: []string{
"Could not fetch the remote app manifest",
"The template manifest was kept unchanged",
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
},
},
}, func(cf *shared.ClientFactory) *cobra.Command {
return NewCreateCommand(cf)
})
}

var mockCreateLinkAuth = types.SlackAuth{
Token: "xoxp-example",
TeamDomain: "team1",
Expand All @@ -991,8 +1083,6 @@ var mockCreateLinkAuth = types.SlackAuth{
UserID: "U001",
}

// setupCreateLinkMocks prepares the in-memory project config and manifest mocks
// needed by app.LinkExistingApp when called from the create command.
func setupCreateLinkMocks(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
projectDirPath := slackdeps.MockWorkingDirectory
cm.Os.On("Getwd").Return(projectDirPath, nil)
Expand All @@ -1007,8 +1097,10 @@ func setupCreateLinkMocks(t *testing.T, ctx context.Context, cm *shared.ClientsM
require.FailNow(t, fmt.Sprintf("Failed to set the manifest source: %s", err))
}

manifestMock := &app.ManifestMockObject{}
manifestMock := &internalApp.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).
Return(types.SlackYaml{}, nil)
manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).
Return(types.SlackYaml{}, nil)
cf.AppClient().Manifest = manifestMock
}
2 changes: 1 addition & 1 deletion cmd/project/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func projectInitCommandRunE(clients *shared.ClientFactory, cmd *cobra.Command, a
_ = create.InstallProjectDependencies(ctx, clients, projectDirPath)

// Add an existing app to the project
err = app.LinkExistingApp(ctx, clients, &types.App{}, true)
_, err = app.LinkExistingApp(ctx, clients, &types.App{}, true)
if err != nil {
// Display the error but continue to init
clients.IO.PrintError(ctx, "%s", err.Error())
Expand Down
Loading