// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "os" "strings" "testing" "text/template" "time" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" ) func TestPluginDeadlock(t *testing.T) { mainHelper.Parallel(t) t.Run("Single Plugin", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() pluginPostOnActivate := template.Must(template.New("pluginPostOnActivate").Parse(` package main import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/model" ) type MyPlugin struct { plugin.MattermostPlugin } func (p *MyPlugin) OnActivate() error { _, err := p.API.CreatePost(&model.Post{ UserId: "{{.User.Id}}", ChannelId: "{{.Channel.Id}}", Message: "message", }) if err != nil { panic(err.Error()) } return nil } func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) { if _, from_plugin := post.GetProps()["from_plugin"]; from_plugin { return nil, "" } p.API.CreatePost(&model.Post{ UserId: "{{.User.Id}}", ChannelId: "{{.Channel.Id}}", Message: "message", Props: map[string]any{ "from_plugin": true, }, }) return nil, "" } func main() { plugin.ClientMain(&MyPlugin{}) } `, )) templateData := struct { User *model.User Channel *model.Channel }{ th.BasicUser, th.BasicChannel, } plugins := []string{} pluginTemplates := []*template.Template{ pluginPostOnActivate, } for _, pluginTemplate := range pluginTemplates { b := &strings.Builder{} err := pluginTemplate.Execute(b, templateData) require.NoError(t, err) plugins = append(plugins, b.String()) } done := make(chan bool) go func() { SetAppEnvironmentWithPlugins(t, plugins, th.App, th.NewPluginAPI) close(done) }() select { case <-done: case <-time.After(30 * time.Second): require.Fail(t, "plugin failed to activate: likely deadlocked") go func() { time.Sleep(5 * time.Second) os.Exit(1) }() } }) t.Run("Multiple Plugins", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() pluginPostOnHasBeenPosted := template.Must(template.New("pluginPostOnHasBeenPosted").Parse(` package main import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/model" ) type MyPlugin struct { plugin.MattermostPlugin } func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) { if _, from_plugin := post.GetProps()["from_plugin"]; from_plugin { return nil, "" } p.API.CreatePost(&model.Post{ UserId: "{{.User.Id}}", ChannelId: "{{.Channel.Id}}", Message: "message", Props: map[string]any{ "from_plugin": true, }, }) return nil, "" } func main() { plugin.ClientMain(&MyPlugin{}) } `, )) pluginPostOnActivate := template.Must(template.New("pluginPostOnActivate").Parse(` package main import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/model" ) type MyPlugin struct { plugin.MattermostPlugin } func (p *MyPlugin) OnActivate() error { _, err := p.API.CreatePost(&model.Post{ UserId: "{{.User.Id}}", ChannelId: "{{.Channel.Id}}", Message: "message", }) if err != nil { panic(err.Error()) } return nil } func main() { plugin.ClientMain(&MyPlugin{}) } `, )) templateData := struct { User *model.User Channel *model.Channel }{ th.BasicUser, th.BasicChannel, } plugins := []string{} pluginTemplates := []*template.Template{ pluginPostOnHasBeenPosted, pluginPostOnActivate, } for _, pluginTemplate := range pluginTemplates { b := &strings.Builder{} err := pluginTemplate.Execute(b, templateData) require.NoError(t, err) plugins = append(plugins, b.String()) } done := make(chan bool) go func() { SetAppEnvironmentWithPlugins(t, plugins, th.App, th.NewPluginAPI) close(done) }() select { case <-done: case <-time.After(30 * time.Second): require.Fail(t, "plugin failed to activate: likely deadlocked") go func() { time.Sleep(5 * time.Second) os.Exit(1) }() } }) t.Run("CreatePost on OnDeactivate Plugin", func(t *testing.T) { th := Setup(t).InitBasic() pluginPostOnActivate := template.Must(template.New("pluginPostOnActivate").Parse(` package main import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/model" ) type MyPlugin struct { plugin.MattermostPlugin } func (p *MyPlugin) OnDeactivate() error { _, err := p.API.CreatePost(&model.Post{ UserId: "{{.User.Id}}", ChannelId: "{{.Channel.Id}}", Message: "OnDeactivate", }) if err != nil { panic(err.Error()) } return nil } func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) { updatedPost := &model.Post{ UserId: "{{.User.Id}}", ChannelId: "{{.Channel.Id}}", Message: "messageUpdated", Props: map[string]any{ "from_plugin": true, }, } return updatedPost, "" } func main() { plugin.ClientMain(&MyPlugin{}) } `, )) templateData := struct { User *model.User Channel *model.Channel }{ th.BasicUser, th.BasicChannel, } plugins := []string{} pluginTemplates := []*template.Template{ pluginPostOnActivate, } for _, pluginTemplate := range pluginTemplates { b := &strings.Builder{} err := pluginTemplate.Execute(b, templateData) require.NoError(t, err) plugins = append(plugins, b.String()) } done := make(chan bool) go func() { posts, appErr := th.App.GetPosts(th.Context, th.BasicChannel.Id, 0, 2) require.Nil(t, appErr) require.NotNil(t, posts) messageWillBePostedCalled := false for _, p := range posts.Posts { if p.Message == "messageUpdated" { messageWillBePostedCalled = true } } require.False(t, messageWillBePostedCalled, "MessageWillBePosted should not have been called") SetAppEnvironmentWithPlugins(t, plugins, th.App, th.NewPluginAPI) th.TearDown() posts, appErr = th.App.GetPosts(th.Context, th.BasicChannel.Id, 0, 2) require.Nil(t, appErr) require.NotNil(t, posts) messageWillBePostedCalled = false for _, p := range posts.Posts { if p.Message == "messageUpdated" { messageWillBePostedCalled = true } } require.True(t, messageWillBePostedCalled, "MessageWillBePosted was not called on deactivate") close(done) }() select { case <-done: case <-time.After(30 * time.Second): require.Fail(t, "plugin failed to activate: likely deadlocked") go func() { time.Sleep(5 * time.Second) os.Exit(1) }() } }) }