Files
team/server/channels/app/plugin_hooks_test.go
1bdd985b41 Generate instead of hard-coding test passwords, enforce new minimum for FIPS, shard CI, fix FIPS builds (#35905)
* Replace hardcoded test passwords with model.NewTestPassword()

Add model.NewTestPassword() utility that generates 14+ character
passwords meeting complexity requirements for FIPS compliance. Replace
all short hardcoded test passwords across the test suite with calls to
this function.

* Enforce FIPS compliance for passwords and HMAC keys

FIPS OpenSSL requires HMAC keys to be at least 14 bytes. PBKDF2 uses
the password as the HMAC key internally, so short passwords cause
PKCS5_PBKDF2_HMAC to fail.

- Add FIPSEnabled and PasswordFIPSMinimumLength build-tag constants
- Raise the password minimum length floor to 14 when compiled with
  requirefips, applied in SetDefaults only when unset and validated
  independently in IsValid
- Return ErrMismatchedHashAndPassword for too-short passwords in
  PBKDF2 CompareHashAndPassword rather than a cryptic OpenSSL error
- Validate atmos/camo HMAC key length under FIPS and lengthen test
  keys accordingly
- Adjust password validation tests to use PasswordFIPSMinimumLength
  so they work under both FIPS and non-FIPS builds

* CI: shard FIPS test suite and extract merge template

Run FIPS tests on PRs that touch go.mod or have 'fips' in the branch
name. Shard FIPS tests across 4 runners matching the normal Postgres
suite. Extract the test result merge logic into a reusable workflow
template to deduplicate the normal and FIPS merge jobs.

* more

* Fix email test helper to respect FIPS minimum password length

* Fix test helpers to respect FIPS minimum password length

* Remove unnecessary "disable strict password requirements" blocks from test helpers

* Fix CodeRabbit review comments on PR #35905

- Add server-test-merge-template.yml to server-ci.yml pull_request.paths
  so changes to the reusable merge workflow trigger Server CI validation
- Skip merge-postgres-fips-test-results job when test-postgres-normal-fips
  was skipped, preventing failures due to missing artifacts
- Set guest.Password on returned guest in CreateGuestAndClient helper
  to keep contract consistent with CreateUserWithClient
- Use shared LowercaseLetters/UppercaseLetters/NUMBERS/PasswordFIPSMinimumLength
  constants in NewTestPassword() to avoid drift if FIPS floor changes

* Rename FIPS test artifact to match server-ci-report pattern

The server-ci-report job searches for artifacts matching "*-test-logs",
so rename from postgres-server-test-logs-fips to
postgres-server-fips-test-logs to be included in the report.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-04-08 16:49:43 -03:00

2735 lines
72 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
_ "embed"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
"github.com/mattermost/mattermost/server/public/plugin/utils"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
)
func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API) (func(), []string, []error) {
return setAppEnvironmentWithPlugins(t, pluginCode, app, apiFunc, "")
}
func SetAppEnvironmentWithPluginsGoVersion(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API, goVersion string) (func(), []string, []error) {
return setAppEnvironmentWithPlugins(t, pluginCode, app, apiFunc, goVersion)
}
func setAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, apiFunc func(*model.Manifest) plugin.API, goVersion string) (func(), []string, []error) {
pluginDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
webappPluginDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil)
require.NoError(t, err)
app.ch.SetPluginsEnvironment(env)
pluginIDs := []string{}
activationErrors := []error{}
for _, code := range pluginCode {
pluginID := model.NewId()
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGoVersion(t, goVersion, code, backend)
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
require.NoError(t, err)
_, _, activationErr := env.Activate(pluginID)
pluginIDs = append(pluginIDs, pluginID)
activationErrors = append(activationErrors, activationErr)
app.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{
Enable: true,
}
})
}
return func() {
os.RemoveAll(pluginDir)
os.RemoveAll(webappPluginDir)
}, pluginIDs, activationErrors
}
func TestHookMessageWillBePosted(t *testing.T) {
mainHelper.Parallel(t)
t.Run("rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
return nil, "rejected"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message_",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
if assert.NotNil(t, err) {
assert.Equal(t, "Post rejected by plugin. rejected", err.Message)
}
})
t.Run("rejected, returned post ignored", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
post.Message = "ignored"
return post, "rejected"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message_",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
if assert.NotNil(t, err) {
assert.Equal(t, "Post rejected by plugin. rejected", err.Message)
}
})
t.Run("allowed", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
return nil, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
post, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
assert.Equal(t, "message", post.Message)
retrievedPost, errSingle := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false)
require.NoError(t, errSingle)
assert.Equal(t, "message", retrievedPost.Message)
})
t.Run("updated", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
post.Message = post.Message + "_fromplugin"
return post, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
post, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
assert.Equal(t, "message_fromplugin", post.Message)
retrievedPost, errSingle := th.App.Srv().Store().Post().GetSingle(th.Context, post.Id, false)
require.NoError(t, errSingle)
assert.Equal(t, "message_fromplugin", retrievedPost.Message)
})
t.Run("multiple updated", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
post.Message = "prefix_" + post.Message
return post, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
post.Message = post.Message + "_suffix"
return post, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
post, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
assert.Equal(t, "prefix_message_suffix", post.Message)
})
}
func TestHookMessageHasBeenPosted(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
p.API.LogDebug(post.Message)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
}
func TestHookMessageWillBeUpdated(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) {
newPost.Message = newPost.Message + "fromplugin"
return newPost, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message_",
CreateAt: model.GetMillis() - 10000,
}
post, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
assert.Equal(t, "message_", post.Message)
post.Message = post.Message + "edited_"
post, _, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
require.Nil(t, err)
assert.Equal(t, "message_edited_fromplugin", post.Message)
}
func TestHookMessageHasBeenUpdated(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageHasBeenUpdated(c *plugin.Context, newPost, oldPost *model.Post) {
p.API.LogDebug(newPost.Message)
p.API.LogDebug(oldPost.Message)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message_",
CreateAt: model.GetMillis() - 10000,
}
post, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
assert.Equal(t, "message_", post.Message)
post.Message = post.Message + "edited"
_, _, err = th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{SafeUpdate: true})
require.Nil(t, err)
}
func TestHookMessageHasBeenDeleted(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageHasBeenDeleted(c *plugin.Context, post *model.Post) {
p.API.LogDebug(post.Message)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
_, err = th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id)
require.Nil(t, err)
}
func TestHookFileWillBeUploaded(t *testing.T) {
mainHelper.Parallel(t)
t.Run("rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
return nil, "rejected"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
_, appErr := th.App.UploadFile(th.Context,
[]byte("inputfile"),
th.BasicChannel.Id,
"testhook.txt",
)
if assert.NotNil(t, appErr) {
assert.Equal(t, "File rejected by plugin. rejected", appErr.Message)
}
})
t.Run("rejected, returned file ignored", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
n, err := output.Write([]byte("ignored"))
if err != nil {
return info, fmt.Sprintf("FAILED to write output file n: %v, err: %v", n, err)
}
info.Name = "ignored"
return info, "rejected"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
_, appErr := th.App.UploadFile(th.Context,
[]byte("inputfile"),
th.BasicChannel.Id,
"testhook.txt",
)
if assert.NotNil(t, appErr) {
assert.Equal(t, "File rejected by plugin. rejected", appErr.Message)
}
})
t.Run("allowed", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
return nil, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
response, appErr := th.App.UploadFile(th.Context,
[]byte("inputfile"),
th.BasicChannel.Id,
"testhook.txt",
)
assert.Nil(t, appErr)
assert.NotNil(t, response)
fileID := response.Id
fileInfo, appErr := th.App.GetFileInfo(th.Context, fileID)
assert.Nil(t, appErr)
assert.NotNil(t, fileInfo)
assert.Equal(t, "testhook.txt", fileInfo.Name)
fileReader, appErr := th.App.FileReader(fileInfo.Path)
assert.Nil(t, appErr)
var resultBuf bytes.Buffer
_, err := io.Copy(&resultBuf, fileReader)
require.NoError(t, err)
assert.Equal(t, "inputfile", resultBuf.String())
})
t.Run("updated", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
var buf bytes.Buffer
n, err := buf.ReadFrom(file)
if err != nil {
panic(fmt.Sprintf("buf.ReadFrom failed, reading %d bytes: %s", err.Error()))
}
outbuf := bytes.NewBufferString("changedtext")
n, err = io.Copy(output, outbuf)
if err != nil {
panic(fmt.Sprintf("io.Copy failed after %d bytes: %s", n, err.Error()))
}
if n != 11 {
panic(fmt.Sprintf("io.Copy only copied %d bytes", n))
}
info.Name = "modifiedinfo"
return info, ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
response, appErr := th.App.UploadFile(th.Context,
[]byte("inputfile"),
th.BasicChannel.Id,
"testhook.txt",
)
assert.Nil(t, appErr)
assert.NotNil(t, response)
fileID := response.Id
fileInfo, appErr := th.App.GetFileInfo(th.Context, fileID)
assert.Nil(t, appErr)
assert.NotNil(t, fileInfo)
assert.Equal(t, "modifiedinfo", fileInfo.Name)
fileReader, appErr := th.App.FileReader(fileInfo.Path)
assert.Nil(t, appErr)
var resultBuf bytes.Buffer
_, err := io.Copy(&resultBuf, fileReader)
require.NoError(t, err)
assert.Equal(t, "changedtext", resultBuf.String())
})
}
func TestUserWillLogIn_Blocked(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserWillLogIn(c *plugin.Context, user *model.User) string {
return "Blocked By Plugin"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
r := &http.Request{}
w := httptest.NewRecorder()
session, err := th.App.DoLogin(th.Context, w, r, th.BasicUser, "", false, false, false)
assert.Contains(t, err.Id, "Login rejected by plugin", "Expected Login rejected by plugin, got %s", err.Id)
assert.Nil(t, session)
}
func TestUserWillLogInIn_Passed(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserWillLogIn(c *plugin.Context, user *model.User) string {
return ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
r := &http.Request{}
w := httptest.NewRecorder()
session, err := th.App.DoLogin(th.Context, w, r, th.BasicUser, "", false, false, false)
assert.Nil(t, err, "Expected nil, got %s", err)
require.NotNil(t, session)
assert.Equal(t, session.UserId, th.BasicUser.Id)
}
func TestUserHasLoggedIn(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserHasLoggedIn(c *plugin.Context, user *model.User) {
user.FirstName = "plugin-callback-success"
p.API.UpdateUser(user)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
r := &http.Request{}
w := httptest.NewRecorder()
session, err := th.App.DoLogin(th.Context, w, r, th.BasicUser, "", false, false, false)
assert.Nil(t, err, "Expected nil, got %s", err)
assert.NotNil(t, session)
require.EventuallyWithT(t, func(c *assert.CollectT) {
user, _ := th.App.GetUser(th.BasicUser.Id)
assert.Equal(c, user.FirstName, "plugin-callback-success", "Expected firstname overwrite, got default")
}, 2*time.Second, 100*time.Millisecond)
}
func TestUserHasBeenDeactivated(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserHasBeenDeactivated(c *plugin.Context, user *model.User) {
user.Nickname = "plugin-callback-success"
p.API.UpdateUser(user)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
user := &model.User{
Email: "[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)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserHasBeenCreated(c *plugin.Context, user *model.User) {
user.Nickname = "plugin-callback-success"
p.API.UpdateUser(user)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
user := &model.User{
Email: "[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)
t.Run("errors.New", func(t *testing.T) {
tearDown, _, activationErrors := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"errors"
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnActivate() error {
return errors.New("simulate failure")
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, activationErrors, 1)
require.Error(t, activationErrors[0])
require.Contains(t, activationErrors[0].Error(), "simulate failure")
})
t.Run("AppError", func(t *testing.T) {
tearDown, _, activationErrors := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnActivate() error {
return model.NewAppError("where", "id", map[string]any{"param": 1}, "details", 42)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, activationErrors, 1)
require.Error(t, activationErrors[0])
cause := errors.Cause(activationErrors[0])
require.IsType(t, &model.AppError{}, cause)
// params not expected, since not exported
expectedErr := model.NewAppError("where", "id", nil, "details", 42)
require.Equal(t, expectedErr, cause)
})
}
func TestHookContext(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
p.API.LogDebug(c.SessionId)
p.API.LogInfo(c.RequestId)
p.API.LogError(c.IPAddress)
p.API.LogWarn(c.AcceptLanguage)
p.API.DeleteTeam(c.UserAgent)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "not this",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(ctx, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
}
func TestActiveHooks(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("", func(t *testing.T) {
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnActivate() error {
return nil
}
func (p *MyPlugin) OnConfigurationChange() error {
return nil
}
func (p *MyPlugin) UserHasBeenCreated(c *plugin.Context, user *model.User) {
user.Nickname = "plugin-callback-success"
p.API.UpdateUser(user)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, pluginIDs, 1)
pluginID := pluginIDs[0]
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
user1 := &model.User{
Email: "[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)
t.Run("", func(t *testing.T) {
metricsMock := &mocks.MetricsInterface{}
pluginDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
webappPluginDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
defer os.RemoveAll(pluginDir)
defer os.RemoveAll(webappPluginDir)
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), metricsMock)
require.NoError(t, err)
th.App.ch.SetPluginsEnvironment(env)
pluginID := model.NewId()
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
code := `
package main
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnActivate() error {
return nil
}
func (p *MyPlugin) OnConfigurationChange() error {
return nil
}
func (p *MyPlugin) UserHasBeenCreated(c *plugin.Context, user *model.User) {
user.Nickname = "plugin-callback-success"
p.API.UpdateUser(user)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`
utils.CompileGo(t, code, backend)
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
require.NoError(t, err)
// Setup mocks before activating
metricsMock.On("ObservePluginHookDuration", pluginID, "Implemented", true, mock.Anything).Return()
metricsMock.On("ObservePluginHookDuration", pluginID, "OnActivate", true, mock.Anything).Return()
metricsMock.On("ObservePluginHookDuration", pluginID, "OnDeactivate", true, mock.Anything).Return()
metricsMock.On("ObservePluginHookDuration", pluginID, "OnConfigurationChange", true, mock.Anything).Return()
metricsMock.On("ObservePluginHookDuration", pluginID, "UserHasBeenCreated", true, mock.Anything).Return()
// Don't care about these calls.
metricsMock.On("ObservePluginAPIDuration", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
metricsMock.On("ObservePluginMultiHookIterationDuration", mock.Anything, mock.Anything, mock.Anything).Return()
metricsMock.On("ObservePluginMultiHookDuration", mock.Anything).Return()
_, _, activationErr := env.Activate(pluginID)
require.NoError(t, activationErr)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{
Enable: true,
}
})
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
user1 := &model.User{
Email: "[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).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LogDebug", "smile").Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) {
p.API.LogDebug(reaction.EmojiName)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
reaction := &model.Reaction{
UserId: th.BasicUser.Id,
PostId: th.BasicPost.Id,
EmojiName: "smile",
CreateAt: model.GetMillis() - 10000,
}
_, err := th.App.SaveReactionForPost(th.Context, reaction)
require.Nil(t, err)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
mockAPI.AssertExpectations(&testutils.CollectTWithLogf{CollectT: c})
}, 5*time.Second, 100*time.Millisecond)
}
func TestHookReactionHasBeenRemoved(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LogDebug", "star").Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) ReactionHasBeenRemoved(c *plugin.Context, reaction *model.Reaction) {
p.API.LogDebug(reaction.EmojiName)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
reaction := &model.Reaction{
UserId: th.BasicUser.Id,
PostId: th.BasicPost.Id,
EmojiName: "star",
CreateAt: model.GetMillis() - 10000,
}
err := th.App.DeleteReactionForPost(th.Context, reaction)
require.Nil(t, err)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
mockAPI.AssertExpectations(&testutils.CollectTWithLogf{CollectT: c})
}, 5*time.Second, 100*time.Millisecond)
}
func TestHookRunDataRetention(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) RunDataRetention(nowMillis, batchSize int64) (int64, error){
return 100, nil
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, pluginIDs, 1)
pluginID := pluginIDs[0]
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
hookCalled := false
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
n, _ := hooks.RunDataRetention(0, 0)
// Ensure return it correct
assert.Equal(t, int64(100), n)
hookCalled = true
return hookCalled
}, plugin.RunDataRetentionID)
require.True(t, hookCalled)
}
func TestHookOnSendDailyTelemetry(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnSendDailyTelemetry() {
return
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, pluginIDs, 1)
pluginID := pluginIDs[0]
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
hookCalled := false
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
hooks.OnSendDailyTelemetry()
hookCalled = true
return hookCalled
}, plugin.OnSendDailyTelemetryID)
require.True(t, hookCalled)
}
func TestHookOnCloudLimitsUpdated(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnCloudLimitsUpdated(_ *model.ProductLimits) {
return
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, pluginIDs, 1)
pluginID := pluginIDs[0]
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
hookCalled := false
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
hooks.OnCloudLimitsUpdated(nil)
hookCalled = true
return hookCalled
}, plugin.OnCloudLimitsUpdatedID)
require.True(t, hookCalled)
}
//go:embed test_templates/hook_notification_will_be_pushed.tmpl
var hookNotificationWillBePushedTmpl string
func TestHookNotificationWillBePushed(t *testing.T) {
mainHelper.Parallel(t)
if testing.Short() {
t.Skip("skipping TestHookNotificationWillBePushed test in short mode")
}
tests := []struct {
name string
testCode string
expectedNotifications int
expectedNotificationMessage string
}{
{
name: "successfully pushed",
testCode: `return nil, ""`,
expectedNotifications: 6,
},
{
name: "push notification rejected",
testCode: `return nil, "rejected"`,
expectedNotifications: 0,
},
{
name: "push notification modified",
testCode: `notification.Message = "brand new message"
return notification, ""`,
expectedNotifications: 6,
expectedNotificationMessage: "brand new message",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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)
})
}
}
//go:embed test_templates/hook_email_notification_will_be_sent.tmpl
var hookEmailNotificationWillBeSentTmpl string
func TestHookEmailNotificationWillBeSent(t *testing.T) {
mainHelper.Parallel(t)
tests := []struct {
name string
testCode string
expectedNotificationSubject string
expectedNotificationTitle string
expectedButtonText string
expectedFooterText string
}{
{
name: "successfully sent",
testCode: `return nil, ""`,
},
{
name: "email notification rejected",
testCode: `return nil, "rejected"`,
},
{
name: "email notification modified",
testCode: `content := &model.EmailNotificationContent{
Subject: "Modified Subject by Plugin",
Title: "Modified Title by Plugin",
ButtonText: "Modified Button by Plugin",
FooterText: "Modified Footer by Plugin",
}
return content, ""`,
expectedNotificationSubject: "Modified Subject by Plugin",
expectedNotificationTitle: "Modified Title by Plugin",
expectedButtonText: "Modified Button by Plugin",
expectedFooterText: "Modified Footer by Plugin",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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)
setupPlugin := func(t *testing.T, th *TestHelper) {
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil)
mockAPI.On("LogDebug", "message").Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
for _, post := range posts {
post.Message = "mwbc_plugin:" + post.Message
}
return posts
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
t.Cleanup(tearDown)
}
t.Run("feature flag disabled", func(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.ConsumePostHook = false
}).InitBasic(t)
setupPlugin(t, th)
newPost := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(th.Context, newPost, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
post, err := th.App.GetSinglePost(th.Context, newPost.Id, true)
require.Nil(t, err)
assert.Equal(t, "message", post.Message)
})
t.Run("feature flag enabled", func(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.ConsumePostHook = true
}).InitBasic(t)
setupPlugin(t, th)
newPost := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
CreateAt: model.GetMillis() - 10000,
}
_, _, err := th.App.CreatePost(th.Context, newPost, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
post, err := th.App.GetSinglePost(th.Context, newPost.Id, true)
require.Nil(t, err)
assert.Equal(t, "mwbc_plugin:message", post.Message)
})
}
func TestHookPreferencesHaveChanged(t *testing.T) {
mainHelper.Parallel(t)
t.Run("should be called when preferences are changed by non-plugin code", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Setup plugin
var mockAPI plugintest.API
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{`
package main
import (
"fmt"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) {
for _, preference := range preferences {
p.API.LogDebug(fmt.Sprintf("category=%s name=%s value=%s", preference.Category, preference.Name, preference.Value))
}
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Confirm plugin is actually running
require.Len(t, pluginIDs, 1)
pluginID := pluginIDs[0]
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
// Setup test
preferences := model.Preferences{
{
UserId: th.BasicUser.Id,
Category: "test_category",
Name: "test_name_1",
Value: "test_value_1",
},
{
UserId: th.BasicUser.Id,
Category: "test_category",
Name: "test_name_2",
Value: "test_value_2",
},
}
mockAPI.On("LogDebug", "category=test_category name=test_name_1 value=test_value_1")
mockAPI.On("LogDebug", "category=test_category name=test_name_2 value=test_value_2")
// Run test
err := th.App.UpdatePreferences(th.Context, th.BasicUser.Id, preferences)
require.Nil(t, err)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
mockAPI.AssertExpectations(&testutils.CollectTWithLogf{CollectT: c})
}, 5*time.Second, 100*time.Millisecond)
})
t.Run("should be called when preferences are changed by plugin code", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Setup plugin
pluginCode := `
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
const (
userID = "` + th.BasicUser.Id + `"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) {
// Note that plugin hooks can trigger themselves, and this test sets a preference to trigger that
// it has run, so be careful not to introduce an infinite loop here
if len(preferences) == 1 && preferences[0].Category == "test_category" && preferences[0].Name == "test_name" {
if preferences[0].Value == "test_value_first" {
appErr := p.API.UpdatePreferencesForUser(userID, []model.Preference{
{
UserId: userID,
Category: "test_category",
Name: "test_name",
Value: "test_value_second",
},
})
if appErr != nil {
panic("error setting preference to second value")
}
} else if preferences[0].Value == "test_value_second" {
appErr := p.API.UpdatePreferencesForUser(userID, []model.Preference{
{
UserId: userID,
Category: "test_category",
Name: "test_name",
Value: "test_value_third",
},
})
if appErr != nil {
panic("error setting preference to third value")
}
}
}
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`
pluginID := "testplugin"
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
// Confirm plugin is actually running
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
appErr := th.App.UpdatePreferences(th.Context, th.BasicUser.Id, model.Preferences{
{
UserId: th.BasicUser.Id,
Category: "test_category",
Name: "test_name",
Value: "test_value_first",
},
})
require.Nil(t, appErr)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
preference, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, th.BasicUser.Id, "test_category", "test_name")
require.Nil(t, appErr)
assert.Equal(t, "test_value_third", preference.Value)
}, 5*time.Second, 100*time.Millisecond)
})
}
func TestChannelHasBeenCreated(t *testing.T) {
mainHelper.Parallel(t)
getPluginCode := func(th *TestHelper) string {
return `
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
const (
adminUserID = "` + th.SystemAdminUser.Id + `"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) ChannelHasBeenCreated(c *plugin.Context, channel *model.Channel) {
_, appErr := p.API.CreatePost(&model.Post{
UserId: adminUserID,
ChannelId: channel.Id,
Message: "ChannelHasBeenCreated has been called for " + channel.Id,
})
if appErr != nil {
panic(appErr)
}
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`
}
pluginID := "testplugin"
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
t.Run("should call hook when a regular channel is created", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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).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).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 TestUserHasJoinedChannel(t *testing.T) {
mainHelper.Parallel(t)
getPluginCode := func(th *TestHelper) string {
return `
package main
import (
"fmt"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
const (
adminUserID = "` + th.SystemAdminUser.Id + `"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
message := fmt.Sprintf("Test: User %s joined %s", channelMember.UserId, channelMember.ChannelId)
if actor != nil && actor.Id != channelMember.UserId {
message = fmt.Sprintf("Test: User %s added to %s by %s", channelMember.UserId, channelMember.ChannelId, actor.Id)
}
_, appErr := p.API.CreatePost(&model.Post{
UserId: adminUserID,
ChannelId: channelMember.ChannelId,
Message: message,
})
if appErr != nil {
panic(appErr)
}
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`
}
pluginID := "testplugin"
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
t.Run("should call hook when a user joins an existing channel", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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(th), pluginManifest, pluginID, th.App, th.Context)
_, appErr = th.App.AddChannelMember(th.Context, user2.Id, channel, ChannelMemberOpts{
UserRequestorID: user2.Id,
})
require.Nil(t, appErr)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 30)
require.Nil(t, appErr)
assert.True(t, len(posts.Order) > 0)
found := false
for _, post := range posts.Posts {
if post.Message == fmt.Sprintf("Test: User %s joined %s", user2.Id, channel.Id) {
found = true
}
}
if !found {
assert.Fail(t, "Couldn't find user joined channel hook message post")
}
}, 5*time.Second, 100*time.Millisecond)
})
t.Run("should call hook when a user is added to an existing channel", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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(th), pluginManifest, pluginID, th.App, th.Context)
_, appErr = th.App.AddChannelMember(th.Context, user2.Id, channel, ChannelMemberOpts{
UserRequestorID: user1.Id,
})
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("Test: User %s added to %s by %s", user2.Id, channel.Id, user1.Id)
assert.Eventually(t, func() bool {
// Typically, the post we're looking for will be the latest, but there's a race between the plugin and
// "User has joined the channel" post which means the plugin post may not the the latest one
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 10)
require.Nil(t, appErr)
for _, postId := range posts.Order {
post := posts.Posts[postId]
if post.Message == expectedMessage {
return true
}
}
return false
}, 5*time.Second, 100*time.Millisecond)
})
t.Run("should not call hook when a regular channel is created", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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)
var posts *model.PostList
require.EventuallyWithT(t, func(c *assert.CollectT) {
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 10)
assert.Nil(t, appErr)
}, 2*time.Second, 100*time.Millisecond)
for _, postID := range posts.Order {
post := posts.Posts[postID]
if strings.HasPrefix(post.Message, "Test: ") {
t.Log("Plugin message found:", post.Message)
t.FailNow()
}
}
})
t.Run("should not call hook when a DM is created", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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)
var posts *model.PostList
require.EventuallyWithT(t, func(c *assert.CollectT) {
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 10)
assert.Nil(t, appErr)
}, 2*time.Second, 100*time.Millisecond)
for _, postID := range posts.Order {
post := posts.Posts[postID]
if strings.HasPrefix(post.Message, "Test: ") {
t.Log("Plugin message found:", post.Message)
t.FailNow()
}
}
})
t.Run("should not call hook when a GM is created", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(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)
var posts *model.PostList
require.EventuallyWithT(t, func(c *assert.CollectT) {
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 10)
assert.Nil(t, appErr)
}, 2*time.Second, 100*time.Millisecond)
for _, postID := range posts.Order {
post := posts.Posts[postID]
if strings.HasPrefix(post.Message, "Test: ") {
t.Log("Plugin message found:", post.Message)
t.FailNow()
}
}
})
}
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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/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)
})
}