mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
6810 lines
198 KiB
Go
6810 lines
198 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"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/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/plugin/plugintest"
|
|
"github.com/hanzoteam/server/server/public/plugin/utils"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/hanzoteam/server/server/v8/channels/utils/testutils"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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())
|
|
})
|
|
|
|
t.Run("connection id propagated to plugin context", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
const connectionID = "test-connection-id-xyz"
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", "testhook.txt").Return(nil)
|
|
mockAPI.On("LogDebug", "inputfile").Return(nil)
|
|
mockAPI.On("LogDebug", "connection_id="+connectionID).Return(nil)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"io"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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) {
|
|
p.API.LogDebug("connection_id=" + c.ConnectionId)
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
defer tearDown()
|
|
|
|
rctx := th.Context.WithConnectionId(connectionID)
|
|
|
|
_, appErr := th.App.UploadFile(rctx,
|
|
[]byte("inputfile"),
|
|
th.BasicChannel.Id,
|
|
"testhook.txt",
|
|
)
|
|
require.Nil(t, appErr)
|
|
|
|
mockAPI.AssertCalled(t, "LogDebug", "connection_id="+connectionID)
|
|
})
|
|
}
|
|
|
|
func TestUserWillLogIn_Blocked(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics).InitBasic(t)
|
|
|
|
err := th.App.UpdatePassword(th.Context, th.BasicUser, model.NewTestPassword())
|
|
assert.Nil(t, err, "Error updating user password: %s", err)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, model.LoginOptions{})
|
|
|
|
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, StartMetrics).InitBasic(t)
|
|
|
|
err := th.App.UpdatePassword(th.Context, th.BasicUser, model.NewTestPassword())
|
|
|
|
assert.Nil(t, err, "Error updating user password: %s", err)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, model.LoginOptions{})
|
|
|
|
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, StartMetrics).InitBasic(t)
|
|
|
|
err := th.App.UpdatePassword(th.Context, th.BasicUser, model.NewTestPassword())
|
|
|
|
assert.Nil(t, err, "Error updating user password: %s", err)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, model.LoginOptions{})
|
|
|
|
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, StartMetrics)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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: "[email protected]",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: model.NewTestPassword(),
|
|
}
|
|
|
|
_, 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, StartMetrics)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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: "[email protected]",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: model.NewTestPassword(),
|
|
}
|
|
_, 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, StartMetrics)
|
|
|
|
t.Run("errors.New", func(t *testing.T) {
|
|
tearDown, _, activationErrors := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/hanzoteam/server/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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics)
|
|
|
|
t.Run("", func(t *testing.T) {
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/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: "[email protected]",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: model.NewTestPassword(),
|
|
}
|
|
_, 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, StartMetrics)
|
|
|
|
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/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/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: "[email protected]",
|
|
Nickname: "testnickname",
|
|
Username: "testusername",
|
|
Password: model.NewTestPassword(),
|
|
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, StartMetrics).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LogDebug", "smile").Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LogDebug", "star").Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
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(t)
|
|
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(t, 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(t, 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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHookNotificationWillBePushedTransportPreserved(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
if testing.Short() {
|
|
t.Skip("skipping TestHookNotificationWillBePushedTransportPreserved test in short mode")
|
|
}
|
|
|
|
const (
|
|
standardToken = model.PushNotifyAppleReactNative + ":standardtoken"
|
|
voIPToken = model.PushNotifyAppleReactNative + ":voiptoken"
|
|
)
|
|
|
|
// A plugin compiled against an older model.PushNotification struct can't see
|
|
// the Transport field, so returning a replacement notification would zero it.
|
|
// The server must ignore any plugin-driven change to Transport and keep the
|
|
// original routing (here, VoIP).
|
|
tests := []struct {
|
|
name string
|
|
testCode string
|
|
}{
|
|
{
|
|
name: "plugin zeroing the transport keeps the original routing",
|
|
testCode: `notification.Transport = ""
|
|
return notification, ""`,
|
|
},
|
|
{
|
|
name: "plugin overriding the transport keeps the original routing",
|
|
testCode: `notification.Transport = model.PushTransportStandard
|
|
return notification, ""`,
|
|
},
|
|
{
|
|
name: "plugin leaving the transport untouched keeps the original routing",
|
|
testCode: `return notification, ""`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
templatedPlugin := fmt.Sprintf(hookNotificationWillBePushedTmpl, tt.testCode)
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{templatedPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
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.PushNotificationServer = pushServer.URL
|
|
})
|
|
|
|
_, err := th.App.CreateSession(th.Context, &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
DeviceId: standardToken,
|
|
VoIPDeviceId: voIPToken,
|
|
ExpiresAt: model.GetMillis() + 100000,
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
msg := &model.PushNotification{
|
|
Type: model.PushTypeMessage,
|
|
SubType: model.PushSubTypeCalls,
|
|
Transport: model.PushTransportVoIP,
|
|
}
|
|
appErr := th.App.sendPushNotificationToAllSessions(th.Context, msg, th.BasicUser.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
require.Eventually(t, func() bool {
|
|
return len(handler.notifications()) == 1
|
|
}, 2*time.Second, 10*time.Millisecond, "expected exactly one push notification")
|
|
|
|
notifications := handler.notifications()
|
|
assert.Equal(t, model.PushTransportVoIP, notifications[0].Transport, "plugin must not be able to change the transport")
|
|
assert.Equal(t, "voiptoken", notifications[0].DeviceId, "VoIP routing must be preserved despite the plugin")
|
|
})
|
|
}
|
|
}
|
|
|
|
//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, StartMetrics).InitBasic(t)
|
|
|
|
// Create a test user for email notifications
|
|
user := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user, th.BasicTeam)
|
|
th.AddUserToChannel(t, 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)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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)
|
|
|
|
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 TestApplyPostWillBeConsumedHook(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", mock.Anything).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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("regular post is passed to the hook", func(t *testing.T) {
|
|
post := &model.Post{Message: "message"}
|
|
th.App.applyPostWillBeConsumedHook(th.Context, &post)
|
|
assert.Equal(t, "mwbc_plugin:message", post.Message)
|
|
})
|
|
|
|
t.Run("burn-on-read post is not passed to the hook", func(t *testing.T) {
|
|
post := &model.Post{Message: "message", Type: model.PostTypeBurnOnRead}
|
|
th.App.applyPostWillBeConsumedHook(th.Context, &post)
|
|
assert.Equal(t, "message", post.Message)
|
|
})
|
|
|
|
t.Run("post list skips burn-on-read posts", func(t *testing.T) {
|
|
regularPostID := model.NewId()
|
|
burnOnReadPostID := model.NewId()
|
|
posts := map[string]*model.Post{
|
|
regularPostID: {
|
|
Id: regularPostID,
|
|
Message: "message",
|
|
},
|
|
burnOnReadPostID: {
|
|
Id: burnOnReadPostID,
|
|
Message: "burn-on-read message",
|
|
Type: model.PostTypeBurnOnRead,
|
|
},
|
|
}
|
|
|
|
th.App.applyPostsWillBeConsumedHook(th.Context, posts)
|
|
|
|
assert.Equal(t, "mwbc_plugin:message", posts[regularPostID].Message)
|
|
assert.Equal(t, "burn-on-read message", posts[burnOnReadPostID].Message)
|
|
})
|
|
}
|
|
|
|
func TestApplyPostWillBeConsumedHookIgnoresReplacementWithDifferentID(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", mock.Anything).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
return []*model.Post{{
|
|
Id: model.NewId(),
|
|
Message: "unexpected replacement",
|
|
}}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
|
|
post := &model.Post{Id: model.NewId(), Message: "message"}
|
|
th.App.applyPostWillBeConsumedHook(th.Context, &post)
|
|
require.Equal(t, "message", post.Message)
|
|
}
|
|
|
|
func TestHookMessagesWillBeConsumedWithContext(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)
|
|
|
|
// The plugin records whether it received a non-nil context to confirm the context is
|
|
// threaded all the way through to the hook.
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumedWithContext(c *plugin.Context, posts []*model.Post) []*model.Post {
|
|
prefix := "mwbcwc_plugin:"
|
|
if c == nil {
|
|
prefix = "nilctx:"
|
|
}
|
|
for _, post := range posts {
|
|
post.Message = prefix + post.Message
|
|
}
|
|
return posts
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
}
|
|
|
|
t.Run("hook is applied", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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, "mwbcwc_plugin:message", post.Message)
|
|
})
|
|
}
|
|
|
|
func TestUpdatePostFiresConsumeHook(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", mock.Anything).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
for _, post := range posts {
|
|
post.Message = strings.ToUpper(post.Message)
|
|
}
|
|
return posts
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
|
|
wsMessages, closeWS := connectFakeWebSocket(t, th, th.BasicUser.Id, "", []model.WebsocketEventType{
|
|
model.WebsocketEventPosted,
|
|
model.WebsocketEventPostEdited,
|
|
})
|
|
defer closeWS()
|
|
|
|
basePost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original body",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: false})
|
|
require.Nil(t, err)
|
|
|
|
drainTimeout := time.After(500 * time.Millisecond)
|
|
drainLoop:
|
|
for {
|
|
select {
|
|
case <-wsMessages:
|
|
case <-drainTimeout:
|
|
break drainLoop
|
|
}
|
|
}
|
|
|
|
editedMessage := "edited body"
|
|
patchedPost, _, err := th.App.PatchPost(th.Context, basePost.Id, &model.PostPatch{
|
|
Message: &editedMessage,
|
|
}, nil)
|
|
require.Nil(t, err)
|
|
|
|
require.Equal(t, "EDITED BODY", patchedPost.Message)
|
|
|
|
timeout := time.After(5 * time.Second)
|
|
for {
|
|
select {
|
|
case ev := <-wsMessages:
|
|
if ev.EventType() != model.WebsocketEventPostEdited {
|
|
continue
|
|
}
|
|
postJSON, ok := ev.GetData()["post"].(string)
|
|
require.True(t, ok, "post field in websocket event should be a JSON string")
|
|
var wsPost model.Post
|
|
require.NoError(t, json.Unmarshal([]byte(postJSON), &wsPost))
|
|
assert.Equal(t, "EDITED BODY", wsPost.Message)
|
|
return
|
|
case <-timeout:
|
|
require.Fail(t, "timed out waiting for post_edited websocket event")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdatePostConsumeHooksWithOpenGraphMetadata(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
ogServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_, _ = io.WriteString(w, `<html><head><meta property="og:title" content="OG Title"><meta property="og:description" content="OG Description"></head><body>ok</body></html>`)
|
|
}))
|
|
t.Cleanup(ogServer.Close)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
hookImpl string
|
|
expectedMessage string
|
|
}{
|
|
{
|
|
name: "MessagesWillBeConsumed",
|
|
hookImpl: `
|
|
func (p *MyPlugin) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
for _, post := range posts {
|
|
if post.Metadata != nil {
|
|
post.Message = "metadata_leaked:" + post.Message
|
|
continue
|
|
}
|
|
post.Message = "mwbc_plugin:" + post.Message
|
|
}
|
|
return posts
|
|
}
|
|
`,
|
|
expectedMessage: "mwbc_plugin:",
|
|
},
|
|
{
|
|
name: "MessagesWillBeConsumedWithContext",
|
|
hookImpl: `
|
|
func (p *MyPlugin) MessagesWillBeConsumedWithContext(c *plugin.Context, posts []*model.Post) []*model.Post {
|
|
for _, post := range posts {
|
|
if post.Metadata != nil {
|
|
post.Message = "metadata_leaked:" + post.Message
|
|
continue
|
|
}
|
|
post.Message = "mwbcwc_plugin:" + post.Message
|
|
}
|
|
return posts
|
|
}
|
|
`,
|
|
expectedMessage: "mwbcwc_plugin:",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableLinkPreviews = true
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
|
}).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", mock.Anything).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
` + tc.hookImpl + `
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
|
|
basePost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original body",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: false})
|
|
require.Nil(t, err)
|
|
|
|
editedMessage := ogServer.URL + " edited body"
|
|
patchedPost, _, err := th.App.PatchPost(th.Context, basePost.Id, &model.PostPatch{
|
|
Message: &editedMessage,
|
|
}, nil)
|
|
require.Nil(t, err)
|
|
|
|
require.NotNil(t, patchedPost.Metadata)
|
|
require.NotEmpty(t, patchedPost.Metadata.Embeds)
|
|
require.Equal(t, tc.expectedMessage+editedMessage, patchedPost.Message)
|
|
})
|
|
}
|
|
|
|
t.Run("MessagesWillBeConsumed replacement is passed to MessagesWillBeConsumedWithContext", func(t *testing.T) {
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableLinkPreviews = true
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
|
}).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", mock.Anything).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
for _, post := range posts {
|
|
if post.Metadata != nil {
|
|
post.Message = "metadata_leaked:" + post.Message
|
|
continue
|
|
}
|
|
post.Message = "mwbc_plugin:" + post.Message
|
|
}
|
|
return posts
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`, `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumedWithContext(c *plugin.Context, posts []*model.Post) []*model.Post {
|
|
for _, post := range posts {
|
|
if post.Metadata != nil {
|
|
post.Message = "metadata_leaked:" + post.Message
|
|
continue
|
|
}
|
|
post.Message = "mwbcwc_plugin:" + post.Message
|
|
}
|
|
return posts
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
|
|
basePost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original body",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: false})
|
|
require.Nil(t, err)
|
|
|
|
editedMessage := ogServer.URL + " edited body"
|
|
patchedPost, _, err := th.App.PatchPost(th.Context, basePost.Id, &model.PostPatch{
|
|
Message: &editedMessage,
|
|
}, nil)
|
|
require.Nil(t, err)
|
|
|
|
require.NotNil(t, patchedPost.Metadata)
|
|
require.NotEmpty(t, patchedPost.Metadata.Embeds)
|
|
require.Equal(t, "mwbcwc_plugin:mwbc_plugin:"+editedMessage, patchedPost.Message)
|
|
})
|
|
}
|
|
|
|
func TestApplyPostsWillBeConsumedHookPreservesMetadataAndChainsReplacements(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
var mockAPI plugintest.API
|
|
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
|
mockAPI.On("LogDebug", mock.Anything).Return(nil)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
|
|
replacements := make([]*model.Post, 0, len(posts))
|
|
for _, post := range posts {
|
|
replacement := post.Clone()
|
|
if replacement.Metadata != nil {
|
|
replacement.Message = "metadata_leaked:" + replacement.Message
|
|
} else {
|
|
replacement.Message = "mwbc_plugin:" + replacement.Message
|
|
}
|
|
replacements = append(replacements, replacement)
|
|
}
|
|
replacements = append(replacements, &model.Post{Id: model.NewId(), Message: "unexpected replacement"})
|
|
return replacements
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`, `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessagesWillBeConsumedWithContext(c *plugin.Context, posts []*model.Post) []*model.Post {
|
|
replacements := make([]*model.Post, 0, len(posts))
|
|
for _, post := range posts {
|
|
replacement := post.Clone()
|
|
if replacement.Metadata != nil {
|
|
replacement.Message = "metadata_leaked:" + replacement.Message
|
|
} else {
|
|
replacement.Message = "mwbcwc_plugin:" + replacement.Message
|
|
}
|
|
replacements = append(replacements, replacement)
|
|
}
|
|
return replacements
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
|
|
t.Cleanup(tearDown)
|
|
|
|
priority := model.PostPriorityUrgent
|
|
requestedAck := true
|
|
postID := model.NewId()
|
|
metadata := &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{{Type: model.PostEmbedImage, URL: "http://example.com/image.png"}},
|
|
Priority: &model.PostPriority{Priority: &priority, RequestedAck: &requestedAck},
|
|
ExpireAt: 12345,
|
|
Recipients: []string{model.NewId()},
|
|
}
|
|
posts := map[string]*model.Post{
|
|
postID: {
|
|
Id: postID,
|
|
Message: "message",
|
|
Metadata: metadata,
|
|
},
|
|
}
|
|
|
|
th.App.applyPostsWillBeConsumedHook(th.Context, posts)
|
|
|
|
require.Equal(t, "mwbcwc_plugin:mwbc_plugin:message", posts[postID].Message)
|
|
require.Equal(t, metadata, posts[postID].Metadata)
|
|
require.Len(t, posts, 1)
|
|
}
|
|
|
|
func TestUpdatePostNoOpWhenNoPlugin(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
basePost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original body",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: false})
|
|
require.Nil(t, err)
|
|
|
|
editedMessage := "edited body"
|
|
patchedPost, _, err := th.App.PatchPost(th.Context, basePost.Id, &model.PostPatch{
|
|
Message: &editedMessage,
|
|
}, nil)
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, "edited body", patchedPost.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, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
var mockAPI plugintest.API
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser(t)
|
|
|
|
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, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
|
|
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, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(th), pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
user3 := th.CreateUser(t)
|
|
|
|
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 TestHookServeMetrics(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should call plugin ServeMetrics hook", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
// The config store silently drops FeatureFlags writes unless FF
|
|
// read-only mode is disabled first.
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
|
|
|
// Configure metrics
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.MetricsSettings.Enable = true
|
|
*cfg.MetricsSettings.ListenAddress = ":0"
|
|
*cfg.PluginSettings.Enable = true
|
|
cfg.FeatureFlags.AggregatePluginMetrics = true
|
|
})
|
|
|
|
// Create a plugin that implements ServeMetrics
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("# HELP plugin_test_metric Test metric from plugin\n# TYPE plugin_test_metric counter\nplugin_test_metric 42\n"))
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{pluginCode}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
|
|
// Verify plugin is active
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Create a simple handler that returns server metrics
|
|
serverMetrics := "# HELP server_test_metric Test metric from server\n# TYPE server_test_metric gauge\nserver_test_metric 100\n"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(serverMetrics))
|
|
})
|
|
|
|
// Register the metrics handler
|
|
th.App.Srv().Platform().HandleMetrics("/metrics", handler)
|
|
|
|
// Get the metrics router
|
|
metricsRouter := th.App.Srv().Platform().GetMetricsRouter()
|
|
require.NotNil(t, metricsRouter, "Metrics router should be available")
|
|
|
|
// Create a test server with the metrics router
|
|
server := httptest.NewServer(metricsRouter)
|
|
defer server.Close()
|
|
|
|
// Make a request to the metrics endpoint
|
|
resp, err := http.Get(server.URL + "/metrics")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
bodyStr := string(body)
|
|
|
|
// Verify both server and plugin metrics are present
|
|
assert.Contains(t, bodyStr, "server_test_metric 100", "Response should contain server metrics")
|
|
assert.Contains(t, bodyStr, "plugin_test_metric{plugin_id=\""+pluginID+"\"} 42", "Response should contain plugin metrics with plugin_id label")
|
|
})
|
|
|
|
t.Run("should handle multiple plugins providing metrics", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
// The config store silently drops FeatureFlags writes unless FF
|
|
// read-only mode is disabled first.
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
|
|
|
// Configure metrics
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.MetricsSettings.Enable = true
|
|
*cfg.MetricsSettings.ListenAddress = ":0"
|
|
*cfg.PluginSettings.Enable = true
|
|
cfg.FeatureFlags.AggregatePluginMetrics = true
|
|
})
|
|
|
|
// Create two plugins that implement ServeMetrics
|
|
plugin1Code := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("# HELP plugin1_metric Metric from plugin 1\n# TYPE plugin1_metric counter\nplugin1_metric 10\n"))
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
plugin2Code := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("# HELP plugin2_metric Metric from plugin 2\n# TYPE plugin2_metric gauge\nplugin2_metric 20\n"))
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{plugin1Code, plugin2Code}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 2)
|
|
|
|
// Verify both plugins are active
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginIDs[0]))
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginIDs[1]))
|
|
|
|
// Create a simple handler that returns server metrics
|
|
serverMetrics := "# HELP server_metric Server metric\n# TYPE server_metric gauge\nserver_metric 100\n"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(serverMetrics))
|
|
})
|
|
|
|
// Register the metrics handler
|
|
th.App.Srv().Platform().HandleMetrics("/metrics", handler)
|
|
|
|
// Get the metrics router
|
|
metricsRouter := th.App.Srv().Platform().GetMetricsRouter()
|
|
require.NotNil(t, metricsRouter, "Metrics router should be available")
|
|
|
|
// Create a test server with the metrics router
|
|
server := httptest.NewServer(metricsRouter)
|
|
defer server.Close()
|
|
|
|
// Make a request to the metrics endpoint
|
|
resp, err := http.Get(server.URL + "/metrics")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
bodyStr := string(body)
|
|
|
|
// Verify server and both plugin metrics are present
|
|
assert.Contains(t, bodyStr, "server_metric 100", "Response should contain server metrics")
|
|
assert.Contains(t, bodyStr, "plugin1_metric{plugin_id=\""+pluginIDs[0]+"\"} 10", "Response should contain plugin1 metrics")
|
|
assert.Contains(t, bodyStr, "plugin2_metric{plugin_id=\""+pluginIDs[1]+"\"} 20", "Response should contain plugin2 metrics")
|
|
})
|
|
|
|
t.Run("should handle plugin not implementing ServeMetrics", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
// Configure metrics
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.MetricsSettings.Enable = true
|
|
*cfg.MetricsSettings.ListenAddress = ":0"
|
|
cfg.FeatureFlags.AggregatePluginMetrics = true
|
|
})
|
|
|
|
// Create a plugin that does NOT implement ServeMetrics
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{pluginCode}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
|
|
// Create a simple handler that returns server metrics
|
|
serverMetrics := "# HELP server_metric Server metric\n# TYPE server_metric gauge\nserver_metric 100\n"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(serverMetrics))
|
|
})
|
|
|
|
// Register the metrics handler
|
|
th.App.Srv().Platform().HandleMetrics("/metrics", handler)
|
|
|
|
// Get the metrics router
|
|
metricsRouter := th.App.Srv().Platform().GetMetricsRouter()
|
|
require.NotNil(t, metricsRouter, "Metrics router should be available")
|
|
|
|
// Create a test server with the metrics router
|
|
server := httptest.NewServer(metricsRouter)
|
|
defer server.Close()
|
|
|
|
// Make a request to the metrics endpoint
|
|
resp, err := http.Get(server.URL + "/metrics")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
bodyStr := string(body)
|
|
|
|
// Verify only server metrics are present (no plugin metrics)
|
|
assert.Contains(t, bodyStr, "server_metric 100", "Response should contain server metrics")
|
|
// The plugin didn't implement ServeMetrics, so it shouldn't add any metrics
|
|
assert.NotContains(t, bodyStr, "plugin_id=\""+pluginIDs[0]+"\"", "Response should not contain plugin metrics from non-implementing plugin")
|
|
})
|
|
|
|
t.Run("should not collect plugin metrics when AggregatePluginMetrics is disabled", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.MetricsSettings.Enable = true
|
|
*cfg.MetricsSettings.ListenAddress = ":0"
|
|
cfg.FeatureFlags.AggregatePluginMetrics = false
|
|
})
|
|
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("# HELP plugin_metric Plugin metric\n# TYPE plugin_metric counter\nplugin_metric 1\n"))
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{pluginCode}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginIDs[0]))
|
|
|
|
serverMetrics := "# HELP server_metric Server metric\n# TYPE server_metric gauge\nserver_metric 100\n"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(serverMetrics))
|
|
})
|
|
|
|
th.App.Srv().Platform().HandleMetrics("/metrics", handler)
|
|
|
|
metricsRouter := th.App.Srv().Platform().GetMetricsRouter()
|
|
require.NotNil(t, metricsRouter)
|
|
|
|
server := httptest.NewServer(metricsRouter)
|
|
defer server.Close()
|
|
|
|
resp, err := http.Get(server.URL + "/metrics")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
bodyStr := string(body)
|
|
assert.Contains(t, bodyStr, "server_metric 100")
|
|
assert.NotContains(t, bodyStr, "plugin_id=")
|
|
})
|
|
|
|
t.Run("should omit plugin metrics when plugin returns non-200", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.MetricsSettings.Enable = true
|
|
*cfg.MetricsSettings.ListenAddress = ":0"
|
|
cfg.FeatureFlags.AggregatePluginMetrics = true
|
|
})
|
|
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{pluginCode}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginIDs[0]))
|
|
|
|
serverMetrics := "# HELP server_metric Server metric\n# TYPE server_metric gauge\nserver_metric 100\n"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(serverMetrics))
|
|
})
|
|
|
|
th.App.Srv().Platform().HandleMetrics("/metrics", handler)
|
|
|
|
metricsRouter := th.App.Srv().Platform().GetMetricsRouter()
|
|
require.NotNil(t, metricsRouter)
|
|
|
|
server := httptest.NewServer(metricsRouter)
|
|
defer server.Close()
|
|
|
|
resp, err := http.Get(server.URL + "/metrics")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
bodyStr := string(body)
|
|
assert.Contains(t, bodyStr, "server_metric 100")
|
|
assert.NotContains(t, bodyStr, "plugin_id=")
|
|
})
|
|
|
|
t.Run("should omit plugin metrics when plugin returns empty body", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.MetricsSettings.Enable = true
|
|
*cfg.MetricsSettings.ListenAddress = ":0"
|
|
cfg.FeatureFlags.AggregatePluginMetrics = true
|
|
})
|
|
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{pluginCode}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginIDs[0]))
|
|
|
|
serverMetrics := "# HELP server_metric Server metric\n# TYPE server_metric gauge\nserver_metric 100\n"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(serverMetrics))
|
|
})
|
|
|
|
th.App.Srv().Platform().HandleMetrics("/metrics", handler)
|
|
|
|
metricsRouter := th.App.Srv().Platform().GetMetricsRouter()
|
|
require.NotNil(t, metricsRouter)
|
|
|
|
server := httptest.NewServer(metricsRouter)
|
|
defer server.Close()
|
|
|
|
resp, err := http.Get(server.URL + "/metrics")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
bodyStr := string(body)
|
|
assert.Contains(t, bodyStr, "server_metric 100")
|
|
assert.NotContains(t, bodyStr, "plugin_id=")
|
|
})
|
|
}
|
|
|
|
func TestUserHasJoinedChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
// These tests use the KV store to track whether the plugin is activated and whether its hook has been called
|
|
const pluginActivatedKey = "activated"
|
|
joinedKey := func(channelID, userID string) string {
|
|
return fmt.Sprintf("joined_%s_%s", channelID, userID)
|
|
}
|
|
|
|
getPluginCode := func() string {
|
|
return `
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
if appErr := p.API.KVSet("` + pluginActivatedKey + `", []byte("true")); appErr != nil {
|
|
return appErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *MyPlugin) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
|
if appErr := p.API.KVSet(fmt.Sprintf("joined_%s_%s", channelMember.ChannelId, channelMember.UserId), []byte("true")); appErr != nil {
|
|
panic(appErr)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
}
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
waitForPluginActivated := func(t *testing.T, th *TestHelper) {
|
|
t.Helper()
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
value, appErr := th.App.GetPluginKey(pluginID, pluginActivatedKey)
|
|
assert.Nil(c, appErr)
|
|
assert.Equal(c, []byte("true"), value)
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
assertHookCalled := func(t *testing.T, th *TestHelper, channelID, userID string) {
|
|
t.Helper()
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
value, appErr := th.App.GetPluginKey(pluginID, joinedKey(channelID, userID))
|
|
assert.Nil(c, appErr)
|
|
assert.Equal(c, []byte("true"), value)
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
assertHookNotCalled := func(t *testing.T, th *TestHelper, channelID string, userIDs ...string) {
|
|
t.Helper()
|
|
assert.Never(t, func() bool {
|
|
for _, userID := range userIDs {
|
|
value, appErr := th.App.GetPluginKey(pluginID, joinedKey(channelID, userID))
|
|
if appErr == nil && value != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, 1*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
t.Run("should call hook when a user joins an existing channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics).InitBasic(t)
|
|
|
|
user1 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user1, th.BasicTeam)
|
|
user2 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, 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(), pluginManifest, pluginID, th.App, th.Context)
|
|
waitForPluginActivated(t, th)
|
|
|
|
_, appErr = th.App.AddChannelMember(th.Context, user2.Id, channel, ChannelMemberOpts{
|
|
UserRequestorID: user2.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
assertHookCalled(t, th, channel.Id, user2.Id)
|
|
})
|
|
|
|
t.Run("should call hook when a user is added to an existing channel", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics).InitBasic(t)
|
|
|
|
user1 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user1, th.BasicTeam)
|
|
user2 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, 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(), pluginManifest, pluginID, th.App, th.Context)
|
|
waitForPluginActivated(t, th)
|
|
|
|
_, appErr = th.App.AddChannelMember(th.Context, user2.Id, channel, ChannelMemberOpts{
|
|
UserRequestorID: user1.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
assertHookCalled(t, th, channel.Id, user2.Id)
|
|
})
|
|
|
|
t.Run("should not call hook when a regular channel is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(), pluginManifest, pluginID, th.App, th.Context)
|
|
waitForPluginActivated(t, th)
|
|
|
|
user1 := th.CreateUser(t)
|
|
|
|
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)
|
|
|
|
assertHookNotCalled(t, th, channel.Id, user1.Id)
|
|
})
|
|
|
|
t.Run("should not call hook when a DM is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(), pluginManifest, pluginID, th.App, th.Context)
|
|
waitForPluginActivated(t, th)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
|
|
channel, appErr := th.App.GetOrCreateDirectChannel(th.Context, user1.Id, user2.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
assertHookNotCalled(t, th, channel.Id, user1.Id, user2.Id)
|
|
})
|
|
|
|
t.Run("should not call hook when a GM is created", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics).InitBasic(t)
|
|
|
|
// Setup plugin
|
|
setupPluginAPITest(t, getPluginCode(), pluginManifest, pluginID, th.App, th.Context)
|
|
waitForPluginActivated(t, th)
|
|
|
|
user1 := th.CreateUser(t)
|
|
user2 := th.CreateUser(t)
|
|
user3 := th.CreateUser(t)
|
|
|
|
channel, appErr := th.App.CreateGroupChannel(th.Context, []string{user1.Id, user2.Id, user3.Id}, user1.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, channel)
|
|
|
|
assertHookNotCalled(t, th, channel.Id, user1.Id, user2.Id)
|
|
})
|
|
}
|
|
|
|
func TestHookChannelMemberWillBeAdded(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelMemberWillBeAdded(c *plugin.Context, channelMember *model.ChannelMember) (*model.ChannelMember, string) {
|
|
return nil, "not allowed"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, user.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
_, appErr = th.App.AddChannelMember(th.Context, user.Id, th.BasicChannel, ChannelMemberOpts{})
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
})
|
|
|
|
t.Run("modified", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelMemberWillBeAdded(c *plugin.Context, channelMember *model.ChannelMember) (*model.ChannelMember, string) {
|
|
channelMember.NotifyProps = model.GetDefaultChannelNotifyProps()
|
|
channelMember.NotifyProps[model.DesktopNotifyProp] = model.ChannelNotifyAll
|
|
return channelMember, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, user.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
member, appErr := th.App.AddChannelMember(th.Context, user.Id, th.BasicChannel, ChannelMemberOpts{})
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, model.ChannelNotifyAll, member.NotifyProps[model.DesktopNotifyProp])
|
|
})
|
|
|
|
t.Run("allowed", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelMemberWillBeAdded(c *plugin.Context, channelMember *model.ChannelMember) (*model.ChannelMember, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, user.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
member, appErr := th.App.AddChannelMember(th.Context, user.Id, th.BasicChannel, ChannelMemberOpts{})
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, th.BasicChannel.Id, member.ChannelId)
|
|
assert.Equal(t, user.Id, member.UserId)
|
|
})
|
|
}
|
|
|
|
func TestHookTeamMemberWillBeAdded(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) TeamMemberWillBeAdded(c *plugin.Context, teamMember *model.TeamMember) (*model.TeamMember, string) {
|
|
return nil, "not allowed"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
team := th.CreateTeam(t)
|
|
|
|
_, appErr := th.App.JoinUserToTeam(th.Context, team, user, "")
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
})
|
|
|
|
t.Run("modified", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) TeamMemberWillBeAdded(c *plugin.Context, teamMember *model.TeamMember) (*model.TeamMember, string) {
|
|
teamMember.SchemeAdmin = true
|
|
return teamMember, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
team := th.CreateTeam(t)
|
|
|
|
member, appErr := th.App.JoinUserToTeam(th.Context, team, user, "")
|
|
require.Nil(t, appErr)
|
|
assert.True(t, member.SchemeAdmin)
|
|
})
|
|
|
|
t.Run("allowed", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) TeamMemberWillBeAdded(c *plugin.Context, teamMember *model.TeamMember) (*model.TeamMember, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
team := th.CreateTeam(t)
|
|
|
|
member, appErr := th.App.JoinUserToTeam(th.Context, team, user, "")
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, team.Id, member.TeamId)
|
|
assert.Equal(t, user.Id, member.UserId)
|
|
})
|
|
|
|
t.Run("already active member skips hook", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) TeamMemberWillBeAdded(c *plugin.Context, teamMember *model.TeamMember) (*model.TeamMember, string) {
|
|
return nil, "should not fire for existing member"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
// BasicUser is already a member of BasicTeam via InitBasic
|
|
member, appErr := th.App.JoinUserToTeam(th.Context, th.BasicTeam, th.BasicUser, "")
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, th.BasicTeam.Id, member.TeamId)
|
|
})
|
|
|
|
t.Run("re-join after leaving applies hook", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) TeamMemberWillBeAdded(c *plugin.Context, teamMember *model.TeamMember) (*model.TeamMember, string) {
|
|
teamMember.SchemeAdmin = true
|
|
return teamMember, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
user := th.CreateUser(t)
|
|
team := th.CreateTeam(t)
|
|
|
|
// First join
|
|
_, appErr := th.App.JoinUserToTeam(th.Context, team, user, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Leave
|
|
err := th.App.LeaveTeam(th.Context, team, user, "")
|
|
require.Nil(t, err)
|
|
|
|
// Re-join — hook should fire on the re-add path
|
|
member, appErr := th.App.JoinUserToTeam(th.Context, team, user, "")
|
|
require.Nil(t, appErr)
|
|
assert.True(t, member.SchemeAdmin)
|
|
})
|
|
|
|
t.Run("CreateTeamWithUser rejected by hook", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) TeamMemberWillBeAdded(c *plugin.Context, teamMember *model.TeamMember) (*model.TeamMember, string) {
|
|
return nil, "team join blocked"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
team := &model.Team{
|
|
DisplayName: "Test Team",
|
|
Name: "test-team-" + model.NewId()[:8],
|
|
Type: model.TeamOpen,
|
|
}
|
|
_, appErr := th.App.CreateTeamWithUser(th.Context, team, th.BasicUser.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
})
|
|
}
|
|
|
|
func TestHookChannelWillBeArchived(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeArchived(c *plugin.Context, channel *model.Channel) string {
|
|
return "archive not permitted"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
appErr := th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
|
|
// Verify channel was NOT archived
|
|
ch, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, int64(0), ch.DeleteAt)
|
|
})
|
|
|
|
t.Run("allowed", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeArchived(c *plugin.Context, channel *model.Channel) string {
|
|
return ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
appErr := th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
// Verify channel was archived
|
|
ch, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, int64(0), ch.DeleteAt)
|
|
})
|
|
}
|
|
|
|
func TestHookRPCChannelWillBeUpdated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return nil, "rpc test rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
newCh := &model.Channel{Id: model.NewId(), TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, DisplayName: "new"}
|
|
oldCh := &model.Channel{Id: newCh.Id, TeamId: th.BasicTeam.Id, Type: model.ChannelTypeOpen, DisplayName: "old"}
|
|
replacement, reason := hooks.ChannelWillBeUpdated(&plugin.Context{}, newCh, oldCh)
|
|
require.Equal(t, "rpc test rejected", reason)
|
|
require.Nil(t, replacement)
|
|
})
|
|
|
|
t.Run("modify", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
newChannel.DisplayName = "modified-by-plugin"
|
|
return newChannel, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
newCh := &model.Channel{Id: model.NewId(), TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, DisplayName: "new"}
|
|
oldCh := &model.Channel{Id: newCh.Id, TeamId: th.BasicTeam.Id, Type: model.ChannelTypeOpen, DisplayName: "old"}
|
|
replacement, reason := hooks.ChannelWillBeUpdated(&plugin.Context{}, newCh, oldCh)
|
|
require.Equal(t, "", reason)
|
|
require.NotNil(t, replacement)
|
|
require.Equal(t, "modified-by-plugin", replacement.DisplayName)
|
|
})
|
|
}
|
|
|
|
func TestHookRPCChannelWillBeRestored(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
return "rpc test rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
ch := &model.Channel{Id: model.NewId(), TeamId: th.BasicTeam.Id, Type: model.ChannelTypePrivate, DisplayName: "restore"}
|
|
reason := hooks.ChannelWillBeRestored(&plugin.Context{}, ch)
|
|
require.Equal(t, "rpc test rejected", reason)
|
|
}
|
|
|
|
func TestHookRPCScheduledPostWillBeCreated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ScheduledPostWillBeCreated(c *plugin.Context, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, string) {
|
|
return nil, "rpc test rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
sp := &model.ScheduledPost{
|
|
Draft: model.Draft{
|
|
UserId: model.NewId(),
|
|
ChannelId: model.NewId(),
|
|
Message: "scheduled hi",
|
|
},
|
|
Id: model.NewId(),
|
|
ScheduledAt: 1234567890,
|
|
}
|
|
replacement, reason := hooks.ScheduledPostWillBeCreated(&plugin.Context{}, sp)
|
|
require.Equal(t, "rpc test rejected", reason)
|
|
require.Nil(t, replacement)
|
|
})
|
|
|
|
t.Run("modify", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ScheduledPostWillBeCreated(c *plugin.Context, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, string) {
|
|
scheduledPost.Message = "modified-by-plugin"
|
|
return scheduledPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
sp := &model.ScheduledPost{
|
|
Draft: model.Draft{
|
|
UserId: model.NewId(),
|
|
ChannelId: model.NewId(),
|
|
Message: "original",
|
|
},
|
|
Id: model.NewId(),
|
|
ScheduledAt: 1234567890,
|
|
}
|
|
replacement, reason := hooks.ScheduledPostWillBeCreated(&plugin.Context{}, sp)
|
|
require.Equal(t, "", reason)
|
|
require.NotNil(t, replacement)
|
|
require.Equal(t, "modified-by-plugin", replacement.Message)
|
|
})
|
|
}
|
|
|
|
func TestHookRPCDraftWillBeUpserted(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) DraftWillBeUpserted(c *plugin.Context, draft *model.Draft) (*model.Draft, string) {
|
|
return nil, "rpc test rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
draft := &model.Draft{
|
|
UserId: model.NewId(),
|
|
ChannelId: model.NewId(),
|
|
Message: "draft hi",
|
|
}
|
|
replacement, reason := hooks.DraftWillBeUpserted(&plugin.Context{}, draft)
|
|
require.Equal(t, "rpc test rejected", reason)
|
|
require.Nil(t, replacement)
|
|
})
|
|
|
|
t.Run("modify", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) DraftWillBeUpserted(c *plugin.Context, draft *model.Draft) (*model.Draft, string) {
|
|
draft.Message = "modified-by-plugin"
|
|
return draft, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
draft := &model.Draft{
|
|
UserId: model.NewId(),
|
|
ChannelId: model.NewId(),
|
|
Message: "original",
|
|
}
|
|
replacement, reason := hooks.DraftWillBeUpserted(&plugin.Context{}, draft)
|
|
require.Equal(t, "", reason)
|
|
require.NotNil(t, replacement)
|
|
require.Equal(t, "modified-by-plugin", replacement.Message)
|
|
})
|
|
}
|
|
|
|
func TestRegisterChannelGuardIdempotent(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channelID := th.BasicChannel.Id
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
channelID := "` + channelID + `"
|
|
if appErr := p.API.RegisterChannelGuard(channelID); appErr != nil {
|
|
return appErr
|
|
}
|
|
// Second call must be idempotent.
|
|
return p.API.RegisterChannelGuard(channelID)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
|
|
rctx := request.EmptyContext(th.App.Srv().Log())
|
|
guards, err := th.App.Srv().Store().ChannelGuard().GetForChannel(rctx, channelID)
|
|
require.NoError(t, err)
|
|
require.Len(t, guards, 1, "second Register call must be a no-op (DO NOTHING)")
|
|
|
|
cached := th.App.Channels().getGuardsForChannel(channelID)
|
|
require.Len(t, cached, 1, "cache should match the store")
|
|
assert.Equal(t, strings.ToLower(pluginIDs[0]), cached[0].PluginId)
|
|
}
|
|
|
|
func TestRegisterChannelGuardMultiClaim(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channelID := th.BasicChannel.Id
|
|
|
|
pluginCode := func() string {
|
|
return `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
return p.API.RegisterChannelGuard("` + channelID + `")
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
}
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
pluginCode(),
|
|
pluginCode(),
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 2)
|
|
|
|
rctx := request.EmptyContext(th.App.Srv().Log())
|
|
guards, err := th.App.Srv().Store().ChannelGuard().GetForChannel(rctx, channelID)
|
|
require.NoError(t, err)
|
|
require.Len(t, guards, 2, "two distinct plugins must produce two rows")
|
|
|
|
pluginAID := strings.ToLower(pluginIDs[0])
|
|
pluginBID := strings.ToLower(pluginIDs[1])
|
|
|
|
cached := th.App.Channels().getGuardsForChannel(channelID)
|
|
require.Len(t, cached, 2)
|
|
cachedIDs := []string{cached[0].PluginId, cached[1].PluginId}
|
|
assert.Contains(t, cachedIDs, pluginAID)
|
|
assert.Contains(t, cachedIDs, pluginBID)
|
|
|
|
// Unregister plugin A's claim via the App-level method; B's claim must remain.
|
|
require.Nil(t, th.App.UnregisterChannelGuard(rctx, channelID, pluginAID))
|
|
|
|
guards, err = th.App.Srv().Store().ChannelGuard().GetForChannel(rctx, channelID)
|
|
require.NoError(t, err)
|
|
require.Len(t, guards, 1)
|
|
assert.Equal(t, pluginBID, guards[0].PluginId)
|
|
|
|
cached = th.App.Channels().getGuardsForChannel(channelID)
|
|
require.Len(t, cached, 1)
|
|
assert.Equal(t, pluginBID, cached[0].PluginId)
|
|
}
|
|
|
|
func TestChannelGuardSurvivesArchive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channelID := th.BasicChannel.Id
|
|
|
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
return p.API.RegisterChannelGuard("` + channelID + `")
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, pluginIDs, 1)
|
|
|
|
// Archive the channel.
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id))
|
|
|
|
// Guard row must persist (no FK, no cascade).
|
|
rctx := request.EmptyContext(th.App.Srv().Log())
|
|
guards, err := th.App.Srv().Store().ChannelGuard().GetForChannel(rctx, channelID)
|
|
require.NoError(t, err)
|
|
require.Len(t, guards, 1)
|
|
assert.Equal(t, strings.ToLower(pluginIDs[0]), guards[0].PluginId)
|
|
|
|
cached := th.App.Channels().getGuardsForChannel(channelID)
|
|
require.Len(t, cached, 1)
|
|
}
|
|
|
|
func TestHookChannelWillBeUpdated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return nil, "update not permitted"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
original := th.BasicChannel.DisplayName
|
|
updated := th.BasicChannel.DeepCopy()
|
|
updated.DisplayName = "Should Not Persist"
|
|
|
|
_, appErr := th.App.UpdateChannel(th.Context, updated)
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
|
|
fetched, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, original, fetched.DisplayName)
|
|
})
|
|
|
|
t.Run("modified", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
newChannel.DisplayName = strings.ToUpper(newChannel.DisplayName)
|
|
return newChannel, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
updated := th.BasicChannel.DeepCopy()
|
|
updated.DisplayName = "lowercase name"
|
|
|
|
_, appErr := th.App.UpdateChannel(th.Context, updated)
|
|
require.Nil(t, appErr)
|
|
|
|
fetched, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "LOWERCASE NAME", fetched.DisplayName)
|
|
})
|
|
|
|
t.Run("old vs new diff", func(t *testing.T) {
|
|
// Plugin rejects only when the DisplayName changed — proving that oldChannel carries the
|
|
// stored value, not a copy of newChannel.
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
if oldChannel.DisplayName != newChannel.DisplayName {
|
|
return nil, "display name changed"
|
|
}
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
// Call with a changed DisplayName — plugin sees old != new and rejects.
|
|
changed := th.BasicChannel.DeepCopy()
|
|
changed.DisplayName = "Renamed Channel"
|
|
_, appErr := th.App.UpdateChannel(th.Context, changed)
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
|
|
// Call with the same DisplayName — plugin sees old == new and allows.
|
|
same := th.BasicChannel.DeepCopy()
|
|
_, appErr = th.App.UpdateChannel(th.Context, same)
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
t.Run("idempotent across repeat calls", func(t *testing.T) {
|
|
// UpdateChannelPrivacy may invoke UpdateChannel twice on the postChannelPrivacyMessage
|
|
// failure path (forward + revert). This test approximates that double-fire by calling
|
|
// UpdateChannel twice with the same plugin loaded — the hook must tolerate repeat invocations.
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
first := th.BasicChannel.DeepCopy()
|
|
first.DisplayName = "First"
|
|
_, appErr := th.App.UpdateChannel(th.Context, first)
|
|
require.Nil(t, appErr)
|
|
|
|
second := first.DeepCopy()
|
|
second.DisplayName = "Second"
|
|
_, appErr = th.App.UpdateChannel(th.Context, second)
|
|
require.Nil(t, appErr)
|
|
|
|
fetched, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "Second", fetched.DisplayName)
|
|
})
|
|
}
|
|
|
|
func TestHookChannelWillBeRestored(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// First archive the channel so RestoreChannel has something to do.
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id))
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
return "restore not permitted"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
archived, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
|
|
_, appErr := th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
|
|
fetched, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.NotEqual(t, int64(0), fetched.DeleteAt)
|
|
})
|
|
|
|
t.Run("allowed", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id))
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
return ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
archived, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
|
|
_, appErr := th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
fetched, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, int64(0), fetched.DeleteAt)
|
|
})
|
|
}
|
|
|
|
func TestHookScheduledPostWillBeCreated(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("save rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ScheduledPostWillBeCreated(c *plugin.Context, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, string) {
|
|
return nil, "scheduled post not permitted"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
sp := &model.ScheduledPost{
|
|
Draft: model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "scheduled hi",
|
|
},
|
|
ScheduledAt: model.GetMillis() + 60_000,
|
|
}
|
|
_, appErr := th.App.SaveScheduledPost(th.Context, sp, "")
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
})
|
|
|
|
t.Run("save modified", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ScheduledPostWillBeCreated(c *plugin.Context, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, string) {
|
|
scheduledPost.Message = "modified-by-plugin"
|
|
return scheduledPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
sp := &model.ScheduledPost{
|
|
Draft: model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original",
|
|
},
|
|
ScheduledAt: model.GetMillis() + 60_000,
|
|
}
|
|
saved, appErr := th.App.SaveScheduledPost(th.Context, sp, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, saved)
|
|
assert.Equal(t, "modified-by-plugin", saved.Message)
|
|
})
|
|
|
|
t.Run("update rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// First save (no plugin loaded yet so the hook is a no-op).
|
|
sp := &model.ScheduledPost{
|
|
Draft: model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original",
|
|
},
|
|
ScheduledAt: model.GetMillis() + 60_000,
|
|
}
|
|
saved, appErr := th.App.SaveScheduledPost(th.Context, sp, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, saved)
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ScheduledPostWillBeCreated(c *plugin.Context, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, string) {
|
|
return nil, "update not permitted"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
saved.Message = "edited"
|
|
_, appErr = th.App.UpdateScheduledPost(th.Context, th.BasicUser.Id, saved, "")
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
})
|
|
}
|
|
|
|
func TestHookDraftWillBeUpserted(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("rejected", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.Server.platform.SetConfigReadOnlyFF(false)
|
|
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) DraftWillBeUpserted(c *plugin.Context, draft *model.Draft) (*model.Draft, string) {
|
|
return nil, "draft not permitted"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
draft := &model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "draft hi",
|
|
}
|
|
_, appErr := th.App.UpsertDraft(th.Context, draft, "")
|
|
require.NotNil(t, appErr)
|
|
assert.Contains(t, appErr.Id, "rejected_by_plugin")
|
|
|
|
drafts, getErr := th.App.GetDraftsForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id)
|
|
require.Nil(t, getErr)
|
|
assert.Empty(t, drafts)
|
|
})
|
|
|
|
t.Run("modified", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.Server.platform.SetConfigReadOnlyFF(false)
|
|
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) DraftWillBeUpserted(c *plugin.Context, draft *model.Draft) (*model.Draft, string) {
|
|
draft.Message = "modified-by-plugin"
|
|
return draft, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
draft := &model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original",
|
|
}
|
|
saved, appErr := th.App.UpsertDraft(th.Context, draft, "")
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, saved)
|
|
assert.Equal(t, "modified-by-plugin", saved.Message)
|
|
})
|
|
|
|
t.Run("delete-empty does not fire hook", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.Server.platform.SetConfigReadOnlyFF(false)
|
|
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
|
|
|
// Plugin rejects everything; if it fires on the delete path we will see an AppError.
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) DraftWillBeUpserted(c *plugin.Context, draft *model.Draft) (*model.Draft, string) {
|
|
return nil, "should not be called for empty-message delete"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
empty := &model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "",
|
|
}
|
|
_, appErr := th.App.UpsertDraft(th.Context, empty, "")
|
|
require.Nil(t, appErr)
|
|
})
|
|
}
|
|
|
|
func TestHooksNoOpWhenNoPlugin(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// No plugin loaded — all hooks must be no-ops and the affected app calls must succeed
|
|
// (or fail for unrelated reasons). This guards against accidentally turning a no-op
|
|
// RunMultiHook into a hard requirement.
|
|
|
|
updated := th.BasicChannel.DeepCopy()
|
|
updated.DisplayName = "renamed"
|
|
_, appErr := th.App.UpdateChannel(th.Context, updated)
|
|
require.Nil(t, appErr)
|
|
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id))
|
|
archived, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
_, appErr = th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
// UpsertDraft exercises the DraftWillBeUpserted hook path with no plugin loaded.
|
|
th.Server.platform.SetConfigReadOnlyFF(false)
|
|
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
|
draft := &model.Draft{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "no-op draft",
|
|
}
|
|
_, appErr = th.App.UpsertDraft(th.Context, draft, "")
|
|
require.Nil(t, appErr)
|
|
}
|
|
|
|
func TestChannelGuardBlocksPostWhenPluginInactive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that implements MessageWillBePosted (allow all posts).
|
|
// The guard row is registered directly from the test using App.RegisterChannelGuard so
|
|
// the test is not coupled to a particular OnActivate implementation.
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Register a channel guard for BasicChannel under this plugin's ID.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
// Subtest (a): plugin active — CreatePost must succeed.
|
|
t.Run("plugin active allows post", func(t *testing.T) {
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "should be allowed",
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
})
|
|
|
|
// Subtest (b): plugin deactivated — CreatePost must return 503 inactive_guard error
|
|
// and the post must not be persisted.
|
|
t.Run("plugin inactive rejects post", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "should be rejected",
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr, "expected error when guard plugin is inactive")
|
|
require.Nil(t, createdPost)
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Verify the post was not persisted by fetching recent posts for the channel.
|
|
postList, storeErr := th.App.Srv().Store().Post().GetPosts(th.Context, model.GetPostsOptions{
|
|
ChannelId: th.BasicChannel.Id,
|
|
Page: 0,
|
|
PerPage: 10,
|
|
}, false, nil)
|
|
require.NoError(t, storeErr)
|
|
for _, p := range postList.Posts {
|
|
assert.NotEqual(t, "should be rejected", p.Message, "rejected post must not be in the store")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestChannelGuardBlocksPostUpdateWhenPluginInactive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that implements MessageWillBeUpdated (allow all updates).
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
|
return newPost, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Register a channel guard for BasicChannel under this plugin's ID.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
// Create the initial post that will be updated.
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original message",
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
|
|
// Subtest (a): plugin active — UpdatePost must succeed.
|
|
t.Run("plugin active allows update", func(t *testing.T) {
|
|
updatedPost := createdPost.Clone()
|
|
updatedPost.Message = "updated message allowed"
|
|
result, _, appErr := th.App.UpdatePost(th.Context, updatedPost, nil)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, result)
|
|
})
|
|
|
|
// Subtest (b): plugin deactivated — UpdatePost must return 503 inactive_guard error
|
|
// and the post must remain unchanged in the store.
|
|
t.Run("plugin inactive rejects update", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
updatedPost := createdPost.Clone()
|
|
updatedPost.Message = "should be rejected"
|
|
result, _, appErr := th.App.UpdatePost(th.Context, updatedPost, nil)
|
|
require.NotNil(t, appErr, "expected error when guard plugin is inactive")
|
|
require.Nil(t, result)
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Verify the post was not updated by fetching it from the store.
|
|
fetchedPost, storeErr := th.App.GetSinglePost(th.Context, createdPost.Id, false)
|
|
require.Nil(t, storeErr)
|
|
assert.NotEqual(t, "should be rejected", fetchedPost.Message, "rejected update must not be persisted")
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardPostUpdateRejectionReasonPreserved locks in the legacy rejection-reason
|
|
// shape for UpdatePost. A plugin returning (nil, "blocked-by-policy") must surface as
|
|
// AppError with Id "Post rejected by plugin. blocked-by-policy". The unguarded path
|
|
// exercises the legacy AppError shape that existing tooling may grep for.
|
|
func TestChannelGuardPostUpdateRejectionReasonPreserved(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
|
return nil, "blocked-by-policy"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginIDs[0]))
|
|
|
|
// Create the initial post that will be updated.
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original message",
|
|
}
|
|
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdPost)
|
|
|
|
// Unguarded path — no guard registered. The plugin returns (nil, "blocked-by-policy") and the
|
|
// rejection error must include the reason verbatim.
|
|
updatedPost := createdPost.Clone()
|
|
updatedPost.Message = "unguarded rejection"
|
|
result, _, appErr := th.App.UpdatePost(th.Context, updatedPost, nil)
|
|
require.NotNil(t, appErr, "expected rejection from plugin")
|
|
require.Nil(t, result)
|
|
assert.Equal(t, "Post rejected by plugin. blocked-by-policy", appErr.Id)
|
|
}
|
|
|
|
func TestChannelGuardBlocksMemberAddWhenPluginInactive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that implements ChannelMemberWillBeAdded (allow all).
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelMemberWillBeAdded(c *plugin.Context, member *model.ChannelMember) (*model.ChannelMember, string) {
|
|
return member, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Create a private channel to test member addition.
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
|
|
// Register a channel guard for this channel under this plugin's ID.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, privateChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
// Subtest (a): plugin active — AddUserToChannel must succeed.
|
|
t.Run("plugin active allows member add", func(t *testing.T) {
|
|
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser2, privateChannel, false)
|
|
// May already be a member from setup; either success or "already a member" is OK.
|
|
if appErr != nil {
|
|
assert.NotEqual(t, "app.plugin.inactive_guard.app_error", appErr.Id, "must not be a guard error when plugin is active")
|
|
}
|
|
})
|
|
|
|
// Subtest (b): plugin deactivated — AddUserToChannel must return 503 inactive_guard error.
|
|
t.Run("plugin inactive rejects member add", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Use a new user who is definitely not yet a member; add them to the team first.
|
|
newUser := th.CreateUser(t)
|
|
_, _, teamErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, newUser.Id, "")
|
|
require.Nil(t, teamErr)
|
|
_, appErr := th.App.AddUserToChannel(th.Context, newUser, privateChannel, false)
|
|
require.NotNil(t, appErr, "expected error when guard plugin is inactive")
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Verify the user was not added.
|
|
_, memberErr := th.App.GetChannelMember(th.Context, privateChannel.Id, newUser.Id)
|
|
require.NotNil(t, memberErr, "user must not be a member of the channel")
|
|
})
|
|
}
|
|
|
|
func TestChannelGuardBlocksChannelUpdateWhenPluginInactive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that implements ChannelWillBeUpdated (allow all updates).
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return newChannel, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Register a channel guard for BasicChannel under this plugin's ID.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
// Subtest (a): plugin active — UpdateChannel must succeed.
|
|
t.Run("plugin active allows update", func(t *testing.T) {
|
|
channelToUpdate := th.BasicChannel.DeepCopy()
|
|
channelToUpdate.DisplayName = "Updated Name Allowed"
|
|
result, appErr := th.App.UpdateChannel(th.Context, channelToUpdate)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, result)
|
|
})
|
|
|
|
// Subtest (b): plugin deactivated — UpdateChannel must return 503 inactive_guard error
|
|
// and the channel must remain unchanged in the store.
|
|
t.Run("plugin inactive rejects update", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
channelToUpdate := th.BasicChannel.DeepCopy()
|
|
channelToUpdate.DisplayName = "Should Be Rejected"
|
|
result, appErr := th.App.UpdateChannel(th.Context, channelToUpdate)
|
|
require.NotNil(t, appErr, "expected error when guard plugin is inactive")
|
|
require.Nil(t, result)
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Verify the channel was not updated.
|
|
fetched, storeErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, storeErr)
|
|
assert.NotEqual(t, "Should Be Rejected", fetched.DisplayName, "rejected update must not be persisted")
|
|
})
|
|
}
|
|
|
|
func TestChannelGuardRejectsTypeMutationFromPlugin(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that flips the channel Type in its replacement.
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
mutated := newChannel.DeepCopy()
|
|
// Flip Open <-> Private.
|
|
if mutated.Type == model.ChannelTypeOpen {
|
|
mutated.Type = model.ChannelTypePrivate
|
|
} else {
|
|
mutated.Type = model.ChannelTypeOpen
|
|
}
|
|
return mutated, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Register a channel guard so this goes through the guarded path.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
originalType := th.BasicChannel.Type
|
|
|
|
channelToUpdate := th.BasicChannel.DeepCopy()
|
|
channelToUpdate.DisplayName = "Type Mutation Attempt"
|
|
result, appErr := th.App.UpdateChannel(th.Context, channelToUpdate)
|
|
require.NotNil(t, appErr, "expected type-mutation error")
|
|
require.Nil(t, result)
|
|
assert.Equal(t, "app.channel.update_channel.plugin_type_mutation.app_error", appErr.Id)
|
|
assert.Equal(t, 400, appErr.StatusCode)
|
|
// The error string must include the offending plugin ID (from the i18n template).
|
|
assert.Contains(t, appErr.Error(), pluginID)
|
|
|
|
// Verify the channel type was not changed.
|
|
fetched, storeErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, storeErr)
|
|
assert.Equal(t, originalType, fetched.Type, "type must not be mutated by plugin replacement")
|
|
}
|
|
|
|
func TestChannelGuardAllowsNonTypeMutationFromPlugin(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that modifies DisplayName but not Type.
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
modified := newChannel.DeepCopy()
|
|
modified.DisplayName = "plugin-modified-name"
|
|
return modified, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Register a channel guard so this goes through the guarded path.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
channelToUpdate := th.BasicChannel.DeepCopy()
|
|
channelToUpdate.DisplayName = "Original Caller Name"
|
|
result, appErr := th.App.UpdateChannel(th.Context, channelToUpdate)
|
|
require.Nil(t, appErr, "non-type-mutation replacement must succeed")
|
|
require.NotNil(t, result)
|
|
|
|
// Verify the DB has the plugin-modified DisplayName.
|
|
fetched, storeErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, storeErr)
|
|
assert.Equal(t, "plugin-modified-name", fetched.DisplayName, "plugin DisplayName replacement must be persisted")
|
|
}
|
|
|
|
// Guard blocks RestoreChannel when the guard plugin is inactive.
|
|
func TestChannelGuardBlocksRestoreWhenPluginInactive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Compile and activate a plugin that implements ChannelWillBeRestored (allow all).
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
return ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, pluginIDs, 1)
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// Archive BasicChannel so RestoreChannel has something to do.
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id))
|
|
|
|
// Register a channel guard for this channel under this plugin's ID.
|
|
appErr := th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID)
|
|
require.Nil(t, appErr, "RegisterChannelGuard must succeed")
|
|
|
|
// Subtest (a): plugin active — RestoreChannel must succeed.
|
|
t.Run("plugin active allows restore", func(t *testing.T) {
|
|
archived, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
require.NotEqual(t, int64(0), archived.DeleteAt, "channel must be archived before restore")
|
|
|
|
_, appErr := th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.Nil(t, appErr, "expected no error when guard plugin is active")
|
|
|
|
// Re-archive for the next subtest.
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id))
|
|
})
|
|
|
|
// Subtest (b): plugin deactivated — RestoreChannel must return 503 inactive_guard error
|
|
// and the channel must remain archived.
|
|
t.Run("plugin inactive rejects restore", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(pluginID))
|
|
require.False(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
archived, err := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, err)
|
|
require.NotEqual(t, int64(0), archived.DeleteAt, "channel must be archived for this subtest")
|
|
|
|
result, appErr := th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.NotNil(t, appErr, "expected error when guard plugin is inactive")
|
|
require.Nil(t, result)
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Verify the channel was not restored (still archived).
|
|
fetched, storeErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
|
|
require.Nil(t, storeErr)
|
|
assert.NotEqual(t, int64(0), fetched.DeleteAt, "rejected restore must not change DeleteAt")
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cross-cutting e2e tests for channel-guard dispatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestChannelGuardWrapperRejectsOnHookRPCError verifies that when a guard plugin's hook
|
|
// implementation panics (which net/rpc recovers and returns as a non-nil error from
|
|
// client.Call), the guarded site returns 503 app.plugin.guard_hook_failed.app_error.
|
|
//
|
|
// The first sub-test is a panic-discovery smoke test that proves the mechanism works before
|
|
// relying on it for all five sites. The remaining sub-tests cover each guarded site.
|
|
//
|
|
// Each sub-test also verifies that an unguarded channel with the same panicking plugin still
|
|
// succeeds (existing fail-open RunMultiHook swallows RPC errors per long-standing contract).
|
|
func TestChannelGuardWrapperRejectsOnHookRPCError(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
// panicAllPlugin is a single compiled plugin that panics in all five guarded hooks.
|
|
// One plugin, one compile — reused across every sub-test.
|
|
const panicAllPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type PanicPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *PanicPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
panic("forced RPC error")
|
|
}
|
|
|
|
func (p *PanicPlugin) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
|
panic("forced RPC error")
|
|
}
|
|
|
|
func (p *PanicPlugin) ChannelMemberWillBeAdded(c *plugin.Context, member *model.ChannelMember) (*model.ChannelMember, string) {
|
|
panic("forced RPC error")
|
|
}
|
|
|
|
func (p *PanicPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
panic("forced RPC error")
|
|
}
|
|
|
|
func (p *PanicPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
panic("forced RPC error")
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&PanicPlugin{})
|
|
}
|
|
`
|
|
|
|
// One sub-test per guarded site. Each registers the panicking guard plugin on a
|
|
// channel and asserts the guard wrapper returns 503 (Phase B fail-closed). Each also
|
|
// verifies the unguarded path with the same plugin returns no error (fail-open
|
|
// preservation for non-guarded callers).
|
|
|
|
t.Run("MessageWillBePosted", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{panicAllPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
guardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
appErr := th.App.RegisterChannelGuard(th.Context, guardedCh.Id, pluginID)
|
|
require.Nil(t, appErr)
|
|
|
|
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: guardedCh.Id,
|
|
Message: "msg",
|
|
}, guardedCh, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.guard_hook_failed.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Unguarded: fail-open.
|
|
unguardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
_, _, appErr2 := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: unguardedCh.Id,
|
|
Message: "unguarded",
|
|
}, unguardedCh, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr2)
|
|
})
|
|
|
|
t.Run("MessageWillBeUpdated", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{panicAllPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
guardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
appErr := th.App.RegisterChannelGuard(th.Context, guardedCh.Id, pluginID)
|
|
require.Nil(t, appErr)
|
|
|
|
// Create a post to update (without the panicking plugin active on this channel yet).
|
|
// Create the initial post on BasicChannel (no guard) to avoid the guard.
|
|
initialPost := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: guardedCh.Id,
|
|
Message: "original",
|
|
}
|
|
// To create the initial post we need to temporarily bypass the guard.
|
|
// Remove guard, create post, re-add guard.
|
|
require.Nil(t, th.App.UnregisterChannelGuard(th.Context, guardedCh.Id, pluginID))
|
|
created, _, err := th.App.CreatePost(th.Context, initialPost, guardedCh, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, guardedCh.Id, pluginID))
|
|
|
|
updated := created.Clone()
|
|
updated.Message = "updated"
|
|
_, _, appErr = th.App.UpdatePost(th.Context, updated, nil)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.guard_hook_failed.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Unguarded: fail-open.
|
|
unguardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
initial2 := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: unguardedCh.Id,
|
|
Message: "initial2",
|
|
}
|
|
created2, _, err2 := th.App.CreatePost(th.Context, initial2, unguardedCh, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err2)
|
|
updated2 := created2.Clone()
|
|
updated2.Message = "updated2"
|
|
_, _, appErr2 := th.App.UpdatePost(th.Context, updated2, nil)
|
|
require.Nil(t, appErr2)
|
|
})
|
|
|
|
t.Run("ChannelMemberWillBeAdded", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{panicAllPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
guardedCh := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
appErr := th.App.RegisterChannelGuard(th.Context, guardedCh.Id, pluginID)
|
|
require.Nil(t, appErr)
|
|
|
|
newUser := th.CreateUser(t)
|
|
_, _, teamErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, newUser.Id, "")
|
|
require.Nil(t, teamErr)
|
|
|
|
_, appErr = th.App.AddUserToChannel(th.Context, newUser, guardedCh, false)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.guard_hook_failed.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Unguarded: fail-open.
|
|
unguardedCh := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
newUser2 := th.CreateUser(t)
|
|
_, _, teamErr2 := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, newUser2.Id, "")
|
|
require.Nil(t, teamErr2)
|
|
_, appErr2 := th.App.AddUserToChannel(th.Context, newUser2, unguardedCh, false)
|
|
require.Nil(t, appErr2)
|
|
})
|
|
|
|
t.Run("ChannelWillBeUpdated", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{panicAllPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
guardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
appErr := th.App.RegisterChannelGuard(th.Context, guardedCh.Id, pluginID)
|
|
require.Nil(t, appErr)
|
|
|
|
ch := guardedCh.DeepCopy()
|
|
ch.DisplayName = "Panic Test"
|
|
_, appErr = th.App.UpdateChannel(th.Context, ch)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.guard_hook_failed.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Unguarded: fail-open.
|
|
unguardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
ch2 := unguardedCh.DeepCopy()
|
|
ch2.DisplayName = "Unguarded Update"
|
|
_, appErr2 := th.App.UpdateChannel(th.Context, ch2)
|
|
require.Nil(t, appErr2)
|
|
})
|
|
|
|
t.Run("ChannelWillBeRestored", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{panicAllPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
guardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, guardedCh, th.BasicUser.Id))
|
|
appErr := th.App.RegisterChannelGuard(th.Context, guardedCh.Id, pluginID)
|
|
require.Nil(t, appErr)
|
|
|
|
archived, err := th.App.GetChannel(th.Context, guardedCh.Id)
|
|
require.Nil(t, err)
|
|
_, appErr = th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.guard_hook_failed.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Unguarded: fail-open.
|
|
unguardedCh := th.CreateChannel(t, th.BasicTeam)
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, unguardedCh, th.BasicUser.Id))
|
|
archived2, err2 := th.App.GetChannel(th.Context, unguardedCh.Id)
|
|
require.Nil(t, err2)
|
|
_, appErr2 := th.App.RestoreChannel(th.Context, archived2, th.BasicUser.Id)
|
|
require.Nil(t, appErr2)
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardAllowsAllOpsWhenPluginActiveNoRejection registers a guard whose plugin
|
|
// allows every hook and exercises all five guarded sites to confirm no regression.
|
|
func TestChannelGuardAllowsAllOpsWhenPluginActiveNoRejection(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
const allowAllPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type AllowPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *AllowPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func (p *AllowPlugin) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
|
return newPost, ""
|
|
}
|
|
|
|
func (p *AllowPlugin) ChannelMemberWillBeAdded(c *plugin.Context, member *model.ChannelMember) (*model.ChannelMember, string) {
|
|
return member, ""
|
|
}
|
|
|
|
func (p *AllowPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return newChannel, ""
|
|
}
|
|
|
|
func (p *AllowPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
return ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&AllowPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{allowAllPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
// All five sites share the same channel so one guard covers all.
|
|
ch := th.BasicChannel
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, pluginID))
|
|
|
|
// Site 1: MessageWillBePosted (CreatePost).
|
|
t.Run("MessageWillBePosted", func(t *testing.T) {
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "allow all test",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
// Site 2: MessageWillBeUpdated (UpdatePost). Create a post first on BasicChannel (no guard
|
|
// conflict — guard already registered, plugin allows).
|
|
var createdPost *model.Post
|
|
t.Run("MessageWillBeUpdated_setup", func(t *testing.T) {
|
|
var appErr *model.AppError
|
|
createdPost, _, appErr = th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "original for update",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
t.Run("MessageWillBeUpdated", func(t *testing.T) {
|
|
require.NotNil(t, createdPost)
|
|
up := createdPost.Clone()
|
|
up.Message = "updated by allow-all guard"
|
|
_, _, appErr := th.App.UpdatePost(th.Context, up, nil)
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
// Site 3: ChannelMemberWillBeAdded. Use a fresh user to guarantee AddUserToChannel
|
|
// reaches the hook (existing-membership early-return would silently skip it).
|
|
t.Run("ChannelMemberWillBeAdded", func(t *testing.T) {
|
|
newUser := th.CreateUser(t)
|
|
_, _, teamErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, newUser.Id, "")
|
|
require.Nil(t, teamErr)
|
|
_, appErr := th.App.AddUserToChannel(th.Context, newUser, ch, false)
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
// Site 4: ChannelWillBeUpdated.
|
|
t.Run("ChannelWillBeUpdated", func(t *testing.T) {
|
|
update := ch.DeepCopy()
|
|
update.DisplayName = "Allow-All Guard Test"
|
|
result, appErr := th.App.UpdateChannel(th.Context, update)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, result)
|
|
})
|
|
|
|
// Site 5: ChannelWillBeRestored. Archive then restore.
|
|
t.Run("ChannelWillBeRestored", func(t *testing.T) {
|
|
restoreCh := th.CreateChannel(t, th.BasicTeam)
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, restoreCh.Id, pluginID))
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, restoreCh, th.BasicUser.Id))
|
|
archived, err := th.App.GetChannel(th.Context, restoreCh.Id)
|
|
require.Nil(t, err)
|
|
_, appErr := th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardFiresHookWhenPluginActive confirms that for each of the five guarded sites,
|
|
// when a guard plugin's hook returns a rejection, the rejection comes from the hook (not from
|
|
// the guard inactive pre-check). The error reason matches the plugin-returned string.
|
|
func TestChannelGuardFiresHookWhenPluginActive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
const rejectPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type RejectPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *RejectPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
return nil, "guard-rejected-post"
|
|
}
|
|
|
|
func (p *RejectPlugin) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
|
return nil, "guard-rejected-update"
|
|
}
|
|
|
|
func (p *RejectPlugin) ChannelMemberWillBeAdded(c *plugin.Context, member *model.ChannelMember) (*model.ChannelMember, string) {
|
|
return nil, "guard-rejected-member"
|
|
}
|
|
|
|
func (p *RejectPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return nil, "guard-rejected-channel-update"
|
|
}
|
|
|
|
func (p *RejectPlugin) ChannelWillBeRestored(c *plugin.Context, channel *model.Channel) string {
|
|
return "guard-rejected-restore"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&RejectPlugin{})
|
|
}
|
|
`
|
|
|
|
t.Run("MessageWillBePosted", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{rejectPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
ch := th.BasicChannel
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, pluginID))
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "msg",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr, "plugin rejection must return error")
|
|
// The error comes from the hook (plugin active) — Id must contain the rejection reason.
|
|
assert.NotEqual(t, "app.plugin.inactive_guard.app_error", appErr.Id, "must not be inactive-guard error")
|
|
assert.Contains(t, appErr.Id, "guard-rejected-post")
|
|
})
|
|
|
|
t.Run("MessageWillBeUpdated", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a post BEFORE activating the reject plugin (the plugin also rejects
|
|
// MessageWillBePosted, so CreatePost would fail if the plugin were active).
|
|
initialPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{rejectPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID))
|
|
|
|
updated := initialPost.Clone()
|
|
updated.Message = "attempt"
|
|
_, _, appErr := th.App.UpdatePost(th.Context, updated, nil)
|
|
require.NotNil(t, appErr)
|
|
assert.NotEqual(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Contains(t, appErr.Id, "guard-rejected-update")
|
|
})
|
|
|
|
t.Run("ChannelMemberWillBeAdded", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{rejectPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
ch := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, pluginID))
|
|
|
|
newUser := th.CreateUser(t)
|
|
_, _, teamErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, newUser.Id, "")
|
|
require.Nil(t, teamErr)
|
|
|
|
_, appErr := th.App.AddUserToChannel(th.Context, newUser, ch, false)
|
|
require.NotNil(t, appErr)
|
|
assert.NotEqual(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
// ChannelMemberWillBeAdded rejection wraps the reason via app.channel.add_user.to.channel.rejected_by_plugin
|
|
assert.Equal(t, "app.channel.add_user.to.channel.rejected_by_plugin", appErr.Id)
|
|
})
|
|
|
|
t.Run("ChannelWillBeUpdated", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{rejectPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
ch := th.BasicChannel
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, pluginID))
|
|
|
|
update := ch.DeepCopy()
|
|
update.DisplayName = "Rejected"
|
|
_, appErr := th.App.UpdateChannel(th.Context, update)
|
|
require.NotNil(t, appErr)
|
|
assert.NotEqual(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, "app.channel.update_channel.rejected_by_plugin", appErr.Id)
|
|
})
|
|
|
|
t.Run("ChannelWillBeRestored", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{rejectPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
|
|
|
ch := th.CreateChannel(t, th.BasicTeam)
|
|
require.Nil(t, th.App.DeleteChannel(th.Context, ch, th.BasicUser.Id))
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, pluginID))
|
|
|
|
archived, err := th.App.GetChannel(th.Context, ch.Id)
|
|
require.Nil(t, err)
|
|
_, appErr := th.App.RestoreChannel(th.Context, archived, th.BasicUser.Id)
|
|
require.NotNil(t, appErr)
|
|
assert.NotEqual(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, "app.channel.restore_channel.rejected_by_plugin", appErr.Id)
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardTwoPhaseDispatchOrdering installs two plugins: a guard plugin G and a
|
|
// non-guard plugin N. N uppercases the message in Phase A; G sees the uppercased message in
|
|
// Phase B. When N rejects, Phase B is not invoked.
|
|
func TestChannelGuardTwoPhaseDispatchOrdering(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
// Guard plugin G: allow everything; records the message it received.
|
|
// The destination file path is baked into the source at compile time so the
|
|
// plugin doesn't need to read it from the environment — process-global env
|
|
// mutation is incompatible with t.Parallel().
|
|
makeGuardSrc := func(receivedFile string) string {
|
|
return fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type GuardPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *GuardPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
_ = os.WriteFile(%q, []byte(post.Message), 0644)
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&GuardPlugin{})
|
|
}
|
|
`, receivedFile)
|
|
}
|
|
|
|
// Non-guard plugin N: uppercases the message.
|
|
const srcN = `
|
|
package main
|
|
|
|
import (
|
|
"strings"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type NPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *NPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
modified := post.Clone()
|
|
modified.Message = strings.ToUpper(post.Message)
|
|
return modified, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&NPlugin{})
|
|
}
|
|
`
|
|
|
|
// Non-guard plugin N_reject: rejects all posts.
|
|
const srcNReject = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type NRejectPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *NRejectPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
return nil, "n-rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&NRejectPlugin{})
|
|
}
|
|
`
|
|
|
|
// Sub-test (a): N uppercases, G receives the uppercased message.
|
|
t.Run("Phase_A_composes_into_Phase_B_input", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Temp file for the guard plugin to write the received message.
|
|
receivedFile, err := os.CreateTemp("", "guard_received_*.txt")
|
|
require.NoError(t, err)
|
|
receivedFile.Close()
|
|
defer os.Remove(receivedFile.Name())
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{makeGuardSrc(receivedFile.Name()), srcN}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 2)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
|
|
// Determine which ID belongs to G vs N based on position.
|
|
gID := pluginIDs[0]
|
|
nID := pluginIDs[1]
|
|
_ = nID // N is not registered as a guard.
|
|
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, gID))
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
|
|
// Read the message that the guard plugin received; it must be uppercased.
|
|
received, readErr := os.ReadFile(receivedFile.Name())
|
|
require.NoError(t, readErr)
|
|
assert.Equal(t, "HELLO", string(received), "Phase B guard must see Phase A's output (uppercased)")
|
|
})
|
|
|
|
// Sub-test (b): N rejects → Phase B (guard) is not invoked.
|
|
t.Run("Phase_A_rejection_skips_Phase_B", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
receivedFile, err := os.CreateTemp("", "guard_received_*.txt")
|
|
require.NoError(t, err)
|
|
receivedFile.Close()
|
|
defer os.Remove(receivedFile.Name())
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{makeGuardSrc(receivedFile.Name()), srcNReject}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 2)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
|
|
gID := pluginIDs[0]
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, gID))
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "msg",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr, "N_reject must reject")
|
|
assert.Contains(t, appErr.Id, "n-rejected")
|
|
|
|
// Guard plugin must NOT have been called (file stays empty).
|
|
received, readErr := os.ReadFile(receivedFile.Name())
|
|
require.NoError(t, readErr)
|
|
assert.Empty(t, string(received), "Phase B guard must not be invoked when Phase A rejects")
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardMultiClaimAllMustBeActive installs two guard plugins G1 and G2 on the
|
|
// same channel. Both active → CreatePost succeeds. Deactivate either → 503. Re-activate →
|
|
// success. The plugin ID is logged server-side (operator attribution) but intentionally
|
|
// omitted from the user-facing AppError, so this test only asserts the generic 503 shape.
|
|
func TestChannelGuardMultiClaimAllMustBeActive(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
const allowPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type AllowPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *AllowPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&AllowPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{allowPlugin, allowPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 2)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
|
|
g1ID := pluginIDs[0]
|
|
g2ID := pluginIDs[1]
|
|
|
|
ch := th.BasicChannel
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, g1ID))
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, ch.Id, g2ID))
|
|
|
|
// Both active: must succeed.
|
|
t.Run("both_active_succeeds", func(t *testing.T) {
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "both active",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
})
|
|
|
|
// Deactivate G1: must get the generic 503 (plugin ID is in the server log, not the AppError).
|
|
t.Run("g1_inactive_returns_503", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(g1ID))
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "g1 inactive",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Re-activate G1.
|
|
_, _, activateErr := th.App.GetPluginsEnvironment().Activate(g1ID)
|
|
require.NoError(t, activateErr)
|
|
})
|
|
|
|
// Deactivate G2: must get 503.
|
|
t.Run("g2_inactive_returns_503", func(t *testing.T) {
|
|
require.True(t, th.App.GetPluginsEnvironment().Deactivate(g2ID))
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "g2 inactive",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr)
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
|
|
// Re-activate G2.
|
|
_, _, activateErr := th.App.GetPluginsEnvironment().Activate(g2ID)
|
|
require.NoError(t, activateErr)
|
|
})
|
|
|
|
// Both re-activated: must succeed again.
|
|
t.Run("both_reactivated_succeeds", func(t *testing.T) {
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: ch.Id,
|
|
Message: "both reactivated",
|
|
}, ch, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardMultiClaimPhaseBSequence verifies Phase B composition and sequencing with two
|
|
// guard plugins G1 and G2. Plugin IDs are random UUIDs at test time, so the test does not pin which
|
|
// guard sorts first; it asserts properties that hold regardless of order.
|
|
//
|
|
// a) Both allow: each prepends its tag to the message → final message contains both tags in
|
|
// PluginId-sorted-call order, proving Phase B composes left-to-right.
|
|
//
|
|
// b) Whichever guard runs first rejects → the second guard is NOT invoked (test reads
|
|
// either possible counter file and asserts at least one is empty, allowing 0 or 1
|
|
// invocations of the second to satisfy the short-circuit contract).
|
|
//
|
|
// c) Phase A's RunMultiHookExcluding skips both guards: a third non-guard plugin N runs
|
|
// exactly once per CreatePost, while G1/G2's counters do not increment during Phase A.
|
|
func TestChannelGuardMultiClaimPhaseBSequence(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
// Each plugin source is built per-subtest with its counter file path baked
|
|
// in as a Go literal. Reading the path from the environment instead would
|
|
// require t.Setenv, which panics under t.Parallel.
|
|
|
|
// G1: prepends "G1:" to the message; writes its call count to a file.
|
|
makeG1PrependSrc := func(countFile string) string {
|
|
return fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type G1Plugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *G1Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
countFile := %q
|
|
count := 0
|
|
if data, err := os.ReadFile(countFile); err == nil {
|
|
count, _ = strconv.Atoi(strings.TrimSpace(string(data)))
|
|
}
|
|
count++
|
|
_ = os.WriteFile(countFile, []byte(fmt.Sprintf("%%d", count)), 0644)
|
|
|
|
modified := post.Clone()
|
|
modified.Message = "G1:" + post.Message
|
|
return modified, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&G1Plugin{})
|
|
}
|
|
`, countFile)
|
|
}
|
|
|
|
// G1 that rejects.
|
|
makeG1RejectSrc := func(countFile string) string {
|
|
return fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type G1RejectPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *G1RejectPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
countFile := %q
|
|
count := 0
|
|
if data, err := os.ReadFile(countFile); err == nil {
|
|
count, _ = strconv.Atoi(strings.TrimSpace(string(data)))
|
|
}
|
|
count++
|
|
_ = os.WriteFile(countFile, []byte(fmt.Sprintf("%%d", count)), 0644)
|
|
return nil, "g1-rejected"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&G1RejectPlugin{})
|
|
}
|
|
`, countFile)
|
|
}
|
|
|
|
// G2: prepends "G2:" to the message; writes its call count to a file.
|
|
makeG2Src := func(countFile string) string {
|
|
return fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type G2Plugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *G2Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
countFile := %q
|
|
count := 0
|
|
if data, err := os.ReadFile(countFile); err == nil {
|
|
count, _ = strconv.Atoi(strings.TrimSpace(string(data)))
|
|
}
|
|
count++
|
|
_ = os.WriteFile(countFile, []byte(fmt.Sprintf("%%d", count)), 0644)
|
|
|
|
modified := post.Clone()
|
|
modified.Message = "G2:" + post.Message
|
|
return modified, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&G2Plugin{})
|
|
}
|
|
`, countFile)
|
|
}
|
|
|
|
// G3: counts in a temp file but never rejects (used as the third guard in phase-b tests).
|
|
makeG3Src := func(countFile string) string {
|
|
return fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type G3Plugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *G3Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
countFile := %q
|
|
count := 0
|
|
if data, err := os.ReadFile(countFile); err == nil {
|
|
count, _ = strconv.Atoi(strings.TrimSpace(string(data)))
|
|
}
|
|
count++
|
|
_ = os.WriteFile(countFile, []byte(fmt.Sprintf("%%d", count)), 0644)
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&G3Plugin{})
|
|
}
|
|
`, countFile)
|
|
}
|
|
|
|
// Non-guard plugin N: writes its call count to a file.
|
|
makeNSrc := func(countFile string) string {
|
|
return fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type NPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *NPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
countFile := %q
|
|
count := 0
|
|
if data, err := os.ReadFile(countFile); err == nil {
|
|
count, _ = strconv.Atoi(strings.TrimSpace(string(data)))
|
|
}
|
|
count++
|
|
_ = os.WriteFile(countFile, []byte(fmt.Sprintf("%%d", count)), 0644)
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&NPlugin{})
|
|
}
|
|
`, countFile)
|
|
}
|
|
|
|
// Helper to read a counter file.
|
|
readCount := func(t *testing.T, path string) int {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
s := strings.TrimSpace(string(data))
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
n, err := strconv.Atoi(s)
|
|
require.NoError(t, err)
|
|
return n
|
|
}
|
|
|
|
// Sub-test (a): both allow, modifications compose left-to-right.
|
|
// G1 prepends "G1:", G2 prepends "G2:" → "G2:G1:<original>".
|
|
// Phase B order is determined by PluginId alphabetical order (resolveGuards sorts).
|
|
t.Run("composition_left_to_right", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
g1CountFile, _ := os.CreateTemp("", "g1_count_*.txt")
|
|
g1CountFile.Close()
|
|
defer os.Remove(g1CountFile.Name())
|
|
g2CountFile, _ := os.CreateTemp("", "g2_count_*.txt")
|
|
g2CountFile.Close()
|
|
defer os.Remove(g2CountFile.Name())
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{makeG1PrependSrc(g1CountFile.Name()), makeG2Src(g2CountFile.Name())}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 2)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
|
|
// pluginIDs[0] → G1Prepend (prepends "G1:"), pluginIDs[1] → G2 (prepends "G2:").
|
|
// resolveGuards fires Phase B in PluginId alphabetical order. Walk the sorted IDs to
|
|
// predict the expected final message and assert exact equality.
|
|
id0, id1 := pluginIDs[0], pluginIDs[1]
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, id0))
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, id1))
|
|
|
|
sortedIDs := []string{id0, id1}
|
|
sort.Strings(sortedIDs)
|
|
// Each plugin prepends its tag to whatever message it receives. Walking in
|
|
// sorted order: the first plugin sees "original" and produces "G?:original";
|
|
// the second plugin sees that and prepends its own tag. Build the expected
|
|
// result by walking backwards through the sorted list (each plugin wraps the prior).
|
|
pluginTag := map[string]string{id0: "G1:", id1: "G2:"}
|
|
expected := "original"
|
|
for _, id := range sortedIDs {
|
|
expected = pluginTag[id] + expected
|
|
}
|
|
|
|
created, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "original",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, created)
|
|
|
|
// Exact equality: confirms both that both plugins ran AND that they ran in
|
|
// PluginId-sorted order. Contains would accept the wrong order.
|
|
require.Equal(t, expected, created.Message)
|
|
})
|
|
|
|
// Sub-test (b): a guard's rejection propagates and stops Phase B iteration.
|
|
//
|
|
// Three guard plugins are used so that the rejecter can be in the middle of the
|
|
// sorted order (two plugins cannot detect a missing short-circuit: the loop ends
|
|
// naturally after two iterations regardless). The rejecter is G1Reject (pluginIDs[0]);
|
|
// G2 (pluginIDs[1]) and G3 (pluginIDs[2]) are plain counters. After sorting the
|
|
// three plugin IDs, any plugin whose sorted position is after the rejecter MUST have a
|
|
// count of 0 (Phase B short-circuited). Any plugin before the rejecter must have count 1.
|
|
// The rejecter itself must have count 1.
|
|
t.Run("guard_rejection_stops_phase_b", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
g1CountFile, _ := os.CreateTemp("", "g1_count_*.txt")
|
|
g1CountFile.Close()
|
|
defer os.Remove(g1CountFile.Name())
|
|
g2CountFile, _ := os.CreateTemp("", "g2_count_*.txt")
|
|
g2CountFile.Close()
|
|
defer os.Remove(g2CountFile.Name())
|
|
g3CountFile, _ := os.CreateTemp("", "g3_count_*.txt")
|
|
g3CountFile.Close()
|
|
defer os.Remove(g3CountFile.Name())
|
|
|
|
// pluginIDs[0] → G1Reject (rejecter, writes to g1CountFile)
|
|
// pluginIDs[1] → G2 (counter, writes to g2CountFile)
|
|
// pluginIDs[2] → G3 (counter, writes to g3CountFile)
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{makeG1RejectSrc(g1CountFile.Name()), makeG2Src(g2CountFile.Name()), makeG3Src(g3CountFile.Name())}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 3)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
require.NoError(t, errs[2])
|
|
|
|
rejecterID := pluginIDs[0]
|
|
for _, id := range pluginIDs {
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, id))
|
|
}
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "msg",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr, "rejection from a guard in Phase B must propagate")
|
|
assert.Contains(t, appErr.Id, "g1-rejected", "the reject plugin must be the source of the error")
|
|
|
|
// Map each plugin ID to its count file so we can check by sorted position.
|
|
countFile := map[string]string{
|
|
pluginIDs[0]: g1CountFile.Name(),
|
|
pluginIDs[1]: g2CountFile.Name(),
|
|
pluginIDs[2]: g3CountFile.Name(),
|
|
}
|
|
sortedIDs := []string{pluginIDs[0], pluginIDs[1], pluginIDs[2]}
|
|
sort.Strings(sortedIDs)
|
|
|
|
// Find rejecter's index in the sorted order.
|
|
rejecterIdx := -1
|
|
for i, id := range sortedIDs {
|
|
if id == rejecterID {
|
|
rejecterIdx = i
|
|
break
|
|
}
|
|
}
|
|
require.NotEqual(t, -1, rejecterIdx)
|
|
|
|
// Rejecter must have run exactly once.
|
|
rejecterCount := readCount(t, countFile[rejecterID])
|
|
assert.Equal(t, 1, rejecterCount, "rejecter plugin must have been invoked exactly once")
|
|
|
|
// Plugins sorted before the rejecter: each must have run exactly once.
|
|
for _, id := range sortedIDs[:rejecterIdx] {
|
|
c := readCount(t, countFile[id])
|
|
assert.Equal(t, 1, c, "plugin sorted before rejecter must have run once")
|
|
}
|
|
|
|
// Plugins sorted after the rejecter: Phase B must have short-circuited; count must be 0.
|
|
for _, id := range sortedIDs[rejecterIdx+1:] {
|
|
c := readCount(t, countFile[id])
|
|
assert.Equal(t, 0, c, "plugin sorted after rejecter must not have been invoked (short-circuit)")
|
|
}
|
|
})
|
|
|
|
// Sub-test (c): Phase A's RunMultiHookExcluding skips guards; non-guard N runs once.
|
|
t.Run("phase_a_excludes_guards", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
g1CountFile, _ := os.CreateTemp("", "g1_count_*.txt")
|
|
g1CountFile.Close()
|
|
defer os.Remove(g1CountFile.Name())
|
|
g2CountFile, _ := os.CreateTemp("", "g2_count_*.txt")
|
|
g2CountFile.Close()
|
|
defer os.Remove(g2CountFile.Name())
|
|
nCountFile, _ := os.CreateTemp("", "n_count_*.txt")
|
|
nCountFile.Close()
|
|
defer os.Remove(nCountFile.Name())
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{makeG1PrependSrc(g1CountFile.Name()), makeG2Src(g2CountFile.Name()), makeNSrc(nCountFile.Name())}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.Len(t, errs, 3)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
require.NoError(t, errs[2])
|
|
|
|
g1RegID := pluginIDs[0]
|
|
g2RegID := pluginIDs[1]
|
|
// pluginIDs[2] is N — not registered as a guard.
|
|
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, g1RegID))
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, g2RegID))
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "phase-a-test",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr)
|
|
|
|
// N (non-guard) runs exactly once during Phase A.
|
|
nCount := readCount(t, nCountFile.Name())
|
|
assert.Equal(t, 1, nCount, "non-guard plugin N must run once in Phase A")
|
|
|
|
// G1 and G2 each run exactly once during Phase B (not in Phase A).
|
|
g1Count := readCount(t, g1CountFile.Name())
|
|
g2Count := readCount(t, g2CountFile.Name())
|
|
assert.Equal(t, 1, g1Count, "G1 must run once in Phase B only")
|
|
assert.Equal(t, 1, g2Count, "G2 must run once in Phase B only")
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardNoCheckWhenNoRow confirms that channels with no guard registered
|
|
// proceed normally and no guard-related error IDs fire.
|
|
func TestChannelGuardNoCheckWhenNoRow(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// No plugin installed. Channels have no guard rows.
|
|
// CreatePost must succeed without any guard-related error.
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "no guard test",
|
|
}
|
|
created, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr, "CreatePost on unguarded channel must succeed")
|
|
require.NotNil(t, created)
|
|
assert.NotEqual(t, "", created.Id, "created post must have an ID")
|
|
|
|
// UpdatePost must also succeed.
|
|
updated := created.Clone()
|
|
updated.Message = "updated no guard"
|
|
result, _, appErr2 := th.App.UpdatePost(th.Context, updated, nil)
|
|
require.Nil(t, appErr2, "UpdatePost on unguarded channel must succeed")
|
|
require.NotNil(t, result)
|
|
|
|
// UpdateChannel must succeed.
|
|
ch := th.BasicChannel.DeepCopy()
|
|
ch.DisplayName = "No Guard Channel Update"
|
|
updatedCh, appErr3 := th.App.UpdateChannel(th.Context, ch)
|
|
require.Nil(t, appErr3, "UpdateChannel on unguarded channel must succeed")
|
|
require.NotNil(t, updatedCh)
|
|
}
|
|
|
|
// TestChannelGuardFailsClosedWhenPluginsDisabled covers the resolveGuards branch where the
|
|
// plugin system is off (PluginSettings.Enable == false) but a guard row still exists for the
|
|
// channel. The user-facing AppError shape is the same generic 503 used for inactive guards
|
|
// (the distinguishing operator-facing error_id lives in the server log via
|
|
// logAndErrPluginsDisabled), so this test verifies fail-closed enforcement, not log content.
|
|
func TestChannelGuardFailsClosedWhenPluginsDisabled(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/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()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID))
|
|
|
|
// Disable the plugin system globally. resolveGuards now sees env == nil while
|
|
// guards remain in the cache, taking the logAndErrPluginsDisabled branch.
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = false
|
|
})
|
|
|
|
_, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "plugins disabled",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.NotNil(t, appErr, "guarded channel must fail-closed when plugin system is disabled")
|
|
assert.Equal(t, "app.plugin.inactive_guard.app_error", appErr.Id)
|
|
assert.Equal(t, 503, appErr.StatusCode)
|
|
}
|
|
|
|
// TestChannelGuardAllowByDefaultForUnimplementedHook covers the contract documented in
|
|
// guarded_hooks.go: a plugin may register a channel guard without implementing every
|
|
// guarded hook. When Phase B reaches such a claimant, the *WithRPCErr companion's
|
|
// g.implemented[<HookID>] gate skips the RPC entirely and returns zero values with a nil
|
|
// error — which the helper treats as "no opinion" rather than rejection. The op succeeds.
|
|
func TestChannelGuardAllowByDefaultForUnimplementedHook(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
// partialPlugin implements ChannelMemberWillBeAdded only; all other guarded-hook
|
|
// companions return "not implemented" (zero values, nil error), which the helpers
|
|
// treat as allow-by-default.
|
|
const partialPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type PartialPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *PartialPlugin) ChannelMemberWillBeAdded(c *plugin.Context, member *model.ChannelMember) (*model.ChannelMember, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&PartialPlugin{})
|
|
}
|
|
`
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{partialPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 1)
|
|
require.NoError(t, errs[0])
|
|
pluginID := pluginIDs[0]
|
|
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, pluginID))
|
|
|
|
// CreatePost: plugin does not implement MessageWillBePosted → allow-by-default.
|
|
created, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "allow by default",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, appErr, "CreatePost must succeed when guard plugin doesn't implement MessageWillBePosted")
|
|
require.NotNil(t, created)
|
|
|
|
// UpdatePost: plugin does not implement MessageWillBeUpdated → allow-by-default.
|
|
updated := created.Clone()
|
|
updated.Message = "allow by default updated"
|
|
result, _, appErr2 := th.App.UpdatePost(th.Context, updated, nil)
|
|
require.Nil(t, appErr2, "UpdatePost must succeed when guard plugin doesn't implement MessageWillBeUpdated")
|
|
require.NotNil(t, result)
|
|
|
|
// UpdateChannel: plugin does not implement ChannelWillBeUpdated → allow-by-default.
|
|
chCopy := th.BasicChannel.DeepCopy()
|
|
chCopy.DisplayName = "Allow by Default Update"
|
|
updatedCh, appErr3 := th.App.UpdateChannel(th.Context, chCopy)
|
|
require.Nil(t, appErr3, "UpdateChannel must succeed when guard plugin doesn't implement ChannelWillBeUpdated")
|
|
require.NotNil(t, updatedCh)
|
|
|
|
// RestoreChannel: plugin does not implement ChannelWillBeRestored → allow-by-default.
|
|
t.Run("RestoreChannel", func(t *testing.T) {
|
|
th2 := Setup(t).InitBasic(t)
|
|
tearDown2, pluginIDs2, errs2 := SetAppEnvironmentWithPlugins(t, []string{partialPlugin}, th2.App, th2.NewPluginAPI)
|
|
defer tearDown2()
|
|
require.Len(t, errs2, 1)
|
|
require.NoError(t, errs2[0])
|
|
pluginID2 := pluginIDs2[0]
|
|
|
|
restoreCh := th2.CreateChannel(t, th2.BasicTeam)
|
|
require.Nil(t, th2.App.RegisterChannelGuard(th2.Context, restoreCh.Id, pluginID2))
|
|
require.Nil(t, th2.App.DeleteChannel(th2.Context, restoreCh, th2.BasicUser.Id))
|
|
|
|
archived, err := th2.App.GetChannel(th2.Context, restoreCh.Id)
|
|
require.Nil(t, err)
|
|
_, appErr := th2.App.RestoreChannel(th2.Context, archived, th2.BasicUser.Id)
|
|
require.Nil(t, appErr, "RestoreChannel must succeed when guard plugin doesn't implement ChannelWillBeRestored")
|
|
})
|
|
}
|
|
|
|
// TestChannelGuardRejectsTypeMutationFromPhaseAPlugin covers the type-mutation guard at
|
|
// guarded_hooks.go line ~339: when one or more guards exist for a channel, a non-guard
|
|
// (Phase A) plugin that mutates Channel.Type must be rejected. This is the Phase A branch
|
|
// of the type-mutation check, distinct from the Phase B branch covered by
|
|
// TestChannelGuardRejectsTypeMutationFromPlugin.
|
|
func TestChannelGuardRejectsTypeMutationFromPhaseAPlugin(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Plugin G: passive guard (allows everything). Phase B has nothing to do.
|
|
const guardPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type GuardPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *GuardPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&GuardPlugin{})
|
|
}
|
|
`
|
|
|
|
// Plugin N: non-guard plugin that mutates Channel.Type in ChannelWillBeUpdated.
|
|
// On a guarded channel, this must be rejected with the type-mutation AppError.
|
|
const mutatorPlugin = `
|
|
package main
|
|
|
|
import (
|
|
"github.com/hanzoteam/server/server/public/plugin"
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
)
|
|
|
|
type MutatorPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MutatorPlugin) ChannelWillBeUpdated(c *plugin.Context, newChannel *model.Channel, oldChannel *model.Channel) (*model.Channel, string) {
|
|
mutated := newChannel
|
|
mutated.Type = model.ChannelTypePrivate
|
|
return mutated, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MutatorPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, pluginIDs, errs := SetAppEnvironmentWithPlugins(t, []string{guardPlugin, mutatorPlugin}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
require.Len(t, errs, 2)
|
|
require.NoError(t, errs[0])
|
|
require.NoError(t, errs[1])
|
|
guardID := pluginIDs[0]
|
|
// Mutator plugin (pluginIDs[1]) is intentionally NOT registered as a guard.
|
|
|
|
// Use a public channel so type mutation Public → Private is observable.
|
|
require.Equal(t, model.ChannelTypeOpen, th.BasicChannel.Type)
|
|
require.Nil(t, th.App.RegisterChannelGuard(th.Context, th.BasicChannel.Id, guardID))
|
|
|
|
chCopy := th.BasicChannel.DeepCopy()
|
|
chCopy.DisplayName = "Phase A type mutation"
|
|
_, appErr := th.App.UpdateChannel(th.Context, chCopy)
|
|
require.NotNil(t, appErr, "Phase A plugin mutating Channel.Type on a guarded channel must be rejected")
|
|
assert.Equal(t, "app.channel.update_channel.plugin_type_mutation.app_error", appErr.Id)
|
|
assert.Equal(t, 400, appErr.StatusCode)
|
|
}
|