1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2// See LICENSE.txt for license information.
3
4package app
5
6import (
7	"testing"
8	"time"
9
10	"github.com/stretchr/testify/require"
11
12	"github.com/mattermost/mattermost-server/v6/model"
13	"github.com/mattermost/mattermost-server/v6/shared/i18n"
14)
15
16func TestPluginCommand(t *testing.T) {
17	th := Setup(t).InitBasic()
18	defer th.TearDown()
19
20	args := &model.CommandArgs{}
21	args.TeamId = th.BasicTeam.Id
22	args.ChannelId = th.BasicChannel.Id
23	args.UserId = th.BasicUser.Id
24	args.Command = "/plugin"
25
26	t.Run("error before plugin command registered", func(t *testing.T) {
27		_, err := th.App.ExecuteCommand(th.Context, args)
28		require.NotNil(t, err)
29	})
30
31	t.Run("command handled by plugin", func(t *testing.T) {
32		th.App.UpdateConfig(func(cfg *model.Config) {
33			cfg.PluginSettings.Plugins["testloadpluginconfig"] = map[string]interface{}{
34				"TeamId": args.TeamId,
35			}
36		})
37
38		tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
39			package main
40
41			import (
42				"github.com/mattermost/mattermost-server/v6/plugin"
43				"github.com/mattermost/mattermost-server/v6/model"
44			)
45
46			type configuration struct {
47				TeamId string
48			}
49
50			type MyPlugin struct {
51				plugin.MattermostPlugin
52
53				configuration configuration
54			}
55
56			func (p *MyPlugin) OnConfigurationChange() error {
57				if err := p.API.LoadPluginConfiguration(&p.configuration); err != nil {
58					return err
59				}
60
61				return nil
62			}
63
64			func (p *MyPlugin) OnActivate() error {
65				err := p.API.RegisterCommand(&model.Command{
66					TeamId: p.configuration.TeamId,
67					Trigger: "plugin",
68					DisplayName: "Plugin Command",
69					AutoComplete: true,
70					AutoCompleteDesc: "autocomplete",
71				})
72				if err != nil {
73					p.API.LogError("error", "err", err)
74				}
75
76				return err
77			}
78
79			func (p *MyPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
80				return &model.CommandResponse{
81					ResponseType: model.CommandResponseTypeEphemeral,
82					Text: "text",
83				}, nil
84			}
85
86			func main() {
87				plugin.ClientMain(&MyPlugin{})
88			}
89		`}, th.App, th.NewPluginAPI)
90		defer tearDown()
91		require.Len(t, activationErrors, 1)
92		require.Nil(t, nil, activationErrors[0])
93
94		resp, err := th.App.ExecuteCommand(th.Context, args)
95		require.Nil(t, err)
96		require.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
97		require.Equal(t, "text", resp.Text)
98
99		err2 := th.App.DisablePlugin(pluginIDs[0])
100		require.Nil(t, err2)
101
102		commands, err3 := th.App.ListAutocompleteCommands(args.TeamId, i18n.T)
103		require.Nil(t, err3)
104
105		for _, commands := range commands {
106			require.NotEqual(t, "plugin", commands.Trigger)
107		}
108
109		th.App.RemovePlugin(pluginIDs[0])
110	})
111
112	t.Run("re-entrant command registration on config change", func(t *testing.T) {
113		th.App.UpdateConfig(func(cfg *model.Config) {
114			cfg.PluginSettings.Plugins["testloadpluginconfig"] = map[string]interface{}{
115				"TeamId": args.TeamId,
116			}
117		})
118
119		tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
120			package main
121
122			import (
123				"github.com/mattermost/mattermost-server/v6/plugin"
124				"github.com/mattermost/mattermost-server/v6/model"
125			)
126
127			type configuration struct {
128				TeamId string
129			}
130
131			type MyPlugin struct {
132				plugin.MattermostPlugin
133
134				configuration configuration
135			}
136
137			func (p *MyPlugin) OnConfigurationChange() error {
138				p.API.LogInfo("OnConfigurationChange")
139				err := p.API.LoadPluginConfiguration(&p.configuration);
140				if err != nil {
141					return err
142				}
143
144				p.API.LogInfo("About to register")
145				err = p.API.RegisterCommand(&model.Command{
146					TeamId: p.configuration.TeamId,
147					Trigger: "plugin",
148					DisplayName: "Plugin Command",
149					AutoComplete: true,
150					AutoCompleteDesc: "autocomplete",
151				})
152				if err != nil {
153					p.API.LogInfo("Registered, with error", err, err.Error())
154					return err
155				}
156				p.API.LogInfo("Registered, without error")
157				return nil
158			}
159
160			func (p *MyPlugin) ExecuteCommand(c *plugin.Context, commandArgs *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
161				p.API.LogInfo("ExecuteCommand")
162				// Saving the plugin config eventually results in a call to
163				// OnConfigurationChange. This used to deadlock on account of
164				// effectively acquiring a RWLock reentrantly.
165				err := p.API.SavePluginConfig(map[string]interface{}{
166					"TeamId": p.configuration.TeamId,
167				})
168				if err != nil {
169					p.API.LogError("Failed to save plugin config", err, err.Error())
170					return nil, err
171				}
172				p.API.LogInfo("ExecuteCommand, saved plugin config")
173
174				return &model.CommandResponse{
175					ResponseType: model.CommandResponseTypeEphemeral,
176					Text: "text",
177				}, nil
178			}
179
180			func main() {
181				plugin.ClientMain(&MyPlugin{})
182			}
183		`}, th.App, th.NewPluginAPI)
184		defer tearDown()
185
186		require.Len(t, activationErrors, 1)
187		require.Nil(t, nil, activationErrors[0])
188
189		wait := make(chan bool)
190		killed := false
191		go func() {
192			defer close(wait)
193
194			resp, err := th.App.ExecuteCommand(th.Context, args)
195
196			// Ignore if we kill below.
197			if !killed {
198				require.Nil(t, err)
199				require.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
200				require.Equal(t, "text", resp.Text)
201			}
202		}()
203
204		select {
205		case <-wait:
206		case <-time.After(10 * time.Second):
207			killed = true
208		}
209
210		th.App.RemovePlugin(pluginIDs[0])
211		require.False(t, killed, "execute command appears to have deadlocked")
212	})
213
214	t.Run("error after plugin command unregistered", func(t *testing.T) {
215		_, err := th.App.ExecuteCommand(th.Context, args)
216		require.NotNil(t, err)
217	})
218
219	t.Run("plugins can override built-in commands", func(t *testing.T) {
220		th.App.UpdateConfig(func(cfg *model.Config) {
221			cfg.PluginSettings.Plugins["testloadpluginconfig"] = map[string]interface{}{
222				"TeamId": args.TeamId,
223			}
224		})
225
226		tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
227			package main
228
229			import (
230				"github.com/mattermost/mattermost-server/v6/plugin"
231				"github.com/mattermost/mattermost-server/v6/model"
232			)
233
234			type configuration struct {
235				TeamId string
236			}
237
238			type MyPlugin struct {
239				plugin.MattermostPlugin
240
241				configuration configuration
242			}
243
244			func (p *MyPlugin) OnConfigurationChange() error {
245				if err := p.API.LoadPluginConfiguration(&p.configuration); err != nil {
246					return err
247				}
248
249				return nil
250			}
251
252			func (p *MyPlugin) OnActivate() error {
253				err := p.API.RegisterCommand(&model.Command{
254					TeamId: p.configuration.TeamId,
255					Trigger: "code",
256					DisplayName: "Plugin Command",
257					AutoComplete: true,
258					AutoCompleteDesc: "autocomplete",
259				})
260				if err != nil {
261					p.API.LogError("error", "err", err)
262				}
263
264				return err
265			}
266
267			func (p *MyPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
268				return &model.CommandResponse{
269					ResponseType: model.CommandResponseTypeEphemeral,
270					Text: "text",
271				}, nil
272			}
273
274			func main() {
275				plugin.ClientMain(&MyPlugin{})
276			}
277		`}, th.App, th.NewPluginAPI)
278		defer tearDown()
279		require.Len(t, activationErrors, 1)
280		require.Nil(t, nil, activationErrors[0])
281
282		args.Command = "/code"
283		resp, err := th.App.ExecuteCommand(th.Context, args)
284		require.Nil(t, err)
285		require.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType)
286		require.Equal(t, "text", resp.Text)
287
288		th.App.RemovePlugin(pluginIDs[0])
289	})
290	t.Run("plugin has crashed before execution of command", func(t *testing.T) {
291		tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
292			package main
293
294			import (
295				"github.com/mattermost/mattermost-server/v6/plugin"
296				"github.com/mattermost/mattermost-server/v6/model"
297			)
298
299			type MyPlugin struct {
300				plugin.MattermostPlugin
301
302			}
303
304			func (p *MyPlugin) OnActivate() error {
305				err := p.API.RegisterCommand(&model.Command{
306					Trigger: "code",
307				})
308				if err != nil {
309					p.API.LogError("error", "err", err)
310				}
311				panic("Uncaught Error")
312
313				return err
314			}
315
316			func (p *MyPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
317				return &model.CommandResponse{}, nil
318			}
319
320			func main() {
321				plugin.ClientMain(&MyPlugin{})
322			}
323		`}, th.App, th.NewPluginAPI)
324		defer tearDown()
325		require.Len(t, activationErrors, 1)
326		require.Nil(t, nil, activationErrors[0])
327		args.Command = "/code"
328		resp, err := th.App.ExecuteCommand(th.Context, args)
329		require.Nil(t, resp)
330		require.NotNil(t, err)
331		require.Equal(t, err.Id, "model.plugin_command_error.error.app_error")
332		th.App.RemovePlugin(pluginIDs[0])
333	})
334
335	t.Run("plugin has crashed due to the execution of the command", func(t *testing.T) {
336		tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
337			package main
338
339			import (
340				"github.com/mattermost/mattermost-server/v6/plugin"
341				"github.com/mattermost/mattermost-server/v6/model"
342			)
343
344			type MyPlugin struct {
345				plugin.MattermostPlugin
346
347			}
348
349			func (p *MyPlugin) OnActivate() error {
350				err := p.API.RegisterCommand(&model.Command{
351					Trigger: "code",
352				})
353				if err != nil {
354					p.API.LogError("error", "err", err)
355				}
356
357				return err
358			}
359
360			func (p *MyPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
361				panic("Uncaught Error")
362				return &model.CommandResponse{}, nil
363			}
364
365			func main() {
366				plugin.ClientMain(&MyPlugin{})
367			}
368		`}, th.App, th.NewPluginAPI)
369		defer tearDown()
370		require.Len(t, activationErrors, 1)
371		require.Nil(t, nil, activationErrors[0])
372		args.Command = "/code"
373		resp, err := th.App.ExecuteCommand(th.Context, args)
374		require.Nil(t, resp)
375		require.NotNil(t, err)
376		require.Equal(t, err.Id, "model.plugin_command_crash.error.app_error")
377		th.App.RemovePlugin(pluginIDs[0])
378	})
379
380}
381