github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/command_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/mattermost/mattermost-server/v5/model"
    20  	"github.com/mattermost/mattermost-server/v5/services/httpservice"
    21  )
    22  
    23  func TestMoveCommand(t *testing.T) {
    24  	th := Setup(t)
    25  	defer th.TearDown()
    26  
    27  	sourceTeam := th.CreateTeam()
    28  	targetTeam := th.CreateTeam()
    29  
    30  	command := &model.Command{}
    31  	command.CreatorId = model.NewId()
    32  	command.Method = model.COMMAND_METHOD_POST
    33  	command.TeamId = sourceTeam.Id
    34  	command.URL = "http://nowhere.com/"
    35  	command.Trigger = "trigger1"
    36  
    37  	command, err := th.App.CreateCommand(command)
    38  	assert.Nil(t, err)
    39  
    40  	defer func() {
    41  		th.App.PermanentDeleteTeam(sourceTeam)
    42  		th.App.PermanentDeleteTeam(targetTeam)
    43  	}()
    44  
    45  	// Move a command and check the team is updated.
    46  	assert.Nil(t, th.App.MoveCommand(targetTeam, command))
    47  	retrievedCommand, err := th.App.GetCommand(command.Id)
    48  	assert.Nil(t, err)
    49  	assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId)
    50  
    51  	// Move it to the team it's already in. Nothing should change.
    52  	assert.Nil(t, th.App.MoveCommand(targetTeam, command))
    53  	retrievedCommand, err = th.App.GetCommand(command.Id)
    54  	assert.Nil(t, err)
    55  	assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId)
    56  }
    57  
    58  func TestCreateCommandPost(t *testing.T) {
    59  	th := Setup(t).InitBasic()
    60  	defer th.TearDown()
    61  
    62  	post := &model.Post{
    63  		ChannelId: th.BasicChannel.Id,
    64  		UserId:    th.BasicUser.Id,
    65  		Type:      model.POST_SYSTEM_GENERIC,
    66  	}
    67  
    68  	resp := &model.CommandResponse{
    69  		Text: "some message",
    70  	}
    71  
    72  	skipSlackParsing := false
    73  	_, err := th.App.CreateCommandPost(post, th.BasicTeam.Id, resp, skipSlackParsing)
    74  	require.NotNil(t, err)
    75  	require.Equal(t, err.Id, "api.context.invalid_param.app_error")
    76  }
    77  
    78  func TestExecuteCommand(t *testing.T) {
    79  	th := Setup(t).InitBasic()
    80  	defer th.TearDown()
    81  
    82  	t.Run("valid tests with different whitespace characters", func(t *testing.T) {
    83  		TestCases := map[string]string{
    84  			"/code happy path":             "    happy path",
    85  			"/code\nnewline path":          "    newline path",
    86  			"/code\n/nDouble newline path": "    /nDouble newline path",
    87  			"/code  double space":          "     double space",
    88  			"/code\ttab":                   "    tab",
    89  		}
    90  
    91  		for TestCase, result := range TestCases {
    92  			args := &model.CommandArgs{
    93  				Command:   TestCase,
    94  				TeamId:    th.BasicTeam.Id,
    95  				ChannelId: th.BasicChannel.Id,
    96  				UserId:    th.BasicUser.Id,
    97  				T:         func(s string, args ...interface{}) string { return s },
    98  			}
    99  			resp, err := th.App.ExecuteCommand(args)
   100  			require.Nil(t, err)
   101  			require.NotNil(t, resp)
   102  
   103  			assert.Equal(t, resp.Text, result)
   104  		}
   105  	})
   106  
   107  	t.Run("missing slash character", func(t *testing.T) {
   108  		argsMissingSlashCharacter := &model.CommandArgs{
   109  			Command: "missing leading slash character",
   110  			T:       func(s string, args ...interface{}) string { return s },
   111  		}
   112  		_, err := th.App.ExecuteCommand(argsMissingSlashCharacter)
   113  		require.Equal(t, "api.command.execute_command.format.app_error", err.Id)
   114  	})
   115  
   116  	t.Run("empty", func(t *testing.T) {
   117  		argsMissingSlashCharacter := &model.CommandArgs{
   118  			Command: "",
   119  			T:       func(s string, args ...interface{}) string { return s },
   120  		}
   121  		_, err := th.App.ExecuteCommand(argsMissingSlashCharacter)
   122  		require.Equal(t, "api.command.execute_command.format.app_error", err.Id)
   123  	})
   124  }
   125  
   126  func TestHandleCommandResponsePost(t *testing.T) {
   127  	th := Setup(t).InitBasic()
   128  	defer th.TearDown()
   129  
   130  	command := &model.Command{}
   131  	args := &model.CommandArgs{
   132  		ChannelId: th.BasicChannel.Id,
   133  		TeamId:    th.BasicTeam.Id,
   134  		UserId:    th.BasicUser.Id,
   135  		RootId:    "",
   136  		ParentId:  "",
   137  	}
   138  
   139  	resp := &model.CommandResponse{
   140  		Type:         model.POST_DEFAULT,
   141  		ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
   142  		Props:        model.StringInterface{"some_key": "some value"},
   143  		Text:         "some message",
   144  	}
   145  
   146  	builtIn := true
   147  
   148  	post, err := th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   149  	assert.Nil(t, err)
   150  	assert.Equal(t, args.ChannelId, post.ChannelId)
   151  	assert.Equal(t, args.RootId, post.RootId)
   152  	assert.Equal(t, args.ParentId, post.ParentId)
   153  	assert.Equal(t, args.UserId, post.UserId)
   154  	assert.Equal(t, resp.Type, post.Type)
   155  	assert.Equal(t, resp.Props, post.GetProps())
   156  	assert.Equal(t, resp.Text, post.Message)
   157  	assert.Nil(t, post.GetProp("override_icon_url"))
   158  	assert.Nil(t, post.GetProp("override_username"))
   159  	assert.Nil(t, post.GetProp("from_webhook"))
   160  
   161  	// Command is not built in, so it is a bot command.
   162  	builtIn = false
   163  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   164  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   165  
   166  	builtIn = true
   167  
   168  	// Channel id is specified by response, it should override the command args value.
   169  	channel := th.CreateChannel(th.BasicTeam)
   170  	resp.ChannelId = channel.Id
   171  	th.AddUserToChannel(th.BasicUser, channel)
   172  
   173  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   174  	assert.Nil(t, err)
   175  	assert.Equal(t, resp.ChannelId, post.ChannelId)
   176  	assert.NotEqual(t, args.ChannelId, post.ChannelId)
   177  
   178  	// Override username config is turned off. No override should occur.
   179  	*th.App.Config().ServiceSettings.EnablePostUsernameOverride = false
   180  	resp.ChannelId = ""
   181  	command.Username = "Command username"
   182  	resp.Username = "Response username"
   183  
   184  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   185  	assert.Nil(t, err)
   186  	assert.Nil(t, post.GetProp("override_username"))
   187  
   188  	*th.App.Config().ServiceSettings.EnablePostUsernameOverride = true
   189  
   190  	// Override username config is turned on. Override username through command property.
   191  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   192  	assert.Nil(t, err)
   193  	assert.Equal(t, command.Username, post.GetProp("override_username"))
   194  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   195  
   196  	command.Username = ""
   197  
   198  	// Override username through response property.
   199  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   200  	assert.Nil(t, err)
   201  	assert.Equal(t, resp.Username, post.GetProp("override_username"))
   202  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   203  
   204  	*th.App.Config().ServiceSettings.EnablePostUsernameOverride = false
   205  
   206  	// Override icon url config is turned off. No override should occur.
   207  	*th.App.Config().ServiceSettings.EnablePostIconOverride = false
   208  	command.IconURL = "Command icon url"
   209  	resp.IconURL = "Response icon url"
   210  
   211  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   212  	assert.Nil(t, err)
   213  	assert.Nil(t, post.GetProp("override_icon_url"))
   214  
   215  	*th.App.Config().ServiceSettings.EnablePostIconOverride = true
   216  
   217  	// Override icon url config is turned on. Override icon url through command property.
   218  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   219  	assert.Nil(t, err)
   220  	assert.Equal(t, command.IconURL, post.GetProp("override_icon_url"))
   221  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   222  
   223  	command.IconURL = ""
   224  
   225  	// Override icon url through response property.
   226  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   227  	assert.Nil(t, err)
   228  	assert.Equal(t, resp.IconURL, post.GetProp("override_icon_url"))
   229  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   230  
   231  	// Test Slack text conversion.
   232  	resp.Text = "<!channel>"
   233  
   234  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   235  	assert.Nil(t, err)
   236  	assert.Equal(t, "@channel", post.Message)
   237  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   238  
   239  	// Test Slack attachments text conversion.
   240  	resp.Attachments = []*model.SlackAttachment{
   241  		{
   242  			Text: "<!here>",
   243  		},
   244  	}
   245  
   246  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   247  	assert.Nil(t, err)
   248  	assert.Equal(t, "@channel", post.Message)
   249  	if assert.Len(t, post.Attachments(), 1) {
   250  		assert.Equal(t, "@here", post.Attachments()[0].Text)
   251  	}
   252  	assert.Equal(t, "true", post.GetProp("from_webhook"))
   253  
   254  	channel = th.CreatePrivateChannel(th.BasicTeam)
   255  	resp.ChannelId = channel.Id
   256  	args.UserId = th.BasicUser2.Id
   257  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   258  
   259  	require.NotNil(t, err)
   260  	require.Equal(t, err.Id, "api.command.command_post.forbidden.app_error")
   261  
   262  	// Test that /code text is not converted with the Slack text conversion.
   263  	command.Trigger = "code"
   264  	resp.ChannelId = ""
   265  	resp.Text = "<test.com|test website>"
   266  	resp.Attachments = []*model.SlackAttachment{
   267  		{
   268  			Text: "<!here>",
   269  		},
   270  	}
   271  
   272  	// set and unset SkipSlackParsing here seems the nicest way as no separate response objects are created for every testcase.
   273  	resp.SkipSlackParsing = true
   274  	post, err = th.App.HandleCommandResponsePost(command, args, resp, builtIn)
   275  	resp.SkipSlackParsing = false
   276  
   277  	assert.Nil(t, err)
   278  	assert.Equal(t, resp.Text, post.Message, "/code text should not be converted to Slack links")
   279  	assert.Equal(t, "<!here>", resp.Attachments[0].Text)
   280  }
   281  
   282  func TestHandleCommandResponse(t *testing.T) {
   283  	th := Setup(t).InitBasic()
   284  	defer th.TearDown()
   285  
   286  	command := &model.Command{}
   287  
   288  	args := &model.CommandArgs{
   289  		Command:   "/invite username",
   290  		UserId:    th.BasicUser.Id,
   291  		ChannelId: th.BasicChannel.Id,
   292  	}
   293  
   294  	resp := &model.CommandResponse{
   295  		Text: "message 1",
   296  		Type: model.POST_SYSTEM_GENERIC,
   297  	}
   298  
   299  	builtIn := true
   300  
   301  	_, err := th.App.HandleCommandResponse(command, args, resp, builtIn)
   302  	require.NotNil(t, err)
   303  	require.Equal(t, err.Id, "api.command.execute_command.create_post_failed.app_error")
   304  
   305  	resp = &model.CommandResponse{
   306  		Text: "message 1",
   307  	}
   308  
   309  	_, err = th.App.HandleCommandResponse(command, args, resp, builtIn)
   310  	assert.Nil(t, err)
   311  
   312  	resp = &model.CommandResponse{
   313  		Text: "message 1",
   314  		ExtraResponses: []*model.CommandResponse{
   315  			{
   316  				Text: "message 2",
   317  			},
   318  			{
   319  				Type: model.POST_SYSTEM_GENERIC,
   320  				Text: "message 3",
   321  			},
   322  		},
   323  	}
   324  
   325  	_, err = th.App.HandleCommandResponse(command, args, resp, builtIn)
   326  	require.NotNil(t, err)
   327  	require.Equal(t, err.Id, "api.command.execute_command.create_post_failed.app_error")
   328  
   329  	resp = &model.CommandResponse{
   330  		ExtraResponses: []*model.CommandResponse{
   331  			{},
   332  			{},
   333  		},
   334  	}
   335  
   336  	_, err = th.App.HandleCommandResponse(command, args, resp, builtIn)
   337  	assert.Nil(t, err)
   338  }
   339  
   340  func TestDoCommandRequest(t *testing.T) {
   341  	th := Setup(t)
   342  	defer th.TearDown()
   343  
   344  	th.App.UpdateConfig(func(cfg *model.Config) {
   345  		cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1")
   346  		cfg.ServiceSettings.EnableCommands = model.NewBool(true)
   347  	})
   348  
   349  	t.Run("with a valid text response", func(t *testing.T) {
   350  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   351  			io.Copy(w, strings.NewReader("Hello, World!"))
   352  		}))
   353  		defer server.Close()
   354  
   355  		_, resp, err := th.App.doCommandRequest(&model.Command{URL: server.URL}, url.Values{})
   356  		require.Nil(t, err)
   357  
   358  		assert.NotNil(t, resp)
   359  		assert.Equal(t, "Hello, World!", resp.Text)
   360  	})
   361  
   362  	t.Run("with a valid json response", func(t *testing.T) {
   363  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   364  			w.Header().Add("Content-Type", "application/json")
   365  
   366  			io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
   367  		}))
   368  		defer server.Close()
   369  
   370  		_, resp, err := th.App.doCommandRequest(&model.Command{URL: server.URL}, url.Values{})
   371  		require.Nil(t, err)
   372  
   373  		assert.NotNil(t, resp)
   374  		assert.Equal(t, "Hello, World!", resp.Text)
   375  	})
   376  
   377  	t.Run("with a large text response", func(t *testing.T) {
   378  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   379  			io.Copy(w, InfiniteReader{})
   380  		}))
   381  		defer server.Close()
   382  
   383  		// Since we limit the length of the response, no error will be returned and resp.Text will be a finite string
   384  
   385  		_, resp, err := th.App.doCommandRequest(&model.Command{URL: server.URL}, url.Values{})
   386  		require.Nil(t, err)
   387  		require.NotNil(t, resp)
   388  	})
   389  
   390  	t.Run("with a large, valid json response", func(t *testing.T) {
   391  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   392  			w.Header().Add("Content-Type", "application/json")
   393  
   394  			io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`)))
   395  		}))
   396  		defer server.Close()
   397  
   398  		_, _, err := th.App.doCommandRequest(&model.Command{URL: server.URL}, url.Values{})
   399  		require.NotNil(t, err)
   400  		require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
   401  	})
   402  
   403  	t.Run("with a large, invalid json response", func(t *testing.T) {
   404  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   405  			w.Header().Add("Content-Type", "application/json")
   406  
   407  			io.Copy(w, InfiniteReader{})
   408  		}))
   409  		defer server.Close()
   410  
   411  		_, _, err := th.App.doCommandRequest(&model.Command{URL: server.URL}, url.Values{})
   412  		require.NotNil(t, err)
   413  		require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
   414  	})
   415  
   416  	t.Run("with a slow response", func(t *testing.T) {
   417  		done := make(chan bool)
   418  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   419  			<-done
   420  			io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
   421  		}))
   422  		defer server.Close()
   423  
   424  		th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = 100 * time.Millisecond
   425  		defer func() {
   426  			th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout
   427  		}()
   428  
   429  		_, _, err := th.App.doCommandRequest(&model.Command{URL: server.URL}, url.Values{})
   430  		require.NotNil(t, err)
   431  		require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
   432  		close(done)
   433  	})
   434  }
   435  
   436  func TestMentionsToTeamMembers(t *testing.T) {
   437  	th := Setup(t).InitBasic()
   438  	defer th.TearDown()
   439  
   440  	otherTeam := th.CreateTeam()
   441  	otherUser := th.CreateUser()
   442  	th.LinkUserToTeam(otherUser, otherTeam)
   443  
   444  	fixture := []struct {
   445  		message     string
   446  		inTeam      string
   447  		expectedMap model.UserMentionMap
   448  	}{
   449  		{
   450  			"",
   451  			th.BasicTeam.Id,
   452  			model.UserMentionMap{},
   453  		},
   454  		{
   455  			"/trigger",
   456  			th.BasicTeam.Id,
   457  			model.UserMentionMap{},
   458  		},
   459  		{
   460  			"/trigger 0 mentions",
   461  			th.BasicTeam.Id,
   462  			model.UserMentionMap{},
   463  		},
   464  		{
   465  			fmt.Sprintf("/trigger 1 valid user @%s", th.BasicUser.Username),
   466  			th.BasicTeam.Id,
   467  			model.UserMentionMap{th.BasicUser.Username: th.BasicUser.Id},
   468  		},
   469  		{
   470  			fmt.Sprintf("/trigger 2 valid users @%s @%s",
   471  				th.BasicUser.Username, th.BasicUser2.Username,
   472  			),
   473  			th.BasicTeam.Id,
   474  			model.UserMentionMap{
   475  				th.BasicUser.Username:  th.BasicUser.Id,
   476  				th.BasicUser2.Username: th.BasicUser2.Id,
   477  			},
   478  		},
   479  		{
   480  			fmt.Sprintf("/trigger 1 user from another team @%s", otherUser.Username),
   481  			th.BasicTeam.Id,
   482  			model.UserMentionMap{},
   483  		},
   484  		{
   485  			fmt.Sprintf("/trigger 2 valid users + 1 from another team @%s @%s @%s",
   486  				th.BasicUser.Username, th.BasicUser2.Username, otherUser.Username,
   487  			),
   488  			th.BasicTeam.Id,
   489  			model.UserMentionMap{
   490  				th.BasicUser.Username:  th.BasicUser.Id,
   491  				th.BasicUser2.Username: th.BasicUser2.Id,
   492  			},
   493  		},
   494  		{
   495  			fmt.Sprintf("/trigger a valid channel ~%s", th.BasicChannel.Name),
   496  			th.BasicTeam.Id,
   497  			model.UserMentionMap{},
   498  		},
   499  		{
   500  			fmt.Sprintf("/trigger channel and mentions ~%s @%s",
   501  				th.BasicChannel.Name, th.BasicUser.Username),
   502  			th.BasicTeam.Id,
   503  			model.UserMentionMap{th.BasicUser.Username: th.BasicUser.Id},
   504  		},
   505  		{
   506  			fmt.Sprintf("/trigger repeated users @%s @%s @%s",
   507  				th.BasicUser.Username, th.BasicUser2.Username, th.BasicUser.Username),
   508  			th.BasicTeam.Id,
   509  			model.UserMentionMap{
   510  				th.BasicUser.Username:  th.BasicUser.Id,
   511  				th.BasicUser2.Username: th.BasicUser2.Id,
   512  			},
   513  		},
   514  	}
   515  
   516  	for _, data := range fixture {
   517  		actualMap := th.App.mentionsToTeamMembers(data.message, data.inTeam)
   518  		require.Equal(t, actualMap, data.expectedMap)
   519  	}
   520  }
   521  
   522  func TestMentionsToPublicChannels(t *testing.T) {
   523  	th := Setup(t).InitBasic()
   524  	defer th.TearDown()
   525  
   526  	otherPublicChannel := th.CreateChannel(th.BasicTeam)
   527  	privateChannel := th.CreatePrivateChannel(th.BasicTeam)
   528  
   529  	fixture := []struct {
   530  		message     string
   531  		inTeam      string
   532  		expectedMap model.ChannelMentionMap
   533  	}{
   534  		{
   535  			"",
   536  			th.BasicTeam.Id,
   537  			model.ChannelMentionMap{},
   538  		},
   539  		{
   540  			"/trigger",
   541  			th.BasicTeam.Id,
   542  			model.ChannelMentionMap{},
   543  		},
   544  		{
   545  			"/trigger 0 mentions",
   546  			th.BasicTeam.Id,
   547  			model.ChannelMentionMap{},
   548  		},
   549  		{
   550  			fmt.Sprintf("/trigger 1 public channel ~%s", th.BasicChannel.Name),
   551  			th.BasicTeam.Id,
   552  			model.ChannelMentionMap{th.BasicChannel.Name: th.BasicChannel.Id},
   553  		},
   554  		{
   555  			fmt.Sprintf("/trigger 2 public channels ~%s ~%s",
   556  				th.BasicChannel.Name, otherPublicChannel.Name,
   557  			),
   558  			th.BasicTeam.Id,
   559  			model.ChannelMentionMap{
   560  				th.BasicChannel.Name:    th.BasicChannel.Id,
   561  				otherPublicChannel.Name: otherPublicChannel.Id,
   562  			},
   563  		},
   564  		{
   565  			fmt.Sprintf("/trigger 1 private channel ~%s", privateChannel.Name),
   566  			th.BasicTeam.Id,
   567  			model.ChannelMentionMap{},
   568  		},
   569  		{
   570  			fmt.Sprintf("/trigger 2 public channel + 1 private ~%s ~%s ~%s",
   571  				th.BasicChannel.Name, otherPublicChannel.Name, privateChannel.Name,
   572  			),
   573  			th.BasicTeam.Id,
   574  			model.ChannelMentionMap{
   575  				th.BasicChannel.Name:    th.BasicChannel.Id,
   576  				otherPublicChannel.Name: otherPublicChannel.Id,
   577  			},
   578  		},
   579  		{
   580  			fmt.Sprintf("/trigger a valid user @%s", th.BasicUser.Username),
   581  			th.BasicTeam.Id,
   582  			model.ChannelMentionMap{},
   583  		},
   584  		{
   585  			fmt.Sprintf("/trigger channel and mentions ~%s @%s",
   586  				th.BasicChannel.Name, th.BasicUser.Username),
   587  			th.BasicTeam.Id,
   588  			model.ChannelMentionMap{th.BasicChannel.Name: th.BasicChannel.Id},
   589  		},
   590  		{
   591  			fmt.Sprintf("/trigger repeated channels ~%s ~%s ~%s",
   592  				th.BasicChannel.Name, otherPublicChannel.Name, th.BasicChannel.Name),
   593  			th.BasicTeam.Id,
   594  			model.ChannelMentionMap{
   595  				th.BasicChannel.Name:    th.BasicChannel.Id,
   596  				otherPublicChannel.Name: otherPublicChannel.Id,
   597  			},
   598  		},
   599  	}
   600  
   601  	for _, data := range fixture {
   602  		actualMap := th.App.mentionsToPublicChannels(data.message, data.inTeam)
   603  		require.Equal(t, actualMap, data.expectedMap)
   604  	}
   605  }