Skip to content

Commit 37c62fb

Browse files
committed
updates
1 parent eabc9ba commit 37c62fb

File tree

11 files changed

+577
-153
lines changed

11 files changed

+577
-153
lines changed

cmd/app/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func RunDeleteCommand(ctx context.Context, clients *shared.ClientFactory, cmd *c
111111
func confirmDeletion(ctx context.Context, IO iostreams.IOStreamer, app prompts.SelectedApp) (bool, error) {
112112
IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
113113
Emoji: "warning",
114-
Text: style.Bold(" Danger zone"),
114+
Text: style.Bold("Danger zone"),
115115
Secondary: []string{
116116
fmt.Sprintf("App (%s) will be permanently deleted", app.App.AppID),
117117
"All triggers, workflows, and functions will be deleted",

cmd/sandbox/create.go

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"time"
2323

2424
"github.com/slackapi/slack-cli/internal/shared"
25-
"github.com/slackapi/slack-cli/internal/shared/types"
2625
"github.com/slackapi/slack-cli/internal/slackerror"
2726
"github.com/slackapi/slack-cli/internal/style"
2827
"github.com/spf13/cobra"
@@ -35,26 +34,22 @@ type createFlags struct {
3534
locale string
3635
owningOrgID string
3736
template string
37+
demoIDs []string
3838
eventCode string
3939
ttl string
40-
autoLogin bool
4140
output string
42-
token string
4341
}
4442

4543
var createCmdFlags createFlags
4644

4745
func NewCreateCommand(clients *shared.ClientFactory) *cobra.Command {
4846
cmd := &cobra.Command{
4947
Use: "create [flags]",
50-
Short: "Create a new sandbox",
51-
Long: `Create a new Slack developer sandbox.
52-
53-
Provisions a new sandbox. Domain is derived from org name if --domain is not provided.`,
48+
Short: "Create a developer sandbox",
49+
Long: `Create a new Slack developer sandbox`,
5450
Example: style.ExampleCommandsf([]style.ExampleCommand{
55-
{Command: "sandbox create --name test-box", Meaning: "Create a sandbox named test-box"},
56-
{Command: "sandbox create --name test-box --password mypass --owning-org-id E12345", Meaning: "Create a sandbox with login password and owning org"},
57-
{Command: "sandbox create --name test-box --domain test-box --ttl 24h --output json", Meaning: "Create an ephemeral sandbox for CI/CD with JSON output"},
51+
{Command: "sandbox create --name test-box --password mypass", Meaning: "Create a sandbox named test-box"},
52+
{Command: "sandbox create --name test-box --password mypass --domain test-box --ttl 1d", Meaning: "Create a temporary sandbox that will be archived in 1 day"},
5853
}),
5954
Args: cobra.NoArgs,
6055
PreRunE: func(cmd *cobra.Command, args []string) error {
@@ -68,24 +63,32 @@ Provisions a new sandbox. Domain is derived from org name if --domain is not pro
6863
cmd.Flags().StringVar(&createCmdFlags.name, "name", "", "Organization name for the new sandbox")
6964
cmd.Flags().StringVar(&createCmdFlags.domain, "domain", "", "Team domain (e.g., pizzaknifefight). If not provided, derived from org name")
7065
cmd.Flags().StringVar(&createCmdFlags.password, "password", "", "Password used to log into the sandbox")
71-
cmd.Flags().StringVar(&createCmdFlags.owningOrgID, "owning-org-id", "", "Enterprise team ID that manages your developer account, if applicable")
7266
cmd.Flags().StringVar(&createCmdFlags.template, "template", "", "Template ID for pre-defined data to preload")
67+
cmd.Flags().StringSliceVar(&createCmdFlags.demoIDs, "demo-ids", nil, "Demo IDs to preload in the sandbox")
7368
cmd.Flags().StringVar(&createCmdFlags.eventCode, "event-code", "", "Event code for the sandbox")
7469
cmd.Flags().StringVar(&createCmdFlags.ttl, "ttl", "", "Time-to-live duration; sandbox will be archived after this period (e.g., 2h, 1d, 7d)")
7570
cmd.Flags().StringVar(&createCmdFlags.output, "output", "text", "Output format: json, text")
76-
cmd.Flags().StringVar(&createCmdFlags.token, "token", "", "Service account token for CI/CD authentication")
7771

78-
cmd.MarkFlagRequired("name")
79-
cmd.MarkFlagRequired("domain")
80-
cmd.MarkFlagRequired("password")
72+
// If one's developer account is managed by multiple Production Slack teams, one of those team IDs must be provided in the command
73+
cmd.Flags().StringVar(&createCmdFlags.owningOrgID, "owning-org-id", "", "Enterprise team ID that manages your developer account, if applicable")
74+
75+
if err := cmd.MarkFlagRequired("name"); err != nil {
76+
panic(err)
77+
}
78+
if err := cmd.MarkFlagRequired("domain"); err != nil {
79+
panic(err)
80+
}
81+
if err := cmd.MarkFlagRequired("password"); err != nil {
82+
panic(err)
83+
}
8184

8285
return cmd
8386
}
8487

8588
func runCreateCommand(cmd *cobra.Command, clients *shared.ClientFactory) error {
8689
ctx := cmd.Context()
8790

88-
token, err := getSandboxToken(ctx, clients, createCmdFlags.token)
91+
auth, err := getSandboxAuth(ctx, clients)
8992
if err != nil {
9093
return err
9194
}
@@ -100,7 +103,7 @@ func runCreateCommand(cmd *cobra.Command, clients *shared.ClientFactory) error {
100103
return err
101104
}
102105

103-
result, err := clients.API().CreateSandbox(ctx, token,
106+
teamID, sandboxURL, err := clients.API().CreateSandbox(ctx, auth.Token,
104107
createCmdFlags.name,
105108
domain,
106109
createCmdFlags.password,
@@ -118,15 +121,11 @@ func runCreateCommand(cmd *cobra.Command, clients *shared.ClientFactory) error {
118121
case "json":
119122
encoder := json.NewEncoder(clients.IO.WriteOut())
120123
encoder.SetIndent("", " ")
121-
if err := encoder.Encode(result); err != nil {
124+
if err := encoder.Encode(map[string]string{"team_id": teamID, "url": sandboxURL}); err != nil {
122125
return err
123126
}
124127
default:
125-
printCreateSuccess(cmd, clients, result)
126-
}
127-
128-
if createCmdFlags.autoLogin && result.URL != "" {
129-
clients.Browser().OpenURL(result.URL)
128+
printCreateSuccess(cmd, clients, teamID, sandboxURL)
130129
}
131130

132131
return nil
@@ -195,15 +194,14 @@ func slugFromsandboxName(name string) string {
195194
return string(b)
196195
}
197196

198-
func printCreateSuccess(cmd *cobra.Command, clients *shared.ClientFactory, result types.CreateSandboxResult) {
197+
func printCreateSuccess(cmd *cobra.Command, clients *shared.ClientFactory, teamID, url string) {
199198
ctx := cmd.Context()
200199
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
201200
Emoji: "beach_with_umbrella",
202201
Text: " Sandbox Created",
203202
Secondary: []string{
204-
fmt.Sprintf("Team ID: %s", result.TeamID),
205-
fmt.Sprintf("User ID: %s", result.UserID),
206-
fmt.Sprintf("URL: %s", result.URL),
203+
fmt.Sprintf("Team ID: %s", teamID),
204+
fmt.Sprintf("URL: %s", url),
207205
},
208206
}))
209207
}

cmd/sandbox/create_test.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sandbox
16+
17+
import (
18+
"context"
19+
"errors"
20+
"testing"
21+
22+
"github.com/slackapi/slack-cli/internal/experiment"
23+
"github.com/slackapi/slack-cli/internal/shared"
24+
"github.com/slackapi/slack-cli/internal/shared/types"
25+
"github.com/slackapi/slack-cli/test/testutil"
26+
"github.com/spf13/cobra"
27+
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/mock"
29+
)
30+
31+
func TestCreateCommand(t *testing.T) {
32+
testutil.TableTestCommand(t, testutil.CommandTests{
33+
"create success": {
34+
CmdArgs: []string{
35+
"--experiment=sandboxes",
36+
"--token", "xoxb-test-token",
37+
"--name", "test-box",
38+
"--domain", "test-box",
39+
"--password", "mypass",
40+
},
41+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
42+
testToken := "xoxb-test-token"
43+
cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil)
44+
cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com")
45+
cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything, mock.Anything).Return("https://slackb.com/events/cli")
46+
cm.API.On("CreateSandbox", mock.Anything, testToken, "test-box", "test-box", "mypass", "", "", "", "", int64(0)).
47+
Return("T123", "https://test-box.slack.com", nil)
48+
cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil)
49+
50+
cm.AddDefaultMocks()
51+
cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)}
52+
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
53+
},
54+
ExpectedStdoutOutputs: []string{"T123", "https://test-box.slack.com", "Sandbox Created"},
55+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
56+
cm.Auth.AssertCalled(t, "AuthWithToken", mock.Anything, "xoxb-test-token")
57+
cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "test-box", "test-box", "mypass", "", "", "", "", int64(0))
58+
},
59+
},
60+
"create with JSON output": {
61+
CmdArgs: []string{
62+
"--experiment=sandboxes",
63+
"--token", "xoxb-test-token",
64+
"--name", "json-box",
65+
"--domain", "json-box",
66+
"--password", "secret",
67+
"--output", "json",
68+
},
69+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
70+
testToken := "xoxb-test-token"
71+
cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil)
72+
cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com")
73+
cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything, mock.Anything).Return("https://slackb.com/events/cli")
74+
cm.API.On("CreateSandbox", mock.Anything, testToken, "json-box", "json-box", "secret", "", "", "", "", int64(0)).
75+
Return("T456", "https://json-box.slack.com", nil)
76+
77+
cm.AddDefaultMocks()
78+
cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)}
79+
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
80+
},
81+
ExpectedStdoutOutputs: []string{`"team_id": "T456"`, `"url": "https://json-box.slack.com"`},
82+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
83+
cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "json-box", "json-box", "secret", "", "", "", "", int64(0))
84+
},
85+
},
86+
"create with derived domain": {
87+
CmdArgs: []string{
88+
"--experiment=sandboxes",
89+
"--token", "xoxb-test-token",
90+
"--name", "My Test Box",
91+
"--domain", "my-test-box",
92+
"--password", "pass",
93+
},
94+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
95+
testToken := "xoxb-test-token"
96+
cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil)
97+
cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com")
98+
cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything, mock.Anything).Return("https://slackb.com/events/cli")
99+
cm.API.On("CreateSandbox", mock.Anything, testToken, "My Test Box", "my-test-box", "pass", "", "", "", "", int64(0)).
100+
Return("T789", "https://my-test-box.slack.com", nil)
101+
102+
cm.AddDefaultMocks()
103+
cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)}
104+
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
105+
},
106+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
107+
cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "My Test Box", "my-test-box", "pass", "", "", "", "", int64(0))
108+
},
109+
},
110+
"create with TTL": {
111+
CmdArgs: []string{
112+
"--experiment=sandboxes",
113+
"--token", "xoxb-test-token",
114+
"--name", "ttl-box",
115+
"--domain", "ttl-box",
116+
"--password", "pass",
117+
"--ttl", "24h",
118+
},
119+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
120+
testToken := "xoxb-test-token"
121+
cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil)
122+
cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com")
123+
cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything, mock.Anything).Return("https://slackb.com/events/cli")
124+
cm.API.On("CreateSandbox", mock.Anything, testToken, "ttl-box", "ttl-box", "pass", "", "", "", "", mock.MatchedBy(func(v int64) bool { return v > 0 })).
125+
Return("T111", "https://ttl-box.slack.com", nil)
126+
127+
cm.AddDefaultMocks()
128+
cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)}
129+
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
130+
},
131+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
132+
cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "ttl-box", "ttl-box", "pass", "", "", "", "", mock.MatchedBy(func(v int64) bool { return v > 0 }))
133+
},
134+
},
135+
"create API error": {
136+
CmdArgs: []string{
137+
"--experiment=sandboxes",
138+
"--token", "xoxb-test-token",
139+
"--name", "err-box",
140+
"--domain", "err-box",
141+
"--password", "pass",
142+
},
143+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
144+
testToken := "xoxb-test-token"
145+
cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil)
146+
cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com")
147+
cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything, mock.Anything).Return("https://slackb.com/events/cli")
148+
cm.API.On("CreateSandbox", mock.Anything, testToken, "err-box", "err-box", "pass", "", "", "", "", int64(0)).
149+
Return("", "", errors.New("api_error"))
150+
151+
cm.AddDefaultMocks()
152+
cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)}
153+
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
154+
},
155+
ExpectedErrorStrings: []string{"api_error"},
156+
},
157+
"invalid TTL": {
158+
CmdArgs: []string{
159+
"--experiment=sandboxes",
160+
"--token", "xoxb-test-token",
161+
"--name", "ttl-box",
162+
"--domain", "ttl-box",
163+
"--password", "pass",
164+
"--ttl", "invalid",
165+
},
166+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
167+
testToken := "xoxb-test-token"
168+
cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil)
169+
cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com")
170+
cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything, mock.Anything).Return("https://slackb.com/events/cli")
171+
172+
cm.AddDefaultMocks()
173+
cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)}
174+
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
175+
},
176+
ExpectedErrorStrings: []string{"Invalid TTL"},
177+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
178+
cm.API.AssertNotCalled(t, "CreateSandbox", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
179+
},
180+
},
181+
"experiment required": {
182+
CmdArgs: []string{
183+
"--name", "test-box",
184+
"--domain", "test-box",
185+
"--password", "pass",
186+
},
187+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
188+
cm.AddDefaultMocks()
189+
},
190+
ExpectedErrorStrings: []string{"sandbox"},
191+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
192+
cm.API.AssertNotCalled(t, "CreateSandbox", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
193+
},
194+
},
195+
}, func(cf *shared.ClientFactory) *cobra.Command {
196+
return NewCreateCommand(cf)
197+
})
198+
}
199+
200+
func Test_ttlToArchiveDate(t *testing.T) {
201+
tests := []struct {
202+
name string
203+
ttl string
204+
wantErr bool
205+
}{
206+
{"empty", "", false},
207+
{"24h", "24h", false},
208+
{"1d", "1d", false},
209+
{"7d", "7d", false},
210+
{"invalid", "invalid", true},
211+
{"exceeds max", "200d", true},
212+
}
213+
for _, tt := range tests {
214+
t.Run(tt.name, func(t *testing.T) {
215+
got, err := ttlToArchiveDate(tt.ttl)
216+
if tt.wantErr {
217+
assert.Error(t, err)
218+
return
219+
}
220+
assert.NoError(t, err)
221+
if tt.ttl == "" {
222+
assert.Equal(t, int64(0), got)
223+
} else {
224+
assert.Greater(t, got, int64(0), "archive date should be in the future")
225+
}
226+
})
227+
}
228+
}
229+
230+
func Test_slugFromsandboxName(t *testing.T) {
231+
tests := []struct {
232+
name string
233+
in string
234+
want string
235+
}{
236+
{"simple", "test-box", "test-box"},
237+
{"spaces", "My Test Box", "my-test-box"},
238+
{"uppercase", "MyBox", "mybox"},
239+
{"mixed", "Hello_World 123", "hello-world-123"},
240+
{"hyphens", "a--b", "a-b"},
241+
{"leading trailing", "-test-", "test"},
242+
{"empty", "", "sandbox"},
243+
}
244+
for _, tt := range tests {
245+
t.Run(tt.name, func(t *testing.T) {
246+
got := slugFromsandboxName(tt.in)
247+
assert.Equal(t, tt.want, got)
248+
})
249+
}
250+
}

0 commit comments

Comments
 (0)