Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2350 lines
62 KiB
Go
2350 lines
62 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
|
|
"github.com/mattermost/mattermost/server/public/plugin/utils"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
|
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
|
)
|
|
|
|
func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API) (func(), []string, []error) {
|
|
return setAppEnvironmentWithPlugins(t, pluginCode, app, apiFunc, "")
|
|
}
|
|
|
|
func SetAppEnvironmentWithPluginsGoVersion(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API, goVersion string) (func(), []string, []error) {
|
|
return setAppEnvironmentWithPlugins(t, pluginCode, app, apiFunc, goVersion)
|
|
}
|
|
|
|
func setAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API, goVersion string) (func(), []string, []error) {
|
|
pluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
webappPluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
|
|
env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil)
|
|
require.NoError(t, err)
|
|
|
|
app.ch.SetPluginsEnvironment(env)
|
|
pluginIDs := []string{}
|
|
activationErrors := []error{}
|
|
for _, code := range pluginCode {
|
|
pluginID := model.NewId()
|
|
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
|
|
utils.CompileGoVersion(t, goVersion, code, backend)
|
|
|
|
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
|
|
require.NoError(t, err)
|
|
_, _, activationErr := env.Activate(pluginID)
|
|
pluginIDs = append(pluginIDs, pluginID)
|
|
activationErrors = append(activationErrors, activationErr)
|
|
|
|
app.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{
|
|
Enable: true,
|
|
}
|
|
})
|
|
}
|
|
|
|
return func() {
|
|
os.RemoveAll(pluginDir)
|
|
os.RemoveAll(webappPluginDir)
|
|
}, pluginIDs, activationErrors
|
|
}
|
|
|
|
func TestHookMessageWillBePosted(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
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) {
|
|
return nil, "rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message_",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
if assert.NotNil(t, err) {
|
|
assert.Equal(t, "Post rejected by plugin. rejected", err.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("rejected, returned post ignored", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
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) {
|
|
post.Message = "ignored"
|
|
return post, "rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message_",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
if assert.NotNil(t, err) {
|
|
assert.Equal(t, "Post rejected by plugin. rejected", err.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("allowed", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
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) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
post, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, "message", post.Message)
|
|
retrievedPost, errSingle := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false)
|
|
require.NoError(t, errSingle)
|
|
assert.Equal(t, "message", retrievedPost.Message)
|
|
})
|
|
|
|
t.Run("updated", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
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) {
|
|
post.Message = post.Message + "_fromplugin"
|
|
return post, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
post, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, "message_fromplugin", post.Message)
|
|
retrievedPost, errSingle := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false)
|
|
require.NoError(t, errSingle)
|
|
assert.Equal(t, "message_fromplugin", retrievedPost.Message)
|
|
})
|
|
|
|
t.Run("multiple updated", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
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) {
|
|
|
|
post.Message = "prefix_" + post.Message
|
|
return post, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
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) {
|
|
post.Message = post.Message + "_suffix"
|
|
return post, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
post, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "prefix_message_suffix", post.Message)
|
|
})
|
|
}
|
|
|
|
func TestHookMessageHasBeenPosted(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "message").Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
|
p.API.LogDebug(post.Message)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
func TestHookMessageWillBeUpdated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
|
newPost.Message = newPost.Message + "fromplugin"
|
|
return newPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message_",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
post, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "message_", post.Message)
|
|
post.Message = post.Message + "edited_"
|
|
post, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "message_edited_fromplugin", post.Message)
|
|
}
|
|
|
|
func TestHookMessageHasBeenUpdated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "message_edited").Return(nil)
|
|
mockAPI.On("LogDebug", "message_").Return(nil)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) MessageHasBeenUpdated(c *plugin.Context, newPost, oldPost *model.Post) {
|
|
p.API.LogDebug(newPost.Message)
|
|
p.API.LogDebug(oldPost.Message)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message_",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
post, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "message_", post.Message)
|
|
post.Message = post.Message + "edited"
|
|
_, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
func TestHookMessageHasBeenDeleted(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "message").Return(nil).Times(1)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) MessageHasBeenDeleted(c *plugin.Context, post *model.Post) {
|
|
p.API.LogDebug(post.Message)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
_, err = th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
func TestHookFileWillBeUploaded(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "testhook.txt").Return(nil)
|
|
mockAPI.On("LogDebug", "inputfile").Return(nil)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"io"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
|
return nil, "rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
_, appErr := th.App.UploadFile(th.Context,
|
|
[]byte("inputfile"),
|
|
th.BasicChannel.Id,
|
|
"testhook.txt",
|
|
)
|
|
|
|
if assert.NotNil(t, appErr) {
|
|
assert.Equal(t, "File rejected by plugin. rejected", appErr.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("rejected, returned file ignored", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "testhook.txt").Return(nil)
|
|
mockAPI.On("LogDebug", "inputfile").Return(nil)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
|
n, err := output.Write([]byte("ignored"))
|
|
if err != nil {
|
|
return info, fmt.Sprintf("FAILED to write output file n: %v, err: %v", n, err)
|
|
}
|
|
info.Name = "ignored"
|
|
return info, "rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
_, appErr := th.App.UploadFile(th.Context,
|
|
[]byte("inputfile"),
|
|
th.BasicChannel.Id,
|
|
"testhook.txt",
|
|
)
|
|
|
|
if assert.NotNil(t, appErr) {
|
|
assert.Equal(t, "File rejected by plugin. rejected", appErr.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("allowed", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "testhook.txt").Return(nil)
|
|
mockAPI.On("LogDebug", "inputfile").Return(nil)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"io"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
response, appErr := th.App.UploadFile(th.Context,
|
|
[]byte("inputfile"),
|
|
th.BasicChannel.Id,
|
|
"testhook.txt",
|
|
)
|
|
|
|
assert.Nil(t, appErr)
|
|
assert.NotNil(t, response)
|
|
|
|
fileID := response.Id
|
|
fileInfo, appErr := th.App.GetFileInfo(th.Context, fileID)
|
|
assert.Nil(t, appErr)
|
|
assert.NotNil(t, fileInfo)
|
|
assert.Equal(t, "testhook.txt", fileInfo.Name)
|
|
|
|
fileReader, appErr := th.App.FileReader(fileInfo.Path)
|
|
assert.Nil(t, appErr)
|
|
var resultBuf bytes.Buffer
|
|
_, err := io.Copy(&resultBuf, fileReader)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "inputfile", resultBuf.String())
|
|
})
|
|
|
|
t.Run("updated", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "testhook.txt").Return(nil)
|
|
mockAPI.On("LogDebug", "inputfile").Return(nil)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"io"
|
|
"fmt"
|
|
"bytes"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
|
var buf bytes.Buffer
|
|
n, err := buf.ReadFrom(file)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("buf.ReadFrom failed, reading %d bytes: %s", err.Error()))
|
|
}
|
|
|
|
outbuf := bytes.NewBufferString("changedtext")
|
|
n, err = io.Copy(output, outbuf)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("io.Copy failed after %d bytes: %s", n, err.Error()))
|
|
}
|
|
if n != 11 {
|
|
panic(fmt.Sprintf("io.Copy only copied %d bytes", n))
|
|
}
|
|
info.Name = "modifiedinfo"
|
|
return info, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
response, appErr := th.App.UploadFile(th.Context,
|
|
[]byte("inputfile"),
|
|
th.BasicChannel.Id,
|
|
"testhook.txt",
|
|
)
|
|
assert.Nil(t, appErr)
|
|
assert.NotNil(t, response)
|
|
fileID := response.Id
|
|
|
|
fileInfo, appErr := th.App.GetFileInfo(th.Context, fileID)
|
|
assert.Nil(t, appErr)
|
|
assert.NotNil(t, fileInfo)
|
|
assert.Equal(t, "modifiedinfo", fileInfo.Name)
|
|
|
|
fileReader, appErr := th.App.FileReader(fileInfo.Path)
|
|
assert.Nil(t, appErr)
|
|
var resultBuf bytes.Buffer
|
|
_, err := io.Copy(&resultBuf, fileReader)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "changedtext", resultBuf.String())
|
|
})
|
|
}
|
|
|
|
func TestUserWillLogIn_Blocked(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
err := th.App.UpdatePassword(th.Context, th.BasicUser, "hunter2")
|
|
assert.Nil(t, err, "Error updating user password: %s", err)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) UserWillLogIn(c *plugin.Context, user *model.User) string {
|
|
return "Blocked By Plugin"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
r := &http.Request{}
|
|
w := httptest.NewRecorder()
|
|
session, err := th.App.DoLogin(th.Context, w, r, th.BasicUser, "", false, false, false)
|
|
|
|
assert.Contains(t, err.Id, "Login rejected by plugin", "Expected Login rejected by plugin, got %s", err.Id)
|
|
assert.Nil(t, session)
|
|
}
|
|
|
|
func TestUserWillLogInIn_Passed(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
err := th.App.UpdatePassword(th.Context, th.BasicUser, "hunter2")
|
|
|
|
assert.Nil(t, err, "Error updating user password: %s", err)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) UserWillLogIn(c *plugin.Context, user *model.User) string {
|
|
return ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
r := &http.Request{}
|
|
w := httptest.NewRecorder()
|
|
session, err := th.App.DoLogin(th.Context, w, r, th.BasicUser, "", false, false, false)
|
|
|
|
assert.Nil(t, err, "Expected nil, got %s", err)
|
|
require.NotNil(t, session)
|
|
assert.Equal(t, session.UserId, th.BasicUser.Id)
|
|
}
|
|
|
|
func TestUserHasLoggedIn(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
err := th.App.UpdatePassword(th.Context, th.BasicUser, "hunter2")
|
|
|
|
assert.Nil(t, err, "Error updating user password: %s", err)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) UserHasLoggedIn(c *plugin.Context, user *model.User) {
|
|
user.FirstName = "plugin-callback-success"
|
|
p.API.UpdateUser(user)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
r := &http.Request{}
|
|
w := httptest.NewRecorder()
|
|
session, err := th.App.DoLogin(th.Context, w, r, th.BasicUser, "", false, false, false)
|
|
|
|
assert.Nil(t, err, "Expected nil, got %s", err)
|
|
assert.NotNil(t, session)
|
|
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
user, _ := th.App.GetUser(th.BasicUser.Id)
|
|
assert.Equal(c, user.FirstName, "plugin-callback-success", "Expected firstname overwrite, got default")
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func TestUserHasBeenDeactivated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) UserHasBeenDeactivated(c *plugin.Context, user *model.User) {
|
|
user.Nickname = "plugin-callback-success"
|
|
p.API.UpdateUser(user)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := &model.User{
|
|
Email: "success+test@example.com",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: "testpassword",
|
|
}
|
|
|
|
_, err := th.App.CreateUser(th.Context, user)
|
|
require.Nil(t, err)
|
|
|
|
_, err = th.App.UpdateActive(th.Context, user, false)
|
|
require.Nil(t, err)
|
|
|
|
time.Sleep(2 * time.Second)
|
|
user, err = th.App.GetUser(user.Id)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "plugin-callback-success", user.Nickname)
|
|
}
|
|
|
|
func TestUserHasBeenCreated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) UserHasBeenCreated(c *plugin.Context, user *model.User) {
|
|
user.Nickname = "plugin-callback-success"
|
|
p.API.UpdateUser(user)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := &model.User{
|
|
Email: "success+test@example.com",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: "testpassword",
|
|
}
|
|
_, err := th.App.CreateUser(th.Context, user)
|
|
require.Nil(t, err)
|
|
|
|
time.Sleep(2 * time.Second)
|
|
user, err = th.App.GetUser(user.Id)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "plugin-callback-success", user.Nickname)
|
|
}
|
|
|
|
func TestErrorString(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
t.Run("errors.New", func(t *testing.T) {
|
|
tearDown, _, activationErrors := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
return errors.New("simulate failure")
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, activationErrors, 1)
|
|
require.Error(t, activationErrors[0])
|
|
require.Contains(t, activationErrors[0].Error(), "simulate failure")
|
|
})
|
|
|
|
t.Run("AppError", func(t *testing.T) {
|
|
tearDown, _, activationErrors := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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 {
|
|
return model.NewAppError("where", "id", map[string]any{"param": 1}, "details", 42)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, activationErrors, 1)
|
|
require.Error(t, activationErrors[0])
|
|
|
|
cause := errors.Cause(activationErrors[0])
|
|
require.IsType(t, &model.AppError{}, cause)
|
|
|
|
// params not expected, since not exported
|
|
expectedErr := model.NewAppError("where", "id", nil, "details", 42)
|
|
require.Equal(t, expectedErr, cause)
|
|
})
|
|
}
|
|
|
|
func TestHookContext(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
ctx := request.EmptyContext(th.TestLogger)
|
|
|
|
// We don't actually have a session, we are faking it so just set something arbitrarily
|
|
ctx.Session().Id = model.NewId()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", ctx.Session().Id).Return(nil)
|
|
mockAPI.On("LogInfo", ctx.RequestId()).Return(nil)
|
|
mockAPI.On("LogError", ctx.IPAddress()).Return(nil)
|
|
mockAPI.On("LogWarn", ctx.AcceptLanguage()).Return(nil)
|
|
mockAPI.On("DeleteTeam", ctx.UserAgent()).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
|
p.API.LogDebug(c.SessionId)
|
|
p.API.LogInfo(c.RequestId)
|
|
p.API.LogError(c.IPAddress)
|
|
p.API.LogWarn(c.AcceptLanguage)
|
|
p.API.DeleteTeam(c.UserAgent)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "not this",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(ctx, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
func TestActiveHooks(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
t.Run("", func(t *testing.T) {
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
return nil
|
|
}
|
|
|
|
func (p *MyPlugin) OnConfigurationChange() error {
|
|
return nil
|
|
}
|
|
|
|
func (p *MyPlugin) UserHasBeenCreated(c *plugin.Context, user *model.User) {
|
|
user.Nickname = "plugin-callback-success"
|
|
p.API.UpdateUser(user)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
user1 := &model.User{
|
|
Email: "success+test@example.com",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: "testpassword",
|
|
}
|
|
_, appErr := th.App.CreateUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
time.Sleep(2 * time.Second)
|
|
user1, appErr = th.App.GetUser(user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, "plugin-callback-success", user1.Nickname)
|
|
|
|
// Disable plugin
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginID)
|
|
require.Error(t, err)
|
|
require.Nil(t, hooks)
|
|
|
|
// Should fail to find pluginID as it was deleted when deactivated
|
|
path, err := th.App.GetPluginsEnvironment().PublicFilesPath(pluginID)
|
|
require.Error(t, err)
|
|
require.Empty(t, path)
|
|
})
|
|
}
|
|
|
|
func TestHookMetrics(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
t.Run("", func(t *testing.T) {
|
|
metricsMock := &mocks.MetricsInterface{}
|
|
|
|
pluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
webappPluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(pluginDir)
|
|
defer os.RemoveAll(webappPluginDir)
|
|
|
|
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), metricsMock)
|
|
require.NoError(t, err)
|
|
|
|
th.App.ch.SetPluginsEnvironment(env)
|
|
|
|
pluginID := model.NewId()
|
|
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
|
|
code := `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
return nil
|
|
}
|
|
|
|
func (p *MyPlugin) OnConfigurationChange() error {
|
|
return nil
|
|
}
|
|
|
|
func (p *MyPlugin) UserHasBeenCreated(c *plugin.Context, user *model.User) {
|
|
user.Nickname = "plugin-callback-success"
|
|
p.API.UpdateUser(user)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
utils.CompileGo(t, code, backend)
|
|
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
|
|
require.NoError(t, err)
|
|
|
|
// Setup mocks before activating
|
|
metricsMock.On("ObservePluginHookDuration", pluginID, "Implemented", true, mock.Anything).Return()
|
|
metricsMock.On("ObservePluginHookDuration", pluginID, "OnActivate", true, mock.Anything).Return()
|
|
metricsMock.On("ObservePluginHookDuration", pluginID, "OnDeactivate", true, mock.Anything).Return()
|
|
metricsMock.On("ObservePluginHookDuration", pluginID, "OnConfigurationChange", true, mock.Anything).Return()
|
|
metricsMock.On("ObservePluginHookDuration", pluginID, "UserHasBeenCreated", true, mock.Anything).Return()
|
|
|
|
// Don't care about these calls.
|
|
metricsMock.On("ObservePluginAPIDuration", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
|
|
metricsMock.On("ObservePluginMultiHookIterationDuration", mock.Anything, mock.Anything, mock.Anything).Return()
|
|
metricsMock.On("ObservePluginMultiHookDuration", mock.Anything).Return()
|
|
|
|
_, _, activationErr := env.Activate(pluginID)
|
|
require.NoError(t, activationErr)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{
|
|
Enable: true,
|
|
}
|
|
})
|
|
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
user1 := &model.User{
|
|
Email: "success+test@example.com",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: "testpassword",
|
|
AuthService: "",
|
|
}
|
|
_, appErr := th.App.CreateUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
time.Sleep(2 * time.Second)
|
|
user1, appErr = th.App.GetUser(user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, "plugin-callback-success", user1.Nickname)
|
|
|
|
// Disable plugin
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
metricsMock.AssertExpectations(t)
|
|
})
|
|
}
|
|
|
|
func TestHookReactionHasBeenAdded(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LogDebug", "smile").Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) {
|
|
p.API.LogDebug(reaction.EmojiName)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: th.BasicUser.Id,
|
|
PostId: th.BasicPost.Id,
|
|
EmojiName: "smile",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.SaveReactionForPost(th.Context, reaction)
|
|
require.Nil(t, err)
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
mockAPI.AssertExpectations(&testutils.CollectTWithLogf{CollectT: c})
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func TestHookReactionHasBeenRemoved(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LogDebug", "star").Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
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) ReactionHasBeenRemoved(c *plugin.Context, reaction *model.Reaction) {
|
|
p.API.LogDebug(reaction.EmojiName)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
reaction := &model.Reaction{
|
|
UserId: th.BasicUser.Id,
|
|
PostId: th.BasicPost.Id,
|
|
EmojiName: "star",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
|
|
err := th.App.DeleteReactionForPost(th.Context, reaction)
|
|
|
|
require.Nil(t, err)
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
mockAPI.AssertExpectations(&testutils.CollectTWithLogf{CollectT: c})
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
func TestHookRunDataRetention(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) RunDataRetention(nowMillis, batchSize int64) (int64, error){
|
|
return 100, nil
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
hookCalled := false
|
|
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
n, _ := hooks.RunDataRetention(0, 0)
|
|
// Ensure return it correct
|
|
assert.Equal(t, int64(100), n)
|
|
hookCalled = true
|
|
return hookCalled
|
|
}, plugin.RunDataRetentionID)
|
|
|
|
require.True(t, hookCalled)
|
|
}
|
|
|
|
func TestHookOnSendDailyTelemetry(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnSendDailyTelemetry() {
|
|
return
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
hookCalled := false
|
|
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.OnSendDailyTelemetry()
|
|
|
|
hookCalled = true
|
|
return hookCalled
|
|
}, plugin.OnSendDailyTelemetryID)
|
|
|
|
require.True(t, hookCalled)
|
|
}
|
|
|
|
func TestHookOnCloudLimitsUpdated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnCloudLimitsUpdated(_ *model.ProductLimits) {
|
|
return
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
hookCalled := false
|
|
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.OnCloudLimitsUpdated(nil)
|
|
|
|
hookCalled = true
|
|
return hookCalled
|
|
}, plugin.OnCloudLimitsUpdatedID)
|
|
|
|
require.True(t, hookCalled)
|
|
}
|
|
|
|
//go:embed test_templates/hook_notification_will_be_pushed.tmpl
|
|
var hookNotificationWillBePushedTmpl string
|
|
|
|
func TestHookNotificationWillBePushed(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
if testing.Short() {
|
|
t.Skip("skipping TestHookNotificationWillBePushed test in short mode")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
testCode string
|
|
expectedNotifications int
|
|
expectedNotificationMessage string
|
|
}{
|
|
{
|
|
name: "successfully pushed",
|
|
testCode: `return nil, ""`,
|
|
expectedNotifications: 6,
|
|
},
|
|
{
|
|
name: "push notification rejected",
|
|
testCode: `return nil, "rejected"`,
|
|
expectedNotifications: 0,
|
|
},
|
|
{
|
|
name: "push notification modified",
|
|
testCode: `notification.Message = "brand new message"
|
|
return notification, ""`,
|
|
expectedNotifications: 6,
|
|
expectedNotificationMessage: "brand new message",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
templatedPlugin := fmt.Sprintf(hookNotificationWillBePushedTmpl, tt.testCode)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{templatedPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
// Create 3 users, each having 2 sessions.
|
|
type userSession struct {
|
|
user *model.User
|
|
session *model.Session
|
|
}
|
|
var userSessions []userSession
|
|
for range 3 {
|
|
u := th.CreateUser()
|
|
sess, err := th.App.CreateSession(th.Context, &model.Session{
|
|
UserId: u.Id,
|
|
DeviceId: "deviceID" + u.Id,
|
|
ExpiresAt: model.GetMillis() + 100000,
|
|
})
|
|
require.Nil(t, err)
|
|
// We don't need to track the 2nd session.
|
|
_, err = th.App.CreateSession(th.Context, &model.Session{
|
|
UserId: u.Id,
|
|
DeviceId: "deviceID" + u.Id,
|
|
ExpiresAt: model.GetMillis() + 100000,
|
|
})
|
|
require.Nil(t, err)
|
|
_, err = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, u.Id)
|
|
require.Nil(t, err)
|
|
th.AddUserToChannel(u, th.BasicChannel)
|
|
userSessions = append(userSessions, userSession{
|
|
user: u,
|
|
session: sess,
|
|
})
|
|
}
|
|
|
|
handler := &testPushNotificationHandler{
|
|
t: t,
|
|
behavior: "simple",
|
|
}
|
|
pushServer := httptest.NewServer(
|
|
http.HandlerFunc(handler.handleReq),
|
|
)
|
|
defer pushServer.Close()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.EmailSettings.PushNotificationContents = model.GenericNotification
|
|
*cfg.EmailSettings.PushNotificationServer = pushServer.URL
|
|
})
|
|
|
|
var wg sync.WaitGroup
|
|
for _, data := range userSessions {
|
|
wg.Add(1)
|
|
go func(user model.User) {
|
|
defer wg.Done()
|
|
notification := &PostNotification{
|
|
Post: th.CreatePost(th.BasicChannel),
|
|
Channel: th.BasicChannel,
|
|
ProfileMap: map[string]*model.User{
|
|
user.Id: &user,
|
|
},
|
|
Sender: &user,
|
|
}
|
|
th.App.sendPushNotification(notification, &user, true, false, model.CommentsNotifyAny)
|
|
}(*data.user)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Hack to let the worker goroutines complete.
|
|
time.Sleep(2 * time.Second)
|
|
// Server side verification.
|
|
assert.Equal(t, tt.expectedNotifications, handler.numReqs())
|
|
var numMessages int
|
|
for _, n := range handler.notifications() {
|
|
switch n.Type {
|
|
case model.PushTypeMessage:
|
|
numMessages++
|
|
assert.Equal(t, th.BasicChannel.Id, n.ChannelId)
|
|
if tt.expectedNotificationMessage != "" {
|
|
assert.Equal(t, tt.expectedNotificationMessage, n.Message)
|
|
} else {
|
|
assert.Contains(t, n.Message, "mentioned you")
|
|
}
|
|
default:
|
|
assert.Fail(t, "should not receive any other push notification types")
|
|
}
|
|
}
|
|
assert.Equal(t, tt.expectedNotifications, numMessages)
|
|
})
|
|
}
|
|
}
|
|
|
|
//go:embed test_templates/hook_email_notification_will_be_sent.tmpl
|
|
var hookEmailNotificationWillBeSentTmpl string
|
|
|
|
func TestHookEmailNotificationWillBeSent(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
testCode string
|
|
expectedNotificationSubject string
|
|
expectedNotificationTitle string
|
|
expectedButtonText string
|
|
expectedFooterText string
|
|
}{
|
|
{
|
|
name: "successfully sent",
|
|
testCode: `return nil, ""`,
|
|
},
|
|
{
|
|
name: "email notification rejected",
|
|
testCode: `return nil, "rejected"`,
|
|
},
|
|
{
|
|
name: "email notification modified",
|
|
testCode: `content := &model.EmailNotificationContent{
|
|
Subject: "Modified Subject by Plugin",
|
|
Title: "Modified Title by Plugin",
|
|
ButtonText: "Modified Button by Plugin",
|
|
FooterText: "Modified Footer by Plugin",
|
|
}
|
|
return content, ""`,
|
|
expectedNotificationSubject: "Modified Subject by Plugin",
|
|
expectedNotificationTitle: "Modified Title by Plugin",
|
|
expectedButtonText: "Modified Button by Plugin",
|
|
expectedFooterText: "Modified Footer by Plugin",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Create a test user for email notifications
|
|
user := th.CreateUser()
|
|
th.LinkUserToTeam(user, th.BasicTeam)
|
|
th.AddUserToChannel(user, th.BasicChannel)
|
|
|
|
// Set up email notification preferences to disable batching
|
|
appErr := th.App.UpdatePreferences(th.Context, user.Id, model.Preferences{
|
|
{
|
|
UserId: user.Id,
|
|
Category: model.PreferenceCategoryNotifications,
|
|
Name: model.PreferenceNameEmailInterval,
|
|
Value: model.PreferenceEmailIntervalNoBatchingSeconds,
|
|
},
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
// Disable email batching in config
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.EmailSettings.EnableEmailBatching = false
|
|
})
|
|
|
|
// Create and set up plugin
|
|
templatedPlugin := fmt.Sprintf(hookEmailNotificationWillBeSentTmpl, tt.testCode)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{templatedPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
// For the modification test, create a simple test that verifies the hook is called
|
|
// The detailed verification would require more complex mocking which is beyond this test's scope
|
|
|
|
// Create a post that will trigger email notification
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "@" + user.Username + " test message",
|
|
CreateAt: model.GetMillis(),
|
|
}
|
|
|
|
// Create notification
|
|
notification := &PostNotification{
|
|
Post: post,
|
|
Channel: th.BasicChannel,
|
|
ProfileMap: map[string]*model.User{
|
|
user.Id: user,
|
|
},
|
|
Sender: th.BasicUser,
|
|
}
|
|
|
|
// Send email notification (this will trigger the hook)
|
|
// Use assert.Eventually to handle any potential race conditions with plugin activation/deactivation
|
|
assert.Eventually(t, func() bool {
|
|
modifiedNotification, err := th.App.sendNotificationEmail(th.Context, notification, user, th.BasicTeam, nil)
|
|
|
|
// For the rejected test case, we expect the notification to be rejected
|
|
if tt.name == "email notification rejected" {
|
|
// When rejected, sendNotificationEmail returns nil for the notification
|
|
return modifiedNotification == nil && err == nil
|
|
}
|
|
if err != nil || modifiedNotification == nil {
|
|
return false
|
|
}
|
|
|
|
// Verify the modified notification fields
|
|
if tt.expectedNotificationSubject != "" && modifiedNotification.Subject != tt.expectedNotificationSubject {
|
|
return false
|
|
}
|
|
if tt.expectedNotificationTitle != "" && modifiedNotification.Title != tt.expectedNotificationTitle {
|
|
return false
|
|
}
|
|
if tt.expectedButtonText != "" && modifiedNotification.ButtonText != tt.expectedButtonText {
|
|
return false
|
|
}
|
|
if tt.expectedFooterText != "" && modifiedNotification.FooterText != tt.expectedFooterText {
|
|
return false
|
|
}
|
|
return true
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHookMessagesWillBeConsumed(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
setupPlugin := func(t *testing.T, th *TestHelper) {
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "message").Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
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) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
for _, post := range posts {
|
|
post.Message = "mwbc_plugin:" + post.Message
|
|
}
|
|
return posts
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
}
|
|
|
|
t.Run("feature flag disabled", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.ConsumePostHook = false
|
|
}).InitBasic()
|
|
t.Cleanup(th.TearDown)
|
|
|
|
setupPlugin(t, th)
|
|
|
|
newPost := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, newPost, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
post, err := th.App.GetSinglePost(th.Context, newPost.Id, true)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "message", post.Message)
|
|
})
|
|
|
|
t.Run("feature flag enabled", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.ConsumePostHook = true
|
|
}).InitBasic()
|
|
|
|
t.Cleanup(th.TearDown)
|
|
|
|
setupPlugin(t, th)
|
|
|
|
newPost := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, newPost, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
post, err := th.App.GetSinglePost(th.Context, newPost.Id, true)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "mwbc_plugin:message", post.Message)
|
|
})
|
|
}
|
|
|
|
func TestHookPreferencesHaveChanged(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should be called when preferences are changed by non-plugin code", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
var mockAPI plugintest.API
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) {
|
|
for _, preference := range preferences {
|
|
p.API.LogDebug(fmt.Sprintf("category=%s name=%s value=%s", preference.Category, preference.Name, preference.Value))
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
// Confirm plugin is actually running
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Setup test
|
|
preferences := model.Preferences{
|
|
{
|
|
UserId: th.BasicUser.Id,
|
|
Category: "test_category",
|
|
Name: "test_name_1",
|
|
Value: "test_value_1",
|
|
},
|
|
{
|
|
UserId: th.BasicUser.Id,
|
|
Category: "test_category",
|
|
Name: "test_name_2",
|
|
Value: "test_value_2",
|
|
},
|
|
}
|
|
|
|
mockAPI.On("LogDebug", "category=test_category name=test_name_1 value=test_value_1")
|
|
mockAPI.On("LogDebug", "category=test_category name=test_name_2 value=test_value_2")
|
|
|
|
// Run test
|
|
err := th.App.UpdatePreferences(th.Context, th.BasicUser.Id, preferences)
|
|
|
|
require.Nil(t, err)
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
mockAPI.AssertExpectations(&testutils.CollectTWithLogf{CollectT: c})
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("should be called when preferences are changed by plugin code", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
const (
|
|
userID = "` + th.BasicUser.Id + `"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) {
|
|
// Note that plugin hooks can trigger themselves, and this test sets a preference to trigger that
|
|
// it has run, so be careful not to introduce an infinite loop here
|
|
|
|
if len(preferences) == 1 && preferences[0].Category == "test_category" && preferences[0].Name == "test_name" {
|
|
if preferences[0].Value == "test_value_first" {
|
|
appErr := p.API.UpdatePreferencesForUser(userID, []model.Preference{
|
|
{
|
|
UserId: userID,
|
|
Category: "test_category",
|
|
Name: "test_name",
|
|
Value: "test_value_second",
|
|
},
|
|
})
|
|
if appErr != nil {
|
|
panic("error setting preference to second value")
|
|
}
|
|
} else if preferences[0].Value == "test_value_second" {
|
|
appErr := p.API.UpdatePreferencesForUser(userID, []model.Preference{
|
|
{
|
|
UserId: userID,
|
|
Category: "test_category",
|
|
Name: "test_name",
|
|
Value: "test_value_third",
|
|
},
|
|
})
|
|
if appErr != nil {
|
|
panic("error setting preference to third value")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
// Confirm plugin is actually running
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
appErr := th.App.UpdatePreferences(th.Context, th.BasicUser.Id, model.Preferences{
|
|
{
|
|
UserId: th.BasicUser.Id,
|
|
Category: "test_category",
|
|
Name: "test_name",
|
|
Value: "test_value_first",
|
|
},
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
preference, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, "test_category", "test_name")
|
|
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, "test_value_third", preference.Value)
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
}
|
|
|
|
func TestChannelHasBeenCreated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
getPluginCode := func(th *TestHelper) string {
|
|
return `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
const (
|
|
adminUserID = "` + th.SystemAdminUser.Id + `"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelHasBeenCreated(c *plugin.Context, channel *model.Channel) {
|
|
_, appErr := p.API.CreatePost(&model.Post{
|
|
UserId: adminUserID,
|
|
ChannelId: channel.Id,
|
|
Message: "ChannelHasBeenCreated has been called for " + channel.Id,
|
|
})
|
|
if appErr != nil {
|
|
panic(appErr)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
}
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
t.Run("should call hook when a regular channel is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser()
|
|
|
|
channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
|
CreatorId: user1.Id,
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "test_channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 1)
|
|
|
|
require.Nil(t, appErr)
|
|
|
|
if assert.NotEmpty(t, posts.Order) {
|
|
post := posts.Posts[posts.Order[0]]
|
|
assert.Equal(t, channel.Id, post.ChannelId)
|
|
assert.Equal(t, "ChannelHasBeenCreated has been called for "+channel.Id, post.Message)
|
|
}
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("should call hook when a DM is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
|
|
channel, appErr := th.App.GetOrCreateDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 1)
|
|
|
|
require.Nil(t, appErr)
|
|
if assert.NotEmpty(t, posts.Order) {
|
|
post := posts.Posts[posts.Order[0]]
|
|
assert.Equal(t, channel.Id, post.ChannelId)
|
|
assert.Equal(t, "ChannelHasBeenCreated has been called for "+channel.Id, post.Message)
|
|
}
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("should call hook when a GM is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
user3 := th.CreateUser()
|
|
|
|
channel, appErr := th.App.CreateGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 1)
|
|
|
|
require.Nil(t, appErr)
|
|
if assert.NotEmpty(t, posts.Order) {
|
|
post := posts.Posts[posts.Order[0]]
|
|
assert.Equal(t, channel.Id, post.ChannelId)
|
|
assert.Equal(t, "ChannelHasBeenCreated has been called for "+channel.Id, post.Message)
|
|
}
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
}
|
|
|
|
func TestUserHasJoinedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
getPluginCode := func(th *TestHelper) string {
|
|
return `
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
const (
|
|
adminUserID = "` + th.SystemAdminUser.Id + `"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
|
message := fmt.Sprintf("Test: User %s joined %s", channelMember.UserId, channelMember.ChannelId)
|
|
if actor != nil && actor.Id != channelMember.UserId {
|
|
message = fmt.Sprintf("Test: User %s added to %s by %s", channelMember.UserId, channelMember.ChannelId, actor.Id)
|
|
}
|
|
|
|
_, appErr := p.API.CreatePost(&model.Post{
|
|
UserId: adminUserID,
|
|
ChannelId: channelMember.ChannelId,
|
|
Message: message,
|
|
})
|
|
if appErr != nil {
|
|
panic(appErr)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
}
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
t.Run("should call hook when a user joins an existing channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.CreateUser()
|
|
th.LinkUserToTeam(user1, th.BasicTeam)
|
|
user2 := th.CreateUser()
|
|
th.LinkUserToTeam(user2, th.BasicTeam)
|
|
|
|
channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
|
CreatorId: user1.Id,
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "test_channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
// Setup plugin after creating the channel
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
_, appErr = th.App.AddChannelMember(th.Context, user2.Id, channel, ChannelMemberOpts{
|
|
UserRequestorID: user2.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 30)
|
|
|
|
require.Nil(t, appErr)
|
|
assert.True(t, len(posts.Order) > 0)
|
|
|
|
found := false
|
|
for _, post := range posts.Posts {
|
|
if post.Message == fmt.Sprintf("Test: User %s joined %s", user2.Id, channel.Id) {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
assert.Fail(t, "Couldn't find user joined channel hook message post")
|
|
}
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("should call hook when a user is added to an existing channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
user1 := th.CreateUser()
|
|
th.LinkUserToTeam(user1, th.BasicTeam)
|
|
user2 := th.CreateUser()
|
|
th.LinkUserToTeam(user2, th.BasicTeam)
|
|
|
|
channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
|
CreatorId: user1.Id,
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "test_channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
// Setup plugin after creating the channel
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
_, appErr = th.App.AddChannelMember(th.Context, user2.Id, channel, ChannelMemberOpts{
|
|
UserRequestorID: user1.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
expectedMessage := fmt.Sprintf("Test: User %s added to %s by %s", user2.Id, channel.Id, user1.Id)
|
|
assert.Eventually(t, func() bool {
|
|
// Typically, the post we're looking for will be the latest, but there's a race between the plugin and
|
|
// "User has joined the channel" post which means the plugin post may not the the latest one
|
|
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 10)
|
|
require.Nil(t, appErr)
|
|
|
|
for _, postId := range posts.Order {
|
|
post := posts.Posts[postId]
|
|
|
|
if post.Message == expectedMessage {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("should not call hook when a regular channel is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser()
|
|
|
|
channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
|
CreatorId: user1.Id,
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "test_channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
var posts *model.PostList
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 10)
|
|
assert.Nil(t, appErr)
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
|
|
for _, postID := range posts.Order {
|
|
post := posts.Posts[postID]
|
|
|
|
if strings.HasPrefix(post.Message, "Test: ") {
|
|
t.Log("Plugin message found:", post.Message)
|
|
t.FailNow()
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("should not call hook when a DM is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
|
|
channel, appErr := th.App.GetOrCreateDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
var posts *model.PostList
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 10)
|
|
assert.Nil(t, appErr)
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
|
|
for _, postID := range posts.Order {
|
|
post := posts.Posts[postID]
|
|
|
|
if strings.HasPrefix(post.Message, "Test: ") {
|
|
t.Log("Plugin message found:", post.Message)
|
|
t.FailNow()
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("should not call hook when a GM is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser()
|
|
user2 := th.CreateUser()
|
|
user3 := th.CreateUser()
|
|
|
|
channel, appErr := th.App.CreateGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
var posts *model.PostList
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 10)
|
|
assert.Nil(t, appErr)
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
|
|
for _, postID := range posts.Order {
|
|
post := posts.Posts[postID]
|
|
|
|
if strings.HasPrefix(post.Message, "Test: ") {
|
|
t.Log("Plugin message found:", post.Message)
|
|
t.FailNow()
|
|
}
|
|
}
|
|
})
|
|
}
|