package slack_test import ( "errors" "fmt" "log" "net/url" "os" "strings" "testing" "github.com/jarcoal/httpmock" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "github.com/onsi/gomega/format" "github.com/nicholas-fedor/shoutrrr/internal/testutils" "github.com/nicholas-fedor/shoutrrr/pkg/services/slack" ) const ( TestWebhookURL = "https://hooks.slack.com/services/AAAAAAAAA/BBBBBBBBB/123456789123456789123456" ) func TestSlack(t *testing.T) { format.CharactersAroundMismatchToInclude = 20 gomega.RegisterFailHandler(ginkgo.Fail) ginkgo.RunSpecs(t, "Shoutrrr Slack Suite") } var ( service *slack.Service envSlackURL *url.URL logger *log.Logger _ = ginkgo.BeforeSuite(func() { service = &slack.Service{} logger = log.New(ginkgo.GinkgoWriter, "Test", log.LstdFlags) envSlackURL, _ = url.Parse(os.Getenv("SHOUTRRR_SLACK_URL")) }) ) var _ = ginkgo.Describe("the slack service", func() { ginkgo.When("running integration tests", func() { ginkgo.It("should not error out", func() { if envSlackURL.String() == "" { return } serviceURL, _ := url.Parse(envSlackURL.String()) err := service.Initialize(serviceURL, testutils.TestLogger()) gomega.Expect(err).NotTo(gomega.HaveOccurred()) err = service.Send("This is an integration test message", nil) gomega.Expect(err).NotTo(gomega.HaveOccurred()) }) ginkgo.It("returns the correct service identifier", func() { gomega.Expect(service.GetID()).To(gomega.Equal("slack")) }) }) // xoxb:123456789012-1234567890123-4mt0t4l1YL3g1T5L4cK70k3N ginkgo.When("given a token with a malformed part", func() { ginkgo.It("should return an error if part A is not 9 letters", func() { expectErrorMessageGivenURL( slack.ErrInvalidToken, "slack://lol@12345678/123456789/123456789123456789123456", ) }) ginkgo.It("should return an error if part B is not 9 letters", func() { expectErrorMessageGivenURL( slack.ErrInvalidToken, "slack://lol@123456789/12345678/123456789123456789123456", ) }) ginkgo.It("should return an error if part C is not 24 letters", func() { expectErrorMessageGivenURL( slack.ErrInvalidToken, "slack://123456789/123456789/12345678912345678912345", ) }) }) ginkgo.When("given a token missing a part", func() { ginkgo.It("should return an error if the missing part is A", func() { expectErrorMessageGivenURL( slack.ErrInvalidToken, "slack://lol@/123456789/123456789123456789123456", ) }) ginkgo.It("should return an error if the missing part is B", func() { expectErrorMessageGivenURL(slack.ErrInvalidToken, "slack://lol@123456789//123456789") }) ginkgo.It("should return an error if the missing part is C", func() { expectErrorMessageGivenURL(slack.ErrInvalidToken, "slack://lol@123456789/123456789/") }) }) ginkgo.Describe("the slack config", func() { ginkgo.When("parsing the configuration URL", func() { ginkgo.When("given a config using the legacy format", func() { ginkgo.It("should be converted to the new format after de-/serialization", func() { oldURL := "slack://testbot@AAAAAAAAA/BBBBBBBBB/123456789123456789123456?color=3f00fe&title=Test+title" newURL := "slack://hook:AAAAAAAAA-BBBBBBBBB-123456789123456789123456@webhook?botname=testbot&color=3f00fe&title=Test+title" config := &slack.Config{} err := config.SetURL(testutils.URLMust(oldURL)) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "verifying") gomega.Expect(config.GetURL().String()).To(gomega.Equal(newURL)) }) }) }) ginkgo.When("the URL contains an invalid property", func() { testURL := testutils.URLMust( "slack://hook:AAAAAAAAA-BBBBBBBBB-123456789123456789123456@webhook?bass=dirty", ) err := (&slack.Config{}).SetURL(testURL) gomega.Expect(err).To(gomega.HaveOccurred()) }) ginkgo.It("should be identical after de-/serialization", func() { testURL := "slack://hook:AAAAAAAAA-BBBBBBBBB-123456789123456789123456@webhook?botname=testbot&color=3f00fe&title=Test+title" config := &slack.Config{} err := config.SetURL(testutils.URLMust(testURL)) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "verifying") outputURL := config.GetURL() gomega.Expect(outputURL.String()).To(gomega.Equal(testURL)) }) ginkgo.When("generating a config object", func() { ginkgo.It( "should use the default botname if the argument list contains three strings", func() { slackURL, _ := url.Parse("slack://AAAAAAAAA/BBBBBBBBB/123456789123456789123456") config, configError := slack.CreateConfigFromURL(slackURL) gomega.Expect(configError).NotTo(gomega.HaveOccurred()) gomega.Expect(config.BotName).To(gomega.BeEmpty()) }, ) ginkgo.It("should set the botname if the argument list is three", func() { slackURL, _ := url.Parse( "slack://testbot@AAAAAAAAA/BBBBBBBBB/123456789123456789123456", ) config, configError := slack.CreateConfigFromURL(slackURL) gomega.Expect(configError).NotTo(gomega.HaveOccurred()) gomega.Expect(config.BotName).To(gomega.Equal("testbot")) }) ginkgo.It("should return an error if the argument list is shorter than three", func() { slackURL, _ := url.Parse("slack://AAAAAAAA") _, configError := slack.CreateConfigFromURL(slackURL) gomega.Expect(configError).To(gomega.HaveOccurred()) }) }) ginkgo.When("getting credentials from token", func() { ginkgo.It("should return a valid webhook URL for the given token", func() { token := tokenMust("AAAAAAAAA/BBBBBBBBB/123456789123456789123456") gomega.Expect(token.WebhookURL()).To(gomega.Equal(TestWebhookURL)) }) ginkgo.It( "should return a valid authorization header value for the given token", func() { token := tokenMust("xoxb:AAAAAAAAA-BBBBBBBBB-123456789123456789123456") expected := "Bearer xoxb-AAAAAAAAA-BBBBBBBBB-123456789123456789123456" gomega.Expect(token.Authorization()).To(gomega.Equal(expected)) }, ) }) }) ginkgo.Describe("creating the payload", func() { ginkgo.Describe("the icon fields", func() { payload := slack.MessagePayload{} ginkgo.It("should set IconURL when the configured icon looks like an URL", func() { payload.SetIcon("https://example.com/logo.png") gomega.Expect(payload.IconURL).To(gomega.Equal("https://example.com/logo.png")) gomega.Expect(payload.IconEmoji).To(gomega.BeEmpty()) }) ginkgo.It( "should set IconEmoji when the configured icon does not look like an URL", func() { payload.SetIcon("tanabata_tree") gomega.Expect(payload.IconEmoji).To(gomega.Equal("tanabata_tree")) gomega.Expect(payload.IconURL).To(gomega.BeEmpty()) }, ) ginkgo.It("should clear both fields when icon is empty", func() { payload.SetIcon("") gomega.Expect(payload.IconEmoji).To(gomega.BeEmpty()) gomega.Expect(payload.IconURL).To(gomega.BeEmpty()) }) }) ginkgo.When("when more than 99 lines are being sent", func() { ginkgo.It("should append the exceeding lines to the last attachment", func() { config := slack.Config{} sb := strings.Builder{} for i := 1; i <= 110; i++ { sb.WriteString(fmt.Sprintf("Line %d\n", i)) } payload := slack.CreateJSONPayload(&config, sb.String()).(slack.MessagePayload) atts := payload.Attachments fmt.Fprint( ginkgo.GinkgoWriter, "\nLines: ", len(atts), " Last: ", atts[len(atts)-1], "\n", ) gomega.Expect(atts).To(gomega.HaveLen(100)) gomega.Expect(atts[len(atts)-1].Text).To(gomega.ContainSubstring("Line 110")) }) }) ginkgo.When("when the last message line ends with a newline", func() { ginkgo.It("should not send an empty attachment", func() { payload := slack.CreateJSONPayload(&slack.Config{}, "One\nTwo\nThree\n").(slack.MessagePayload) atts := payload.Attachments gomega.Expect(atts[len(atts)-1].Text).NotTo(gomega.BeEmpty()) }) }) }) ginkgo.Describe("sending the payload", func() { ginkgo.When("sending via webhook URL", func() { var err error ginkgo.BeforeEach(func() { httpmock.Activate() }) ginkgo.AfterEach(func() { httpmock.DeactivateAndReset() }) ginkgo.It("should not report an error if the server accepts the payload", func() { serviceURL, _ := url.Parse( "slack://testbot@AAAAAAAAA/BBBBBBBBB/123456789123456789123456", ) err = service.Initialize(serviceURL, logger) gomega.Expect(err).NotTo(gomega.HaveOccurred()) httpmock.RegisterResponder( "POST", TestWebhookURL, httpmock.NewStringResponder(200, ""), ) err = service.Send("Message", nil) gomega.Expect(err).NotTo(gomega.HaveOccurred()) }) ginkgo.It("should not panic if an error occurs when sending the payload", func() { serviceURL, _ := url.Parse( "slack://testbot@AAAAAAAAA/BBBBBBBBB/123456789123456789123456", ) err = service.Initialize(serviceURL, logger) gomega.Expect(err).NotTo(gomega.HaveOccurred()) httpmock.RegisterResponder( "POST", TestWebhookURL, httpmock.NewErrorResponder(errors.New("dummy error")), ) err = service.Send("Message", nil) gomega.Expect(err).To(gomega.HaveOccurred()) }) }) ginkgo.When("sending via bot API", func() { var err error ginkgo.BeforeEach(func() { httpmock.Activate() }) ginkgo.AfterEach(func() { httpmock.DeactivateAndReset() }) ginkgo.It("should not report an error if the server accepts the payload", func() { serviceURL := testutils.URLMust( "slack://xoxb:123456789012-1234567890123-4mt0t4l1YL3g1T5L4cK70k3N@C0123456789", ) err = service.Initialize(serviceURL, logger) gomega.Expect(err).NotTo(gomega.HaveOccurred()) targetURL := "https://slack.com/api/chat.postMessage" httpmock.RegisterResponder( "POST", targetURL, testutils.JSONRespondMust(200, slack.APIResponse{ Ok: true, }), ) err = service.Send("Message", nil) gomega.Expect(err).NotTo(gomega.HaveOccurred()) }) ginkgo.It("should not panic if an error occurs when sending the payload", func() { serviceURL := testutils.URLMust( "slack://xoxb:123456789012-1234567890123-4mt0t4l1YL3g1T5L4cK70k3N@C0123456789", ) err = service.Initialize(serviceURL, logger) gomega.Expect(err).NotTo(gomega.HaveOccurred()) targetURL := "https://slack.com/api/chat.postMessage" httpmock.RegisterResponder( "POST", targetURL, testutils.JSONRespondMust(200, slack.APIResponse{ Error: "someone turned off the internet", }), ) err = service.Send("Message", nil) gomega.Expect(err).To(gomega.HaveOccurred()) }) }) }) }) func tokenMust(rawToken string) *slack.Token { token, err := slack.ParseToken(rawToken) gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) return token } func expectErrorMessageGivenURL(expected error, rawURL string) { err := service.Initialize(testutils.URLMust(rawURL), testutils.TestLogger()) gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred()) gomega.ExpectWithOffset(1, err).To(gomega.Equal(expected)) }