github.com/levb/mattermost-server@v5.3.1+incompatible/web/webhook_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package web
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"net/http"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/mattermost/mattermost-server/model"
    17  )
    18  
    19  func TestIncomingWebhook(t *testing.T) {
    20  	th := Setup().InitBasic()
    21  	defer th.TearDown()
    22  
    23  	if !th.App.Config().ServiceSettings.EnableIncomingWebhooks {
    24  		_, err := http.Post(ApiClient.Url+"/hooks/123", "", strings.NewReader("123"))
    25  		assert.NotNil(t, err, "should have errored - webhooks turned off")
    26  		return
    27  	}
    28  
    29  	hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
    30  	require.Nil(t, err)
    31  
    32  	url := ApiClient.Url + "/hooks/" + hook.Id
    33  
    34  	tooLongText := ""
    35  	for i := 0; i < 8200; i++ {
    36  		tooLongText += "a"
    37  	}
    38  
    39  	t.Run("WebhookBasics", func(t *testing.T) {
    40  		payload := "payload={\"text\": \"test text\"}"
    41  		resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
    42  		require.Nil(t, err)
    43  		assert.True(t, resp.StatusCode == http.StatusOK)
    44  
    45  		payload = "payload={\"text\": \"\"}"
    46  		resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
    47  		require.Nil(t, err)
    48  		assert.True(t, resp.StatusCode != http.StatusOK, "should have errored - no text to post")
    49  
    50  		payload = "payload={\"text\": \"test text\", \"channel\": \"junk\"}"
    51  		resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
    52  		require.Nil(t, err)
    53  		assert.True(t, resp.StatusCode != http.StatusOK, "should have errored - bad channel")
    54  
    55  		payload = "payload={\"text\": \"test text\"}"
    56  		resp, err = http.Post(ApiClient.Url+"/hooks/abc123", "application/x-www-form-urlencoded", strings.NewReader(payload))
    57  		require.Nil(t, err)
    58  		assert.True(t, resp.StatusCode != http.StatusOK, "should have errored - bad hook")
    59  
    60  		resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\"this is a test\"}"))
    61  		require.Nil(t, err)
    62  		assert.True(t, resp.StatusCode == http.StatusOK)
    63  
    64  		text := `this is a \"test\"
    65  	that contains a newline and a tab`
    66  		resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\""+text+"\"}"))
    67  		require.Nil(t, err)
    68  		assert.True(t, resp.StatusCode == http.StatusOK)
    69  
    70  		resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
    71  		require.Nil(t, err)
    72  		assert.True(t, resp.StatusCode == http.StatusOK)
    73  
    74  		resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"#%s\"}", th.BasicChannel.Name)))
    75  		require.Nil(t, err)
    76  		assert.True(t, resp.StatusCode == http.StatusOK)
    77  
    78  		resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"@%s\"}", th.BasicUser.Username)))
    79  		require.Nil(t, err)
    80  		assert.True(t, resp.StatusCode == http.StatusOK)
    81  
    82  		resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("payload={\"text\":\"this is a test\"}"))
    83  		require.Nil(t, err)
    84  		assert.True(t, resp.StatusCode == http.StatusOK)
    85  
    86  		resp, err = http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("payload={\"text\":\""+text+"\"}"))
    87  		assert.Nil(t, err)
    88  		assert.True(t, resp.StatusCode == http.StatusOK)
    89  
    90  		resp, err = http.Post(url, "application/json", strings.NewReader("{\"text\":\""+tooLongText+"\"}"))
    91  		require.Nil(t, err)
    92  		assert.True(t, resp.StatusCode == http.StatusOK)
    93  
    94  		payloadMultiPart := "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nwebhook-bot\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nthis is a test :tada:\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--"
    95  		resp, err = http.Post(ApiClient.Url+"/hooks/"+hook.Id, "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW", strings.NewReader(payloadMultiPart))
    96  		require.Nil(t, err)
    97  		assert.True(t, resp.StatusCode == http.StatusOK)
    98  	})
    99  
   100  	t.Run("WebhookExperimentalReadOnly", func(t *testing.T) {
   101  		th.App.SetLicense(model.NewTestLicense())
   102  		th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true })
   103  
   104  		// Read only default channel should fail.
   105  		resp, err := http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
   106  		require.Nil(t, err)
   107  		assert.True(t, resp.StatusCode != http.StatusOK)
   108  
   109  		// None-default channel should still work.
   110  		resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
   111  		require.Nil(t, err)
   112  		assert.True(t, resp.StatusCode == http.StatusOK)
   113  
   114  		// System-Admin Owned Hook
   115  		adminHook, err := th.App.CreateIncomingWebhookForChannel(th.SystemAdminUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
   116  		require.Nil(t, err)
   117  		adminUrl := ApiClient.Url + "/hooks/" + adminHook.Id
   118  
   119  		resp, err = http.Post(adminUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
   120  		require.Nil(t, err)
   121  		assert.True(t, resp.StatusCode == http.StatusOK)
   122  
   123  		th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false })
   124  	})
   125  
   126  	t.Run("WebhookAttachments", func(t *testing.T) {
   127  		attachmentPayload := `{
   128  	       "text": "this is a test",
   129  	       "attachments": [
   130  	           {
   131  	               "fallback": "Required plain-text summary of the attachment.",
   132  
   133  	               "color": "#36a64f",
   134  
   135  	               "pretext": "Optional text that appears above the attachment block",
   136  
   137  	               "author_name": "Bobby Tables",
   138  	               "author_link": "http://flickr.com/bobby/",
   139  	               "author_icon": "http://flickr.com/icons/bobby.jpg",
   140  
   141  	               "title": "Slack API Documentation",
   142  	               "title_link": "https://api.slack.com/",
   143  
   144  	               "text": "Optional text that appears within the attachment",
   145  
   146  	               "fields": [
   147  	                   {
   148  	                       "title": "Priority",
   149  	                       "value": "High",
   150  	                       "short": false
   151  	                   }
   152  	               ],
   153  
   154  	               "image_url": "http://my-website.com/path/to/image.jpg",
   155  	               "thumb_url": "http://example.com/path/to/thumb.png"
   156  	           }
   157  	       ]
   158  	   }`
   159  
   160  		resp, err := http.Post(url, "application/json", strings.NewReader(attachmentPayload))
   161  		require.Nil(t, err)
   162  		assert.True(t, resp.StatusCode == http.StatusOK)
   163  
   164  		attachmentPayload = `{
   165  	       "text": "this is a test",
   166  	       "attachments": [
   167  	           {
   168  	               "fallback": "Required plain-text summary of the attachment.",
   169  
   170  	               "color": "#36a64f",
   171  
   172  	               "pretext": "Optional text that appears above the attachment block",
   173  
   174  	               "author_name": "Bobby Tables",
   175  	               "author_link": "http://flickr.com/bobby/",
   176  	               "author_icon": "http://flickr.com/icons/bobby.jpg",
   177  
   178  	               "title": "Slack API Documentation",
   179  	               "title_link": "https://api.slack.com/",
   180  
   181  	               "text": "` + tooLongText + `",
   182  
   183  	               "fields": [
   184  	                   {
   185  	                       "title": "Priority",
   186  	                       "value": "High",
   187  	                       "short": false
   188  	                   }
   189  	               ],
   190  
   191  	               "image_url": "http://my-website.com/path/to/image.jpg",
   192  	               "thumb_url": "http://example.com/path/to/thumb.png"
   193  	           }
   194  	       ]
   195  	   }`
   196  
   197  		resp, err = http.Post(url, "application/json", strings.NewReader(attachmentPayload))
   198  		require.Nil(t, err)
   199  		assert.True(t, resp.StatusCode == http.StatusOK)
   200  	})
   201  
   202  	t.Run("ChannelLockedWebhook", func(t *testing.T) {
   203  		channel, err := th.App.CreateChannel(&model.Channel{TeamId: th.BasicTeam.Id, Name: model.NewId(), DisplayName: model.NewId(), Type: model.CHANNEL_OPEN, CreatorId: th.BasicUser.Id}, true)
   204  		require.Nil(t, err)
   205  
   206  		hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: true})
   207  		require.Nil(t, err)
   208  
   209  		url := ApiClient.Url + "/hooks/" + hook.Id
   210  
   211  		payload := "payload={\"text\": \"test text\"}"
   212  		resp, err2 := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(payload))
   213  		require.Nil(t, err2)
   214  		assert.True(t, resp.StatusCode == http.StatusOK)
   215  
   216  		resp, err2 = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
   217  		require.Nil(t, err2)
   218  		assert.True(t, resp.StatusCode == http.StatusOK)
   219  
   220  		resp, err2 = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", channel.Name)))
   221  		require.Nil(t, err2)
   222  		assert.True(t, resp.StatusCode == http.StatusForbidden)
   223  	})
   224  
   225  	t.Run("DisableWebhooks", func(t *testing.T) {
   226  		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = false })
   227  		resp, err := http.Post(url, "application/json", strings.NewReader("{\"text\":\"this is a test\"}"))
   228  		require.Nil(t, err)
   229  		assert.True(t, resp.StatusCode == http.StatusNotImplemented)
   230  	})
   231  }
   232  
   233  func TestCommandWebhooks(t *testing.T) {
   234  	th := Setup().InitBasic()
   235  	defer th.TearDown()
   236  
   237  	cmd, err := th.App.CreateCommand(&model.Command{
   238  		CreatorId: th.BasicUser.Id,
   239  		TeamId:    th.BasicTeam.Id,
   240  		URL:       "http://nowhere.com",
   241  		Method:    model.COMMAND_METHOD_POST,
   242  		Trigger:   "delayed"})
   243  	require.Nil(t, err)
   244  
   245  	args := &model.CommandArgs{
   246  		TeamId:    th.BasicTeam.Id,
   247  		UserId:    th.BasicUser.Id,
   248  		ChannelId: th.BasicChannel.Id,
   249  	}
   250  
   251  	hook, err := th.App.CreateCommandWebhook(cmd.Id, args)
   252  	if err != nil {
   253  		t.Fatal(err)
   254  	}
   255  
   256  	if resp, _ := http.Post(ApiClient.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusNotFound {
   257  		t.Fatal("expected not-found for non-existent hook")
   258  	}
   259  
   260  	if resp, err := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`)); err != nil || resp.StatusCode != http.StatusBadRequest {
   261  		t.Fatal(err)
   262  	}
   263  
   264  	for i := 0; i < 5; i++ {
   265  		if resp, err := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); err != nil || resp.StatusCode != http.StatusOK {
   266  			t.Fatal(err)
   267  		}
   268  	}
   269  
   270  	if resp, _ := http.Post(ApiClient.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusBadRequest {
   271  		t.Fatal("expected error for sixth usage")
   272  	}
   273  }