diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1505f..c5be712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [v0.5.0] - + +### Breaking Changes + +- **API Deprecation**: As per [Slack's latest API update](https://api.slack.com/changelog/2024-04-a-better-way-to-upload-files-is-here-to-stay), `files.upload` is now deprecated. We have updated our tool to use the new APIs, `files.getUploadURLExternal` and `files.completeUploadExternal`. +- **Channel Specification**: It is no longer possible to specify a `channel` for file uploads. You must now use `channel-id`. +- **Filetype Option Update**: The `-filetype` option has been modified. Please use `-snippet-type` instead for specifying the type of file when uploading to a snippet. + +### Changed + +* update Go version +* update dependent libraries +* update README + ## [v0.4.14] - 2023-09-30 ### Changed diff --git a/README.md b/README.md index 1a15da5..6578344 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,14 @@ The Slack API allows you to specify the filetype of a file when posting it as a config file name -channel string specify channel (unavailable for new Incoming Webhooks) +-channel-id string + specify channel id (for uploading a file) +-debug + debug mode (for developers) -filename string specify a file name (for uploading to snippet) -filetype string - specify a filetype (for uploading to snippet) + [compatible] specify a filetype for uploading to snippet. This option is maintained for compatibility. Please use -snippet-type instead. -icon-emoji string specify icon emoji (unavailable for new Incoming Webhooks) -interval duration @@ -68,6 +72,8 @@ The Slack API allows you to specify the filetype of a file when posting it as a slack url (Incoming Webhooks URL) -snippet switch to snippet uploading mode +-snippet-type string + specify a snippet_type (for uploading to snippet) -token string token (for uploading to snippet) -username string @@ -92,6 +98,7 @@ The toml file contains the following information. url = "https://hooks.slack.com/services/**" token = "xoxp-xxxxx" channel = "#general" +channel_id = "C12345678" username = "tester" icon_emoji = ":rocket:" interval = "1s" @@ -103,13 +110,12 @@ Note: * You can use the following options to customize your message when posting to Slack as text: `channel`, `username`, `icon_emoji`, and `interval`. * Due to a recent change in the specification for Incoming Webhooks, it is currently not possible to override the `channel`, `username`, and `icon_emoji` options when posting to Slack. For more information, please refer to [this resource](https://api.slack.com/messaging/webhooks#advanced_message_formatting) * You can create an Incoming Webhooks URL at https://slack.com/services/new/incoming-webhook - * To post a file as a snippet to Slack, you will need to provide both a `token` and a `channel`. + * To post a file as a snippet to Slack, you will need to provide both a `token` and a `channel_id`. * The `username` and `icon_emoji` options will be ignored when posting a file as a snippet to Slack. * For instructions on how to create a token, please see the next section. - -Tips: - - * If you want to use a different default channel for snippets, you can specify it using the `snippet_channel` option. + * You cannot specify a channel because the slack api support only the `channel_id`. + * If you don't specify `channel_id`, the file will be private. So, if you need to post a file public, you must specify `channel_id`. + * The Slack API can cause delays, so posting might take longer. ### How to create a token @@ -149,7 +155,7 @@ Some settings for the Slack API can be provided using environment variables. NOTIFY_SLACK_WEBHOOK_URL NOTIFY_SLACK_TOKEN NOTIFY_SLACK_CHANNEL -NOTIFY_SLACK_SNIPPET_CHANNEL +NOTIFY_SLACK_CHANNEL_ID NOTIFY_SLACK_USERNAME NOTIFY_SLACK_ICON_EMOJI NOTIFY_SLACK_INTERVAL diff --git a/internal/cli/cli.go b/internal/cli/cli.go index cf9b841..2edda25 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -69,7 +69,8 @@ func (c *CLI) Run(args []string) int { flags := flag.NewFlagSet("notify_slack", flag.ContinueOnError) flags.SetOutput(c.errStream) - flags.StringVar(&c.conf.PrimaryChannel, "channel", "", "specify channel (unavailable for new Incoming Webhooks)") + flags.StringVar(&c.conf.Channel, "channel", "", "specify channel (unavailable for new Incoming Webhooks)") + flags.StringVar(&c.conf.ChannelID, "channel-id", "", "specify channel id (for uploading a file)") flags.StringVar(&c.conf.SlackURL, "slack-url", "", "slack url (Incoming Webhooks URL)") flags.StringVar(&c.conf.Token, "token", "", "token (for uploading to snippet)") flags.StringVar(&c.conf.Username, "username", "", "specify username (unavailable for new Incoming Webhooks)") @@ -77,7 +78,8 @@ func (c *CLI) Run(args []string) int { flags.DurationVar(&c.conf.Duration, "interval", time.Second, "interval") flags.StringVar(&tomlFile, "c", "", "config file name") flags.StringVar(&uploadFilename, "filename", "", "specify a file name (for uploading to snippet)") - flags.StringVar(&filetype, "filetype", "", "specify a filetype (for uploading to snippet)") + flags.StringVar(&filetype, "filetype", "", "[compatible] specify a filetype for uploading to snippet. This option is maintained for compatibility. Please use -snippet-type instead.") + flags.StringVar(&filetype, "snippet-type", "", "specify a snippet_type (for uploading to snippet)") flags.BoolVar(&snippetMode, "snippet", false, "switch to snippet uploading mode") @@ -178,10 +180,7 @@ func (c *CLI) Run(args []string) int { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() - channel := c.conf.PrimaryChannel - if channel == "" { - channel = c.conf.Channel - } + channel := c.conf.Channel param := &slack.PostTextParam{ Channel: channel, @@ -216,18 +215,8 @@ func (c *CLI) Run(args []string) int { return ExitCodeOK } -func (c *CLI) uploadSnippet(ctx context.Context, filename, uploadFilename, filetype string) error { - channel := c.conf.PrimaryChannel - if channel == "" { - channel = c.conf.SnippetChannel - } - if channel == "" { - channel = c.conf.Channel - } - - if channel == "" { - return fmt.Errorf("must specify channel for uploading to snippet") - } +func (c *CLI) uploadSnippet(ctx context.Context, filename, uploadFilename, snippetType string) error { + channelID := c.conf.ChannelID var reader io.ReadCloser if filename == "" { @@ -254,12 +243,12 @@ func (c *CLI) uploadSnippet(ctx context.Context, filename, uploadFilename, filet } param := &slack.PostFileParam{ - Channel: channel, - Filename: uploadFilename, - Content: string(content), - Filetype: filetype, + ChannelID: channelID, + Filename: uploadFilename, + SnippetType: snippetType, } - err = c.sClient.PostFile(ctx, param) + + err = c.sClient.PostFile(ctx, param, content) if err != nil { return err } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 1b4e4c7..30875f8 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -9,16 +9,17 @@ import ( "github.com/catatsuy/notify_slack/internal/config" "github.com/catatsuy/notify_slack/internal/slack" + "github.com/google/go-cmp/cmp" ) type fakeSlackClient struct { slack.Slack - FakePostFile func(ctx context.Context, param *slack.PostFileParam) error + FakePostFile func(ctx context.Context, param *slack.PostFileParam, content []byte) error } -func (c *fakeSlackClient) PostFile(ctx context.Context, param *slack.PostFileParam) error { - return c.FakePostFile(ctx, param) +func (c *fakeSlackClient) PostFile(ctx context.Context, param *slack.PostFileParam, content []byte) error { + return c.FakePostFile(ctx, param, content) } func (c *fakeSlackClient) PostText(ctx context.Context, param *slack.PostTextParam) error { @@ -48,33 +49,23 @@ func TestUploadSnippet(t *testing.T) { conf: config.NewConfig(), } - err := cl.uploadSnippet(context.Background(), "", "", "") - want := "must specify channel" - if err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("error = %v; want %q", err, want) - } - - cl.conf.Channel = "normal_channel" - err = cl.uploadSnippet(context.Background(), "testdata/nofile.txt", "", "") - want = "no such file or directory" + cl.conf.ChannelID = "C12345678" + err := cl.uploadSnippet(context.Background(), "testdata/nofile.txt", "", "") + want := "no such file or directory" if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("error = %v; want %q", err, want) } cl.sClient = &fakeSlackClient{ - FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error { - if param.Channel != cl.conf.Channel { - t.Errorf("expected %s; got %s", cl.conf.Channel, param.Channel) - } - + FakePostFile: func(ctx context.Context, param *slack.PostFileParam, content []byte) error { expectedFilename := "testdata/upload.txt" if param.Filename != expectedFilename { t.Errorf("expected %s; got %s", expectedFilename, param.Filename) } expectedContent := "upload_test\n" - if param.Content != expectedContent { - t.Errorf("expected %q; got %q", expectedContent, param.Content) + if diff := cmp.Diff(expectedContent, string(content)); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) } return nil @@ -87,19 +78,15 @@ func TestUploadSnippet(t *testing.T) { } cl.sClient = &fakeSlackClient{ - FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error { - if param.Channel != cl.conf.Channel { - t.Errorf("expected %s; got %s", cl.conf.Channel, param.Channel) - } - + FakePostFile: func(ctx context.Context, param *slack.PostFileParam, content []byte) error { expectedFilename := "overwrite.txt" if param.Filename != expectedFilename { t.Errorf("expected %s; got %s", expectedFilename, param.Filename) } expectedContent := "upload_test\n" - if param.Content != expectedContent { - t.Errorf("expected %q; got %q", expectedContent, param.Content) + if diff := cmp.Diff(expectedContent, string(content)); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) } return nil @@ -112,9 +99,9 @@ func TestUploadSnippet(t *testing.T) { } cl.sClient = &fakeSlackClient{ - FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error { - if param.Channel != cl.conf.Channel { - t.Errorf("expected %s; got %s", cl.conf.Channel, param.Channel) + FakePostFile: func(ctx context.Context, param *slack.PostFileParam, content []byte) error { + if param.ChannelID != cl.conf.ChannelID { + t.Errorf("expected %s; got %s", cl.conf.ChannelID, param.ChannelID) } expectedFilename := "overwrite.txt" @@ -122,75 +109,21 @@ func TestUploadSnippet(t *testing.T) { t.Errorf("expected %s; got %s", expectedFilename, param.Filename) } - expectedContent := "upload_test\n" - if param.Content != expectedContent { - t.Errorf("expected %q; got %q", expectedContent, param.Content) - } - - expectedFiletype := "diff" - if param.Filetype != expectedFiletype { - t.Errorf("expected %s; got %s", expectedFiletype, param.Filetype) - } - - return nil - }, - } - - err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "overwrite.txt", "diff") - if err != nil { - t.Errorf("expected nil; got %v", err) - } - - cl.conf.SnippetChannel = "snippet_channel" - - cl.sClient = &fakeSlackClient{ - FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error { - if param.Channel != cl.conf.SnippetChannel { - t.Errorf("expected %s; got %s", cl.conf.SnippetChannel, param.Channel) - } - - expectedFilename := "testdata/upload.txt" - if param.Filename != expectedFilename { - t.Errorf("expected %s; got %s", expectedFilename, param.Filename) - } - - expectedContent := "upload_test\n" - if param.Content != expectedContent { - t.Errorf("expected %q; got %q", expectedContent, param.Content) - } - - return nil - }, - } - - err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "", "") - if err != nil { - t.Errorf("expected nil; got %v", err) - } - - cl.conf.PrimaryChannel = "primary_channel" - - cl.sClient = &fakeSlackClient{ - FakePostFile: func(ctx context.Context, param *slack.PostFileParam) error { - if param.Channel != cl.conf.PrimaryChannel { - t.Errorf("expected %s; got %s", cl.conf.PrimaryChannel, param.Channel) - } - - expectedFilename := "testdata/upload.txt" - if param.Filename != expectedFilename { - t.Errorf("expected %s; got %s", expectedFilename, param.Filename) + expectedSnippetType := "diff" + if param.SnippetType != expectedSnippetType { + t.Errorf("expected %s; got %s", expectedSnippetType, param.SnippetType) } expectedContent := "upload_test\n" - if param.Content != expectedContent { - t.Errorf("expected %q; got %q", expectedContent, param.Content) + if diff := cmp.Diff(expectedContent, string(content)); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) } return nil }, } - err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "", "") + err = cl.uploadSnippet(context.Background(), "testdata/upload.txt", "overwrite.txt", "diff") if err != nil { t.Errorf("expected nil; got %v", err) } diff --git a/internal/config/config.go b/internal/config/config.go index e078281..bbf41b4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,9 +16,9 @@ var ( type Config struct { SlackURL string Token string - PrimaryChannel string Channel string SnippetChannel string + ChannelID string Username string IconEmoji string Duration time.Duration @@ -42,7 +42,13 @@ func (c *Config) LoadEnv() error { } if c.SnippetChannel == "" { - c.SnippetChannel = os.Getenv("NOTIFY_SLACK_SNIPPET_CHANNEL") + if os.Getenv("NOTIFY_SLACK_SNIPPET_CHANNEL") != "" { + return fmt.Errorf("the NOTIFY_SLACK_SNIPPET_CHANNEL option is deprecated") + } + } + + if c.ChannelID == "" { + c.ChannelID = os.Getenv("NOTIFY_SLACK_CHANNEL_ID") } if c.Username == "" { @@ -70,6 +76,7 @@ type slackConfig struct { Token string Channel string SnippetChannel string `toml:"snippet_channel"` + ChannelID string `toml:"channel_id"` Username string IconEmoji string `toml:"icon_emoji"` Interval string @@ -109,16 +116,19 @@ func (c *Config) LoadTOML(filename string) error { c.Channel = slackConfig.Channel } } - if c.SnippetChannel == "" { - if slackConfig.SnippetChannel != "" { - c.SnippetChannel = slackConfig.SnippetChannel - } - } if c.Username == "" { if slackConfig.Username != "" { c.Username = slackConfig.Username } } + if slackConfig.SnippetChannel != "" { + return fmt.Errorf("the snippet_channel option is deprecated") + } + if c.ChannelID == "" { + if slackConfig.ChannelID != "" { + c.ChannelID = slackConfig.ChannelID + } + } if c.IconEmoji == "" { if slackConfig.IconEmoji != "" { c.IconEmoji = slackConfig.IconEmoji diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3239e10..93271de 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -3,6 +3,7 @@ package config_test import ( "os" "path/filepath" + "strings" "testing" "time" @@ -27,9 +28,9 @@ func TestLoadTOML(t *testing.T) { if c.Channel != expectedChannel { t.Errorf("got %s, want %s", c.Channel, expectedChannel) } - expectedSnippetChannel := "#general" - if c.SnippetChannel != expectedSnippetChannel { - t.Errorf("got %s, want %s", c.SnippetChannel, expectedSnippetChannel) + expectedChannelID := "C12345678" + if c.ChannelID != expectedChannelID { + t.Errorf("got %s, want %s", c.ChannelID, expectedChannelID) } expectedUsername := "deploy!" if c.Username != expectedUsername { @@ -45,11 +46,24 @@ func TestLoadTOML(t *testing.T) { } } +func TestLoadTOML_Deprecated(t *testing.T) { + c := NewConfig() + err := c.LoadTOML("./testdata/config_deprecated.toml") + if err == nil { + t.Fatal("expected error, but got nil") + } + + expected := "the snippet_channel option is deprecated" + if !strings.Contains(err.Error(), expected) { + t.Errorf("got %s, want %s", err.Error(), expected) + } +} + func TestLoadEnv(t *testing.T) { expectedSlackURL := "https://hooks.slack.com/aaaaa" expectedToken := "xoxp-token" expectedChannel := "#test" - expectedSnippetChannel := "#general" + expectedChannelID := "C12345678" expectedUsername := "deploy!" expectedIconEmoji := ":rocket:" expectedIntervalStr := "2s" @@ -58,7 +72,7 @@ func TestLoadEnv(t *testing.T) { t.Setenv("NOTIFY_SLACK_WEBHOOK_URL", expectedSlackURL) t.Setenv("NOTIFY_SLACK_TOKEN", expectedToken) t.Setenv("NOTIFY_SLACK_CHANNEL", expectedChannel) - t.Setenv("NOTIFY_SLACK_SNIPPET_CHANNEL", expectedSnippetChannel) + t.Setenv("NOTIFY_SLACK_CHANNEL_ID", expectedChannelID) t.Setenv("NOTIFY_SLACK_USERNAME", expectedUsername) t.Setenv("NOTIFY_SLACK_ICON_EMOJI", expectedIconEmoji) t.Setenv("NOTIFY_SLACK_INTERVAL", expectedIntervalStr) @@ -81,8 +95,8 @@ func TestLoadEnv(t *testing.T) { t.Errorf("got %s, want %s", c.Channel, expectedChannel) } - if c.SnippetChannel != expectedSnippetChannel { - t.Errorf("got %s, want %s", c.SnippetChannel, expectedSnippetChannel) + if c.ChannelID != expectedChannelID { + t.Errorf("got %s, want %s", c.ChannelID, expectedChannelID) } if c.Username != expectedUsername { @@ -98,6 +112,35 @@ func TestLoadEnv(t *testing.T) { } } +func TestLoadEnv_Deprecated(t *testing.T) { + expectedSlackURL := "https://hooks.slack.com/aaaaa" + expectedToken := "xoxp-token" + expectedChannel := "#test" + expectedSnippetChannel := "#general" + expectedUsername := "deploy!" + expectedIconEmoji := ":rocket:" + expectedIntervalStr := "2s" + + t.Setenv("NOTIFY_SLACK_WEBHOOK_URL", expectedSlackURL) + t.Setenv("NOTIFY_SLACK_TOKEN", expectedToken) + t.Setenv("NOTIFY_SLACK_CHANNEL", expectedChannel) + t.Setenv("NOTIFY_SLACK_SNIPPET_CHANNEL", expectedSnippetChannel) + t.Setenv("NOTIFY_SLACK_USERNAME", expectedUsername) + t.Setenv("NOTIFY_SLACK_ICON_EMOJI", expectedIconEmoji) + t.Setenv("NOTIFY_SLACK_INTERVAL", expectedIntervalStr) + + c := NewConfig() + err := c.LoadEnv() + if err == nil { + t.Fatal("expected error, but got nil") + } + + expected := "the NOTIFY_SLACK_SNIPPET_CHANNEL option is deprecated" + if !strings.Contains(err.Error(), expected) { + t.Errorf("got %s, want %s", err.Error(), expected) + } +} + func TestLoadTOMLFilename(t *testing.T) { baseDir := "./testdata/" defer SetUserHomeDir(baseDir)() diff --git a/internal/config/testdata/config.toml b/internal/config/testdata/config.toml index fe205cd..88e32f5 100644 --- a/internal/config/testdata/config.toml +++ b/internal/config/testdata/config.toml @@ -2,7 +2,7 @@ url = "https://hooks.slack.com/aaaaa" token = "xoxp-token" channel = "#test" -snippet_channel = "#general" +channel_id = "C12345678" username = "deploy!" icon_emoji = ":rocket:" interval = "2s" diff --git a/internal/config/testdata/config_deprecated.toml b/internal/config/testdata/config_deprecated.toml new file mode 100644 index 0000000..fe205cd --- /dev/null +++ b/internal/config/testdata/config_deprecated.toml @@ -0,0 +1,8 @@ +[slack] +url = "https://hooks.slack.com/aaaaa" +token = "xoxp-token" +channel = "#test" +snippet_channel = "#general" +username = "deploy!" +icon_emoji = ":rocket:" +interval = "2s" diff --git a/internal/slack/client.go b/internal/slack/client.go index a4ae46d..82f1faa 100644 --- a/internal/slack/client.go +++ b/internal/slack/client.go @@ -15,7 +15,6 @@ import ( ) var ( - slackFilesUploadURL = "https://slack.com/api/files.upload" filesGetUploadURLExternalURL = "https://slack.com/api/files.getUploadURLExternal" filesCompleteUploadExternalURL = "https://slack.com/api/files.completeUploadExternal" ) @@ -45,10 +44,11 @@ type PostTextParam struct { } type PostFileParam struct { - Channel string - Content string - Filename string - Filetype string + ChannelID string + Filename string + AltText string + Title string + SnippetType string } type GetUploadURLExternalResParam struct { @@ -60,7 +60,7 @@ type GetUploadURLExternalResParam struct { type Slack interface { PostText(ctx context.Context, param *PostTextParam) error - PostFile(ctx context.Context, param *PostFileParam) error + PostFile(ctx context.Context, param *PostFileParam, content []byte) error } func NewClient(urlStr string, logger *slog.Logger) (*Client, error) { @@ -145,60 +145,35 @@ func (c *Client) PostText(ctx context.Context, param *PostTextParam) error { return nil } -type apiFilesUploadRes struct { - OK bool `json:"ok"` -} - -func (c *Client) PostFile(ctx context.Context, param *PostFileParam) error { - if param.Content == "" { - return fmt.Errorf("the content of the file is empty") +func (c *Client) PostFile(ctx context.Context, param *PostFileParam, content []byte) error { + uParam := &GetUploadURLExternalResParam{ + Filename: param.Filename, + Length: len(content), + SnippetType: param.SnippetType, + AltText: param.AltText, } - v := url.Values{} - v.Set("token", c.Token) - v.Set("content", param.Content) - v.Set("filename", param.Filename) - v.Set("channels", param.Channel) - - if param.Filetype != "" { - v.Set("filetype", param.Filetype) - } - - req, err := http.NewRequest("POST", slackFilesUploadURL, strings.NewReader(v.Encode())) - if err != nil { - return err - } - - req = req.WithContext(ctx) - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - res, err := c.HTTPClient.Do(req) + uploadURL, fileID, err := c.GetUploadURLExternalURL(ctx, uParam) if err != nil { - return err + return fmt.Errorf("failed to get upload url: %w", err) } - defer res.Body.Close() - b, err := io.ReadAll(res.Body) + err = c.UploadToURL(ctx, param.Filename, uploadURL, content) if err != nil { - return err + return fmt.Errorf("failed to upload file: %w", err) } - c.Logger.Debug("request", "url", req.URL.String(), "method", req.Method, "header", req.Header, "status", res.StatusCode, "body", b) - - if res.StatusCode != http.StatusOK { - return fmt.Errorf("failed to read res.Body and the status code of the response from slack was not 200; body: %s", b) + cParam := &CompleteUploadExternalParam{ + FileID: fileID, + Title: param.Title, + ChannelID: param.ChannelID, } - apiRes := apiFilesUploadRes{} - err = json.Unmarshal(b, &apiRes) + err = c.CompleteUploadExternal(ctx, cParam) if err != nil { - return fmt.Errorf("response returned from slack is not json: %w", err) + return fmt.Errorf("failed to complete upload: %w", err) } - if !apiRes.OK { - return fmt.Errorf("response has failed; body: %s", b) - } return nil } diff --git a/internal/slack/client_test.go b/internal/slack/client_test.go index 6bd727f..3713cb0 100644 --- a/internal/slack/client_test.go +++ b/internal/slack/client_test.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "reflect" + "strconv" "strings" "testing" @@ -108,8 +109,13 @@ func TestPostText_Fail(t *testing.T) { } muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + b, err := os.ReadFile("testdata/post_text_fail.html") + if err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusNotFound) - http.ServeFile(w, r, "testdata/post_text_fail.html") + w.Write(b) }) c, err := NewClient(testAPIServer.URL, slog.New(slog.NewTextHandler(io.Discard, nil))) @@ -136,19 +142,24 @@ func TestPostFile_Success(t *testing.T) { slackToken := "slack-token" - param := &PostFileParam{ - Channel: "test-channel", - Content: "testtesttest", + param := &GetUploadURLExternalResParam{ Filename: "test.txt", + Length: 100, } - muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") expectedType := "application/x-www-form-urlencoded" if contentType != expectedType { t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType) } + authorization := r.Header.Get("Authorization") + expectedAuth := "Bearer " + slackToken + if authorization != expectedAuth { + t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization) + } + bodyBytes, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) @@ -161,53 +172,115 @@ func TestPostFile_Success(t *testing.T) { } expectedV := url.Values{} - expectedV.Set("token", slackToken) - expectedV.Set("content", param.Content) expectedV.Set("filename", param.Filename) - expectedV.Set("channels", param.Channel) + expectedV.Set("length", strconv.Itoa(param.Length)) - if !reflect.DeepEqual(actualV, expectedV) { - t.Fatalf("expected %q to equal %q", actualV, expectedV) + if diff := cmp.Diff(expectedV, actualV); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) } - http.ServeFile(w, r, "testdata/post_files_upload_ok.json") + http.ServeFile(w, r, "testdata/files_get_upload_url_external_ok.json") }) - defer SetSlackFilesUploadURL(testAPIServer.URL)() + defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")() c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatal(err) } - err = c.PostFile(context.Background(), param) + uploadURL, fileID, err := c.GetUploadURLExternalURL(context.Background(), param) + if err != nil { + t.Fatal(err) + } + + expectedUploadURL := "https://files.slack.com/upload/v1/ABC123456" + if uploadURL != expectedUploadURL { + t.Fatalf("expected %q to equal %q", uploadURL, expectedUploadURL) + } + + expectedFileID := "F123ABC456" + if fileID != expectedFileID { + t.Fatalf("expected %q to equal %q", fileID, expectedFileID) + } +} + +func TestPostFile_FailCallFunc(t *testing.T) { + muxAPI := http.NewServeMux() + testAPIServer := httptest.NewServer(muxAPI) + defer testAPIServer.Close() + + slackToken := "slack-token" + + muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) { + panic("unexpected call") + }) + + defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")() + _, err := NewClientForPostFile("", slog.New(slog.NewTextHandler(io.Discard, nil))) + expectedErrorPart := "provide Slack token" + if err == nil { + t.Fatal("expected error, but nothing was returned") + } else if !strings.Contains(err.Error(), expectedErrorPart) { + t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart) + } + + c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatal(err) } + + _, _, err = c.GetUploadURLExternalURL(context.Background(), nil) + expectedErrorPart = "provide filename and length" + if err == nil { + t.Fatal("expected error, but nothing was returned") + } else if !strings.Contains(err.Error(), expectedErrorPart) { + t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart) + } + + _, _, err = c.GetUploadURLExternalURL(context.Background(), &GetUploadURLExternalResParam{}) + expectedErrorPart = "provide filename" + if err == nil { + t.Fatal("expected error, but nothing was returned") + } else if !strings.Contains(err.Error(), expectedErrorPart) { + t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart) + } + + _, _, err = c.GetUploadURLExternalURL(context.Background(), &GetUploadURLExternalResParam{Filename: "test.txt"}) + expectedErrorPart = "provide length" + if err == nil { + t.Fatal("expected error, but nothing was returned") + } else if !strings.Contains(err.Error(), expectedErrorPart) { + t.Fatalf("expected %q to contain %q", err.Error(), expectedErrorPart) + } } -func TestPostFile_Success_provideFiletype(t *testing.T) { +func TestPostFile_FailAPINotOK(t *testing.T) { muxAPI := http.NewServeMux() testAPIServer := httptest.NewServer(muxAPI) defer testAPIServer.Close() slackToken := "slack-token" - param := &PostFileParam{ - Channel: "test-channel", - Content: "testtesttest", + param := &GetUploadURLExternalResParam{ Filename: "test.txt", - Filetype: "diff", + Length: 100, } - muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") expectedType := "application/x-www-form-urlencoded" if contentType != expectedType { t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType) } + authorization := r.Header.Get("Authorization") + expectedAuth := "Bearer " + slackToken + if authorization != expectedAuth { + t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization) + } + bodyBytes, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) @@ -220,139 +293,191 @@ func TestPostFile_Success_provideFiletype(t *testing.T) { } expectedV := url.Values{} - expectedV.Set("token", slackToken) - expectedV.Set("content", param.Content) expectedV.Set("filename", param.Filename) - expectedV.Set("filetype", param.Filetype) - expectedV.Set("channels", param.Channel) + expectedV.Set("length", strconv.Itoa(param.Length)) + + if diff := cmp.Diff(expectedV, actualV); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) + } - if !reflect.DeepEqual(actualV, expectedV) { - t.Fatalf("expected %q to equal %q", actualV, expectedV) + w.WriteHeader(http.StatusForbidden) + w.Header().Set("Content-Type", "application/json") + + b, err := os.ReadFile("testdata/files_get_upload_url_external_fail.json") + if err != nil { + t.Fatal(err) } - http.ServeFile(w, r, "testdata/post_files_upload_ok.json") + w.Write(b) }) - defer SetSlackFilesUploadURL(testAPIServer.URL)() + defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")() c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatal(err) } - err = c.PostFile(context.Background(), param) + _, _, err = c.GetUploadURLExternalURL(context.Background(), param) - if err != nil { - t.Fatal(err) + if err == nil { + t.Fatal("expected error, but nothing was returned") + } else { + expected := "status code: 403" + if !strings.Contains(err.Error(), expected) { + t.Errorf("expected %q to contain %q", err.Error(), expected) + } + + expectedBodyPart := `"invalid_auth"` + if !strings.Contains(err.Error(), expectedBodyPart) { + t.Errorf("expected %q to contain %q", err.Error(), expectedBodyPart) + } } } -func TestPostFile_FailNotOk(t *testing.T) { +func TestPostFile_FailAPIStatusOK(t *testing.T) { muxAPI := http.NewServeMux() testAPIServer := httptest.NewServer(muxAPI) defer testAPIServer.Close() slackToken := "slack-token" - param := &PostFileParam{ - Channel: "test-channel", - Content: "testtesttest", + param := &GetUploadURLExternalResParam{ Filename: "test.txt", + Length: 100, } - muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "testdata/post_files_upload_fail.json") - }) - - defer SetSlackFilesUploadURL(testAPIServer.URL)() + muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + expectedType := "application/x-www-form-urlencoded" + if contentType != expectedType { + t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType) + } - c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil))) - if err != nil { - t.Fatal(err) - } + authorization := r.Header.Get("Authorization") + expectedAuth := "Bearer " + slackToken + if authorization != expectedAuth { + t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization) + } - err = c.PostFile(context.Background(), param) + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + defer r.Body.Close() - if err == nil { - t.Fatal("expected error, but nothing was returned") - } + actualV, err := url.ParseQuery(string(bodyBytes)) + if err != nil { + t.Fatal(err) + } - expected := `response has failed; body: {"ok":false,"error":"invalid_auth"}` - if !strings.Contains(err.Error(), expected) { - t.Fatalf("expected %q to contain %q", err.Error(), expected) - } -} + expectedV := url.Values{} + expectedV.Set("filename", param.Filename) + expectedV.Set("length", strconv.Itoa(param.Length)) -func TestPostFile_FailNotResponseStatusCodeNotOK(t *testing.T) { - muxAPI := http.NewServeMux() - testAPIServer := httptest.NewServer(muxAPI) - defer testAPIServer.Close() + if diff := cmp.Diff(expectedV, actualV); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) + } - slackToken := "slack-token" + w.Header().Set("Content-Type", "application/json") - param := &PostFileParam{ - Channel: "test-channel", - Content: "testtesttest", - Filename: "test.txt", - } + b, err := os.ReadFile("testdata/files_get_upload_url_external_fail_invalid_arguments.json") + if err != nil { + t.Fatal(err) + } - muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - http.ServeFile(w, r, "testdata/post_files_upload_fail.json") + w.Write(b) }) - defer SetSlackFilesUploadURL(testAPIServer.URL)() + defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")() c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatal(err) } - err = c.PostFile(context.Background(), param) + _, _, err = c.GetUploadURLExternalURL(context.Background(), param) if err == nil { t.Fatal("expected error, but nothing was returned") - } + } else { + expected := "response has failed" + if !strings.Contains(err.Error(), expected) { + t.Errorf("expected %q to contain %q", err.Error(), expected) + } - expected := `failed to read res.Body and the status code of the response from slack was not 200; body: {"ok":false,"error":"invalid_auth"}` - if !strings.Contains(err.Error(), expected) { - t.Fatalf("expected %q to contain %q", err.Error(), expected) + expectedBodyPart := `"invalid_arguments"` + if !strings.Contains(err.Error(), expectedBodyPart) { + t.Errorf("expected %q to contain %q", err.Error(), expectedBodyPart) + } } } -func TestPostFile_FailNotJSON(t *testing.T) { +func TestPostFile_FailBrokenJSON(t *testing.T) { muxAPI := http.NewServeMux() testAPIServer := httptest.NewServer(muxAPI) defer testAPIServer.Close() slackToken := "slack-token" - param := &PostFileParam{ - Channel: "test-channel", - Content: "testtesttest", + param := &GetUploadURLExternalResParam{ Filename: "test.txt", + Length: 100, } - muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "testdata/post_text_fail.html") + muxAPI.HandleFunc("/api/files.getUploadURLExternal", func(w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + expectedType := "application/x-www-form-urlencoded" + if contentType != expectedType { + t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType) + } + + authorization := r.Header.Get("Authorization") + expectedAuth := "Bearer " + slackToken + if authorization != expectedAuth { + t.Fatalf("Authorization expected %s, but %s", expectedAuth, authorization) + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + defer r.Body.Close() + + actualV, err := url.ParseQuery(string(bodyBytes)) + if err != nil { + t.Fatal(err) + } + + expectedV := url.Values{} + expectedV.Set("filename", param.Filename) + expectedV.Set("length", strconv.Itoa(param.Length)) + + if diff := cmp.Diff(expectedV, actualV); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) + } + + w.Header().Set("Content-Type", "text/plain") + + w.Write([]byte("this is not json")) }) - defer SetSlackFilesUploadURL(testAPIServer.URL)() + defer SetFilesGetUploadURLExternalURL(testAPIServer.URL + "/api/files.getUploadURLExternal")() c, err := NewClientForPostFile(slackToken, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatal(err) } - err = c.PostFile(context.Background(), param) + _, _, err = c.GetUploadURLExternalURL(context.Background(), param) if err == nil { t.Fatal("expected error, but nothing was returned") - } - - expected := `response returned from slack is not json` - if !strings.Contains(err.Error(), expected) { - t.Fatalf("expected %q to contain %q", err.Error(), expected) + } else { + expectedBodyPart := `this is not json` + if !strings.Contains(err.Error(), expectedBodyPart) { + t.Errorf("expected %q to contain %q", err.Error(), expectedBodyPart) + } } } @@ -488,12 +613,12 @@ func TestCompleteUploadExternal_Success(t *testing.T) { t.Fatal(err) } - params := &CompleteUploadExternalParam{ + param := &CompleteUploadExternalParam{ FileID: "file-id", Title: "file-title", ChannelID: "C0NF841BK", } - err = c.CompleteUploadExternal(context.Background(), params) + err = c.CompleteUploadExternal(context.Background(), param) if err != nil { t.Fatal(err) } diff --git a/internal/slack/export_test.go b/internal/slack/export_test.go index c17dc6b..70a70a6 100644 --- a/internal/slack/export_test.go +++ b/internal/slack/export_test.go @@ -1,13 +1,5 @@ package slack -func SetSlackFilesUploadURL(u string) (resetFunc func()) { - var tmp string - tmp, slackFilesUploadURL = slackFilesUploadURL, u - return func() { - slackFilesUploadURL = tmp - } -} - func SetFilesGetUploadURLExternalURL(u string) (resetFunc func()) { var tmp string tmp, filesGetUploadURLExternalURL = filesGetUploadURLExternalURL, u