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
31 changes: 25 additions & 6 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type cmdFlags struct {
data string
headers []string
include bool
noAuth bool
}

var flags cmdFlags
Expand Down Expand Up @@ -69,6 +70,9 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
" 4. SLACK_USER_TOKEN env var User token",
" 5. App prompt (in project) Select installed app and use bot token",
"",
"Use --no-auth to skip authentication entirely and send the request without",
"a token.",
"",
"See all methods at: https://docs.slack.dev/reference/methods",
}, "\n"),
Example: style.ExampleCommandsf([]style.ExampleCommand{
Expand All @@ -90,6 +94,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
{Command: "api users.info user=U0123456", Meaning: "Get user details"},
{Command: "api users.list", Meaning: "List workspace members"},
{Command: "api users.profile.get user=U0123456", Meaning: "Get a user's profile"},
{Command: `api blocks.validate --no-auth blocks='[{"type":"section","text":{"type":"mrkdwn","text":"Hello"}}]'`, Meaning: "Validate Block Kit blocks (no auth required)"},
{Command: "api views.open trigger_id=T0123456 view={...}", Meaning: "Open a modal view"},
{Command: "api views.update view_id=V0123456 view={...}", Meaning: "Update a modal view"},
}),
Expand All @@ -108,6 +113,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
cmd.Flags().StringVar(&flags.data, "data", "", "form-encoded request body string (e.g. \"key1=val1&key2=val2\")")
cmd.Flags().StringSliceVarP(&flags.headers, "header", "H", nil, "additional HTTP headers (format: \"Key: Value\")")
cmd.Flags().BoolVarP(&flags.include, "include", "i", false, "include HTTP status code and response headers in output")
cmd.Flags().BoolVar(&flags.noAuth, "no-auth", false, "skip authentication (send request without a token)")
cmd.MarkFlagsMutuallyExclusive("json", "data")

return cmd
Expand All @@ -119,9 +125,18 @@ func runAPICommand(cmd *cobra.Command, clients *shared.ClientFactory, args []str
method := args[0]
params := args[1:]

token, err := resolveToken(ctx, clients)
if err != nil {
return err
if flags.noAuth && (clients.Config.TokenFlag != "" || clients.Config.AppFlag != "") {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("--no-auth cannot be used with --token or --app")
}

var token = ""
if !flags.noAuth {
var err error
token, err = resolveToken(ctx, clients)
if err != nil {
return err
}
}

apiHost := clients.Config.APIHostResolved
Expand All @@ -142,7 +157,7 @@ func runAPICommand(cmd *cobra.Command, clients *shared.ClientFactory, args []str
case flags.data != "":
contentType = "application/x-www-form-urlencoded"
formData := flags.data
if !strings.Contains(formData, "token=") {
if token != "" && !strings.Contains(formData, "token=") {
if formData != "" {
formData = formData + "&token=" + url.QueryEscape(token)
} else {
Expand All @@ -157,7 +172,9 @@ func runAPICommand(cmd *cobra.Command, clients *shared.ClientFactory, args []str
case len(params) > 0:
contentType = "application/x-www-form-urlencoded"
values := url.Values{}
values.Set("token", token)
if token != "" {
values.Set("token", token)
}
for _, param := range params {
key, value, ok := strings.Cut(param, "=")
if !ok {
Expand All @@ -171,7 +188,9 @@ func runAPICommand(cmd *cobra.Command, clients *shared.ClientFactory, args []str
default:
contentType = "application/x-www-form-urlencoded"
values := url.Values{}
values.Set("token", token)
if token != "" {
values.Set("token", token)
}
bodyReader = strings.NewReader(values.Encode())
token = ""
}
Expand Down
116 changes: 108 additions & 8 deletions cmd/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ func Test_NewCommand(t *testing.T) {

func Test_runAPICommand_BodyFormats(t *testing.T) {
tests := map[string]struct {
flags cmdFlags
args []string
expectedMethod string
expectedCT string
expectedAuth string
bodyContains []string
bodyEquals string
flags cmdFlags
args []string
expectedMethod string
expectedCT string
expectedAuth string
assertNoAuth bool
bodyContains []string
bodyNotContains []string
bodyEquals string
}{
"form-encoded key=value params": {
flags: cmdFlags{method: "POST"},
Expand Down Expand Up @@ -84,6 +86,36 @@ func Test_runAPICommand_BodyFormats(t *testing.T) {
args: []string{"auth.test"},
expectedMethod: "GET",
},
"no-auth with key=value params": {
flags: cmdFlags{method: "POST", noAuth: true},
args: []string{"blocks.validate", "blocks=[...]"},
expectedCT: "application/x-www-form-urlencoded",
assertNoAuth: true,
bodyContains: []string{"blocks="},
bodyNotContains: []string{"token="},
},
"no-auth with --data flag": {
flags: cmdFlags{method: "POST", noAuth: true, data: "blocks=[...]"},
args: []string{"blocks.validate"},
expectedCT: "application/x-www-form-urlencoded",
assertNoAuth: true,
bodyEquals: "blocks=[...]",
bodyNotContains: []string{"token="},
},
"no-auth with --json flag": {
flags: cmdFlags{method: "POST", noAuth: true, json: `{"blocks":[]}`},
args: []string{"blocks.validate"},
expectedCT: "application/json; charset=utf-8",
assertNoAuth: true,
bodyEquals: `{"blocks":[]}`,
},
"no-auth with no params": {
flags: cmdFlags{method: "POST", noAuth: true},
args: []string{"api.test"},
expectedCT: "application/x-www-form-urlencoded",
assertNoAuth: true,
bodyEquals: "",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
Expand All @@ -105,7 +137,9 @@ func Test_runAPICommand_BodyFormats(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clientsMock.AddDefaultMocks()
clientsMock.Config.TokenFlag = "xoxb-test-token"
if !tc.flags.noAuth {
clientsMock.Config.TokenFlag = "xoxb-test-token"
}
clientsMock.Config.APIHostResolved = server.URL
clients := shared.NewClientFactory(clientsMock.MockClientFactory())

Expand All @@ -126,12 +160,22 @@ func Test_runAPICommand_BodyFormats(t *testing.T) {
if tc.expectedAuth != "" {
assert.Equal(t, tc.expectedAuth, receivedAuth)
}
if tc.assertNoAuth {
assert.Empty(t, receivedAuth)
assert.NotContains(t, receivedBody, "token=")
} else {
assert.True(t, receivedAuth != "" || strings.Contains(receivedBody, "token="),
"expected auth via Authorization header or token in body")
}
if tc.bodyEquals != "" {
assert.Equal(t, tc.bodyEquals, receivedBody)
}
for _, s := range tc.bodyContains {
assert.Contains(t, receivedBody, s)
}
for _, s := range tc.bodyNotContains {
assert.NotContains(t, receivedBody, s)
}
})
}
}
Expand Down Expand Up @@ -552,3 +596,59 @@ func Test_resolveToken_NoTokenFound(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "no token found")
}

func Test_runAPICommand_NoAuth_MutualExclusivity(t *testing.T) {
tests := map[string]struct {
tokenFlag string
appFlag string
}{
"no-auth with --token": {
tokenFlag: "xoxb-test",
},
"no-auth with --app": {
appFlag: "A123",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clientsMock.Config.TokenFlag = tc.tokenFlag
clientsMock.Config.AppFlag = tc.appFlag
clientsMock.Config.APIHostResolved = "https://slack.com"
clients := shared.NewClientFactory(clientsMock.MockClientFactory())

cmd := NewCommand(clients)
testutil.MockCmdIO(clients.IO, cmd)

flags = cmdFlags{method: "POST", noAuth: true}
cmd.SetArgs([]string{"blocks.validate"})
err := cmd.ExecuteContext(ctx)

assert.Error(t, err)
assert.Contains(t, err.Error(), "--no-auth cannot be used with --token or --app")
})
}
}

func Test_runAPICommand_NoAuth_SkipsTokenResolution(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"ok":true}`)
}))
defer server.Close()

ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clientsMock.AddDefaultMocks()
clientsMock.Config.APIHostResolved = server.URL
clients := shared.NewClientFactory(clientsMock.MockClientFactory())

cmd := NewCommand(clients)
testutil.MockCmdIO(clients.IO, cmd)

flags = cmdFlags{method: "POST", noAuth: true}
cmd.SetArgs([]string{"api.test"})
err := cmd.ExecuteContext(ctx)

assert.NoError(t, err)
}
Loading