mattermost/app/plugin_api_test.go
Alejandro García Montoro aa47b4633f
MM-23508: Fix plugin API's GetPostsForChannel (#14125)
Add a test checking the basic behaviour of the function
2020-03-25 10:24:42 +01:00

1454 lines
40 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"image"
"image/color"
"image/png"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/mattermost/mattermost-server/v5/einterfaces/mocks"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/mattermost/mattermost-server/v5/utils"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func getDefaultPluginSettingsSchema() string {
ret, _ := json.Marshal(model.PluginSettingsSchema{
Settings: []*model.PluginSetting{
{Key: "BasicChannelName", Type: "text"},
{Key: "BasicChannelId", Type: "text"},
{Key: "BasicTeamDisplayName", Type: "text"},
{Key: "BasicTeamName", Type: "text"},
{Key: "BasicTeamId", Type: "text"},
{Key: "BasicUserEmail", Type: "text"},
{Key: "BasicUserId", Type: "text"},
{Key: "BasicUser2Email", Type: "text"},
{Key: "BasicUser2Id", Type: "text"},
{Key: "BasicPostMessage", Type: "text"},
},
})
return string(ret)
}
func setDefaultPluginConfig(th *TestHelper, pluginId string) {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins[pluginId] = map[string]interface{}{
"BasicChannelName": th.BasicChannel.Name,
"BasicChannelId": th.BasicChannel.Id,
"BasicTeamName": th.BasicTeam.Name,
"BasicTeamId": th.BasicTeam.Id,
"BasicTeamDisplayName": th.BasicTeam.DisplayName,
"BasicUserEmail": th.BasicUser.Email,
"BasicUserId": th.BasicUser.Id,
"BasicUser2Email": th.BasicUser2.Email,
"BasicUser2Id": th.BasicUser2.Id,
"BasicPostMessage": th.BasicPost.Message,
}
})
}
func setupMultiPluginApiTest(t *testing.T, pluginCodes []string, pluginManifests []string, pluginIds []string, app *App) string {
pluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
webappPluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(pluginDir)
defer os.RemoveAll(webappPluginDir)
env, err := plugin.NewEnvironment(app.NewPluginAPI, pluginDir, webappPluginDir, app.Log(), nil)
require.NoError(t, err)
require.Equal(t, len(pluginCodes), len(pluginIds))
require.Equal(t, len(pluginManifests), len(pluginIds))
for i, pluginId := range pluginIds {
backend := filepath.Join(pluginDir, pluginId, "backend.exe")
utils.CompileGo(t, pluginCodes[i], backend)
ioutil.WriteFile(filepath.Join(pluginDir, pluginId, "plugin.json"), []byte(pluginManifests[i]), 0600)
manifest, activated, reterr := env.Activate(pluginId)
require.Nil(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
}
app.SetPluginsEnvironment(env)
return pluginDir
}
func setupPluginApiTest(t *testing.T, pluginCode string, pluginManifest string, pluginId string, app *App) string {
return setupMultiPluginApiTest(t, []string{pluginCode}, []string{pluginManifest}, []string{pluginId}, app)
}
func TestPublicFilesPathConfiguration(t *testing.T) {
th := Setup(t)
defer th.TearDown()
pluginID := "com.mattermost.sample"
pluginDir := setupPluginApiTest(t,
`
package main
import (
"github.com/mattermost/mattermost-server/v5/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
`{"id": "com.mattermost.sample", "server": {"executable": "backend.exe"}, "settings_schema": {"settings": []}}`, pluginID, th.App)
publicFilesFolderInTest := filepath.Join(pluginDir, pluginID, "public")
publicFilesPath, err := th.App.GetPluginsEnvironment().PublicFilesPath(pluginID)
assert.NoError(t, err)
assert.Equal(t, publicFilesPath, publicFilesFolderInTest)
}
func TestPluginAPIGetUsers(t *testing.T) {
th := Setup(t)
defer th.TearDown()
api := th.SetupPluginAPI()
user1, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user1" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user1)
user2, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user2" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user2)
user3, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user3" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user3)
user4, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user4" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user4)
testCases := []struct {
Description string
Page int
PerPage int
ExpectedUsers []*model.User
}{
{
"page 0, perPage 0",
0,
0,
[]*model.User{},
},
{
"page 0, perPage 10",
0,
10,
[]*model.User{user1, user2, user3, user4},
},
{
"page 0, perPage 2",
0,
2,
[]*model.User{user1, user2},
},
{
"page 1, perPage 2",
1,
2,
[]*model.User{user3, user4},
},
{
"page 10, perPage 10",
10,
10,
[]*model.User{},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := api.GetUsers(&model.UserGetOptions{
Page: testCase.Page,
PerPage: testCase.PerPage,
})
assert.Nil(t, err)
assert.Equal(t, testCase.ExpectedUsers, users)
})
}
}
func TestPluginAPIGetUsersInTeam(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
team1 := th.CreateTeam()
team2 := th.CreateTeam()
user1, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user1" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user1)
user2, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user2" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user2)
user3, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user3" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user3)
user4, err := th.App.CreateUser(&model.User{
Email: strings.ToLower(model.NewId()) + "success+test@example.com",
Password: "password",
Username: "user4" + model.NewId(),
})
require.Nil(t, err)
defer th.App.PermanentDeleteUser(user4)
// Add all users to team 1
_, _, err = th.App.joinUserToTeam(team1, user1)
require.Nil(t, err)
_, _, err = th.App.joinUserToTeam(team1, user2)
require.Nil(t, err)
_, _, err = th.App.joinUserToTeam(team1, user3)
require.Nil(t, err)
_, _, err = th.App.joinUserToTeam(team1, user4)
require.Nil(t, err)
// Add only user3 and user4 to team 2
_, _, err = th.App.joinUserToTeam(team2, user3)
require.Nil(t, err)
_, _, err = th.App.joinUserToTeam(team2, user4)
require.Nil(t, err)
testCases := []struct {
Description string
TeamId string
Page int
PerPage int
ExpectedUsers []*model.User
}{
{
"unknown team",
model.NewId(),
0,
0,
[]*model.User{},
},
{
"team 1, page 0, perPage 10",
team1.Id,
0,
10,
[]*model.User{user1, user2, user3, user4},
},
{
"team 1, page 0, perPage 2",
team1.Id,
0,
2,
[]*model.User{user1, user2},
},
{
"team 1, page 1, perPage 2",
team1.Id,
1,
2,
[]*model.User{user3, user4},
},
{
"team 2, page 0, perPage 10",
team2.Id,
0,
10,
[]*model.User{user3, user4},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := api.GetUsersInTeam(testCase.TeamId, testCase.Page, testCase.PerPage)
assert.Nil(t, err)
assert.Equal(t, testCase.ExpectedUsers, users)
})
}
}
func TestPluginAPIGetFile(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
// check a valid file first
uploadTime := time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local)
filename := "testGetFile"
fileData := []byte("Hello World")
info, err := th.App.DoUploadFile(uploadTime, th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, filename, fileData)
require.Nil(t, err)
defer func() {
th.App.Srv().Store.FileInfo().PermanentDelete(info.Id)
th.App.RemoveFile(info.Path)
}()
data, err1 := api.GetFile(info.Id)
require.Nil(t, err1)
assert.Equal(t, data, fileData)
// then checking invalid file
data, err = api.GetFile("../fake/testingApi")
require.NotNil(t, err)
require.Nil(t, data)
}
func TestPluginAPIGetFileInfos(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
fileInfo1, err := th.App.DoUploadFile(
time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC),
th.BasicTeam.Id,
th.BasicChannel.Id,
th.BasicUser.Id,
"testFile1",
[]byte("testfile1 Content"),
)
require.Nil(t, err)
defer func() {
th.App.Srv().Store.FileInfo().PermanentDelete(fileInfo1.Id)
th.App.RemoveFile(fileInfo1.Path)
}()
fileInfo2, err := th.App.DoUploadFile(
time.Date(2020, 1, 2, 1, 1, 1, 1, time.UTC),
th.BasicTeam.Id,
th.BasicChannel.Id,
th.BasicUser2.Id,
"testFile2",
[]byte("testfile2 Content"),
)
require.Nil(t, err)
defer func() {
th.App.Srv().Store.FileInfo().PermanentDelete(fileInfo2.Id)
th.App.RemoveFile(fileInfo2.Path)
}()
fileInfo3, err := th.App.DoUploadFile(
time.Date(2020, 1, 3, 1, 1, 1, 1, time.UTC),
th.BasicTeam.Id,
th.BasicChannel.Id,
th.BasicUser.Id,
"testFile3",
[]byte("testfile3 Content"),
)
require.Nil(t, err)
defer func() {
th.App.Srv().Store.FileInfo().PermanentDelete(fileInfo3.Id)
th.App.RemoveFile(fileInfo3.Path)
}()
_, err = api.CreatePost(&model.Post{
Message: "testFile1",
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
FileIds: model.StringArray{fileInfo1.Id},
})
require.Nil(t, err)
_, err = api.CreatePost(&model.Post{
Message: "testFile2",
UserId: th.BasicUser2.Id,
ChannelId: th.BasicChannel.Id,
FileIds: model.StringArray{fileInfo2.Id},
})
require.Nil(t, err)
t.Run("get file infos with no options 2nd page of 1 per page", func(t *testing.T) {
fileInfos, err := api.GetFileInfos(1, 1, nil)
require.Nil(t, err)
require.Len(t, fileInfos, 1)
})
t.Run("get file infos filtered by user", func(t *testing.T) {
fileInfos, err := api.GetFileInfos(0, 5, &model.GetFileInfosOptions{
UserIds: []string{th.BasicUser.Id},
})
require.Nil(t, err)
require.Len(t, fileInfos, 2)
})
t.Run("get file infos filtered by channel ordered by created at descending", func(t *testing.T) {
fileInfos, err := api.GetFileInfos(0, 5, &model.GetFileInfosOptions{
ChannelIds: []string{th.BasicChannel.Id},
SortBy: model.FILEINFO_SORT_BY_CREATED,
SortDescending: true,
})
require.Nil(t, err)
require.Len(t, fileInfos, 2)
require.Equal(t, fileInfos[0].Id, fileInfo2.Id)
require.Equal(t, fileInfos[1].Id, fileInfo1.Id)
})
}
func TestPluginAPISavePluginConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
manifest := &model.Manifest{
Id: "pluginid",
SettingsSchema: &model.PluginSettingsSchema{
Settings: []*model.PluginSetting{
{Key: "MyStringSetting", Type: "text"},
{Key: "MyIntSetting", Type: "text"},
{Key: "MyBoolSetting", Type: "bool"},
},
},
}
api := NewPluginAPI(th.App, manifest)
pluginConfigJsonString := `{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`
var pluginConfig map[string]interface{}
err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig)
require.NoError(t, err)
appErr := api.SavePluginConfig(pluginConfig)
require.Nil(t, appErr)
type Configuration struct {
MyStringSetting string
MyIntSetting int
MyBoolSetting bool
}
savedConfiguration := new(Configuration)
err = api.LoadPluginConfiguration(savedConfiguration)
require.NoError(t, err)
expectedConfiguration := new(Configuration)
err = json.Unmarshal([]byte(pluginConfigJsonString), &expectedConfiguration)
require.NoError(t, err)
assert.Equal(t, expectedConfiguration, savedConfiguration)
}
func TestPluginAPIGetPluginConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
manifest := &model.Manifest{
Id: "pluginid",
SettingsSchema: &model.PluginSettingsSchema{
Settings: []*model.PluginSetting{
{Key: "MyStringSetting", Type: "text"},
{Key: "MyIntSetting", Type: "text"},
{Key: "MyBoolSetting", Type: "bool"},
},
},
}
api := NewPluginAPI(th.App, manifest)
pluginConfigJsonString := `{"mystringsetting": "str", "myintsetting": 32, "myboolsetting": true}`
var pluginConfig map[string]interface{}
err := json.Unmarshal([]byte(pluginConfigJsonString), &pluginConfig)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins["pluginid"] = pluginConfig
})
savedPluginConfig := api.GetPluginConfig()
assert.Equal(t, pluginConfig, savedPluginConfig)
}
func TestPluginAPILoadPluginConfiguration(t *testing.T) {
th := Setup(t)
defer th.TearDown()
var pluginJson map[string]interface{}
err := json.Unmarshal([]byte(`{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`), &pluginJson)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson
})
testFolder, found := fileutils.FindDir("mattermost-server/app/plugin_api_tests")
require.True(t, found, "Cannot find tests folder")
fullPath := path.Join(testFolder, "manual.test_load_configuration_plugin", "main.go")
err = pluginAPIHookTest(t, th, fullPath, "testloadpluginconfig", `{"id": "testloadpluginconfig", "backend": {"executable": "backend.exe"}, "settings_schema": {
"settings": [
{
"key": "MyStringSetting",
"type": "text"
},
{
"key": "MyIntSetting",
"type": "text"
},
{
"key": "MyBoolSetting",
"type": "bool"
}
]
}}`)
require.NoError(t, err)
}
func TestPluginAPILoadPluginConfigurationDefaults(t *testing.T) {
th := Setup(t)
defer th.TearDown()
var pluginJson map[string]interface{}
err := json.Unmarshal([]byte(`{"mystringsetting": "override"}`), &pluginJson)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson
})
testFolder, found := fileutils.FindDir("mattermost-server/app/plugin_api_tests")
require.True(t, found, "Cannot find tests folder")
fullPath := path.Join(testFolder, "manual.test_load_configuration_defaults_plugin", "main.go")
err = pluginAPIHookTest(t, th, fullPath, "testloadpluginconfig", `{
"settings": [
{
"key": "MyStringSetting",
"type": "text",
"default": "notthis"
},
{
"key": "MyIntSetting",
"type": "text",
"default": 35
},
{
"key": "MyBoolSetting",
"type": "bool",
"default": true
}
]
}`)
require.NoError(t, err)
}
func TestPluginAPIGetPlugins(t *testing.T) {
th := Setup(t)
defer th.TearDown()
api := th.SetupPluginAPI()
pluginCode := `
package main
import (
"github.com/mattermost/mattermost-server/v5/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`
pluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
webappPluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(pluginDir)
defer os.RemoveAll(webappPluginDir)
env, err := plugin.NewEnvironment(th.App.NewPluginAPI, pluginDir, webappPluginDir, th.App.Log(), nil)
require.NoError(t, err)
pluginIDs := []string{"pluginid1", "pluginid2", "pluginid3"}
var pluginManifests []*model.Manifest
for _, pluginID := range pluginIDs {
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGo(t, pluginCode, backend)
ioutil.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(fmt.Sprintf(`{"id": "%s", "server": {"executable": "backend.exe"}}`, pluginID)), 0600)
manifest, activated, reterr := env.Activate(pluginID)
require.Nil(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
pluginManifests = append(pluginManifests, manifest)
}
th.App.SetPluginsEnvironment(env)
// Deactivate the last one for testing
success := env.Deactivate(pluginIDs[len(pluginIDs)-1])
require.True(t, success)
// check existing user first
plugins, err := api.GetPlugins()
assert.Nil(t, err)
assert.NotEmpty(t, plugins)
assert.Equal(t, pluginManifests, plugins)
}
func TestPluginAPIInstallPlugin(t *testing.T) {
th := Setup(t)
defer th.TearDown()
api := th.SetupPluginAPI()
path, _ := fileutils.FindDir("tests")
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
_, err = api.InstallPlugin(bytes.NewReader(tarData), true)
assert.NotNil(t, err, "should not allow upload if upload disabled")
assert.Equal(t, err.Error(), "installPlugin: Plugins and/or plugin uploads have been disabled., ")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
})
manifest, err := api.InstallPlugin(bytes.NewReader(tarData), true)
defer os.RemoveAll("plugins/testplugin")
require.Nil(t, err)
assert.Equal(t, "testplugin", manifest.Id)
// Successfully installed
pluginsResp, err := api.GetPlugins()
require.Nil(t, err)
found := false
for _, m := range pluginsResp {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
}
func TestInstallPlugin(t *testing.T) {
// TODO(ilgooz): remove this setup func to use existent setupPluginApiTest().
// following setupTest() func is a modified version of setupPluginApiTest().
// we need a modified version of setupPluginApiTest() because it wasn't possible to use it directly here
// since it removes plugin dirs right after it returns, does not update App configs with the plugin
// dirs and this behavior tends to break this test as a result.
setupTest := func(t *testing.T, pluginCode string, pluginManifest string, pluginID string, app *App) (func(), string) {
pluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
webappPluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
app.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Directory = pluginDir
*cfg.PluginSettings.ClientDirectory = webappPluginDir
})
env, err := plugin.NewEnvironment(app.NewPluginAPI, pluginDir, webappPluginDir, app.Log(), nil)
require.NoError(t, err)
app.SetPluginsEnvironment(env)
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGo(t, pluginCode, backend)
ioutil.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
manifest, activated, reterr := env.Activate(pluginID)
require.Nil(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
return func() {
os.RemoveAll(pluginDir)
os.RemoveAll(webappPluginDir)
}, pluginDir
}
th := Setup(t).InitBasic()
defer th.TearDown()
// start an http server to serve plugin's tarball to the test.
path, _ := fileutils.FindDir("tests")
ts := httptest.NewServer(http.FileServer(http.Dir(path)))
defer ts.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
cfg.PluginSettings.Plugins["testinstallplugin"] = map[string]interface{}{
"DownloadURL": ts.URL + "/testplugin.tar.gz",
}
})
tearDown, _ := setupTest(t,
`
package main
import (
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v5/plugin"
)
type configuration struct {
DownloadURL string
}
type Plugin struct {
plugin.MattermostPlugin
configuration configuration
}
func (p *Plugin) OnConfigurationChange() error {
if err := p.API.LoadPluginConfiguration(&p.configuration); err != nil {
return err
}
return nil
}
func (p *Plugin) OnActivate() error {
resp, err := http.Get(p.configuration.DownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
_, aerr := p.API.InstallPlugin(resp.Body, true)
if aerr != nil {
return errors.Wrap(aerr, "cannot install plugin")
}
return nil
}
func main() {
plugin.ClientMain(&Plugin{})
}
`,
`{"id": "testinstallplugin", "backend": {"executable": "backend.exe"}, "settings_schema": {
"settings": [
{
"key": "DownloadURL",
"type": "text"
}
]
}}`, "testinstallplugin", th.App)
defer tearDown()
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testinstallplugin")
require.NoError(t, err)
err = hooks.OnActivate()
require.NoError(t, err)
plugins, aerr := th.App.GetPlugins()
require.Nil(t, aerr)
require.Len(t, plugins.Inactive, 1)
require.Equal(t, "testplugin", plugins.Inactive[0].Id)
}
func TestPluginAPIGetTeamIcon(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
// Create an 128 x 128 image
img := image.NewRGBA(image.Rect(0, 0, 128, 128))
// Draw a red dot at (2, 3)
img.Set(2, 3, color.RGBA{255, 0, 0, 255})
buf := new(bytes.Buffer)
err := png.Encode(buf, img)
require.Nil(t, err)
dataBytes := buf.Bytes()
fileReader := bytes.NewReader(dataBytes)
// Set the Team Icon
err = th.App.SetTeamIconFromFile(th.BasicTeam, fileReader)
require.Nil(t, err)
// Get the team icon to check
teamIcon, err := api.GetTeamIcon(th.BasicTeam.Id)
require.Nil(t, err)
require.NotEmpty(t, teamIcon)
colorful := color.NRGBA{255, 0, 0, 255}
byteReader := bytes.NewReader(teamIcon)
img2, _, err2 := image.Decode(byteReader)
require.Nil(t, err2)
require.Equal(t, img2.At(2, 3), colorful)
}
func TestPluginAPISetTeamIcon(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
// Create an 128 x 128 image
img := image.NewRGBA(image.Rect(0, 0, 128, 128))
// Draw a red dot at (2, 3)
img.Set(2, 3, color.RGBA{255, 0, 0, 255})
buf := new(bytes.Buffer)
err := png.Encode(buf, img)
require.Nil(t, err)
dataBytes := buf.Bytes()
// Set the user profile image
err = api.SetTeamIcon(th.BasicTeam.Id, dataBytes)
require.Nil(t, err)
// Get the user profile image to check
teamIcon, err := api.GetTeamIcon(th.BasicTeam.Id)
require.Nil(t, err)
require.NotEmpty(t, teamIcon)
colorful := color.NRGBA{255, 0, 0, 255}
byteReader := bytes.NewReader(teamIcon)
img2, _, err2 := image.Decode(byteReader)
require.Nil(t, err2)
require.Equal(t, img2.At(2, 3), colorful)
}
func TestPluginAPIRemoveTeamIcon(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
// Create an 128 x 128 image
img := image.NewRGBA(image.Rect(0, 0, 128, 128))
// Draw a red dot at (2, 3)
img.Set(2, 3, color.RGBA{255, 0, 0, 255})
buf := new(bytes.Buffer)
err1 := png.Encode(buf, img)
require.Nil(t, err1)
dataBytes := buf.Bytes()
fileReader := bytes.NewReader(dataBytes)
// Set the Team Icon
err := th.App.SetTeamIconFromFile(th.BasicTeam, fileReader)
require.Nil(t, err)
err = api.RemoveTeamIcon(th.BasicTeam.Id)
require.Nil(t, err)
}
func pluginAPIHookTest(t *testing.T, th *TestHelper, fileName string, id string, settingsSchema string) error {
data, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
code := string(data)
schema := `{"settings": [ ] }`
if settingsSchema != "" {
schema = settingsSchema
}
setupPluginApiTest(t, code,
fmt.Sprintf(`{"id": "%v", "backend": {"executable": "backend.exe"}, "settings_schema": %v}`, id, schema),
id, th.App)
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(id)
require.NoError(t, err)
require.NotNil(t, hooks)
_, ret := hooks.MessageWillBePosted(nil, nil)
if ret != "OK" {
return errors.New(ret)
}
return nil
}
// This is a meta-test function. It does the following:
// 1. Scans "tests/plugin_tests" folder
// 2. For each folder - compiles the main.go inside and executes it, validating it's result
// 3. If folder starts with "manual." it is skipped ("manual." tests executed in other part of this file)
// 4. Before compiling the main.go file is passed through templating and the following values are available in the template: BasicUser, BasicUser2, BasicChannel, BasicTeam, BasicPost
// 5. Successfully running test should return nil, "OK". Any other returned string is considered and error
func TestBasicAPIPlugins(t *testing.T) {
defaultSchema := getDefaultPluginSettingsSchema()
testFolder, found := fileutils.FindDir("mattermost-server/app/plugin_api_tests")
require.True(t, found, "Cannot read find app folder")
dirs, err := ioutil.ReadDir(testFolder)
require.NoError(t, err, "Cannot read test folder %v", testFolder)
for _, dir := range dirs {
d := dir.Name()
if dir.IsDir() && !strings.HasPrefix(d, "manual.") {
t.Run(d, func(t *testing.T) {
mainPath := path.Join(testFolder, d, "main.go")
_, err := os.Stat(mainPath)
require.NoError(t, err, "Cannot find plugin main file at %v", mainPath)
th := Setup(t).InitBasic()
defer th.TearDown()
setDefaultPluginConfig(th, dir.Name())
err = pluginAPIHookTest(t, th, mainPath, dir.Name(), defaultSchema)
require.NoError(t, err)
})
}
}
}
func TestPluginAPIKVCompareAndSet(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
testCases := []struct {
Description string
ExpectedValue []byte
}{
{
Description: "Testing non-nil, non-empty value",
ExpectedValue: []byte("value1"),
},
{
Description: "Testing empty value",
ExpectedValue: []byte(""),
},
}
for i, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
expectedKey := fmt.Sprintf("Key%d", i)
expectedValueEmpty := []byte("")
expectedValue1 := testCase.ExpectedValue
expectedValue2 := []byte("value2")
expectedValue3 := []byte("value3")
// Attempt update using an incorrect old value
updated, err := api.KVCompareAndSet(expectedKey, expectedValue2, expectedValue1)
require.Nil(t, err)
require.False(t, updated)
// Make sure no key is already created
value, err := api.KVGet(expectedKey)
require.Nil(t, err)
require.Nil(t, value)
// Insert using nil old value
updated, err = api.KVCompareAndSet(expectedKey, nil, expectedValue1)
require.Nil(t, err)
require.True(t, updated)
// Get inserted value
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue1, value)
// Attempt to insert again using nil old value
updated, err = api.KVCompareAndSet(expectedKey, nil, expectedValue2)
require.Nil(t, err)
require.False(t, updated)
// Get old value to assert nothing has changed
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue1, value)
// Update using correct old value
updated, err = api.KVCompareAndSet(expectedKey, expectedValue1, expectedValue2)
require.Nil(t, err)
require.True(t, updated)
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue2, value)
// Update using incorrect old value
updated, err = api.KVCompareAndSet(expectedKey, []byte("incorrect"), expectedValue3)
require.Nil(t, err)
require.False(t, updated)
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue2, value)
// Update using nil old value
updated, err = api.KVCompareAndSet(expectedKey, nil, expectedValue3)
require.Nil(t, err)
require.False(t, updated)
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue2, value)
// Update using empty old value
updated, err = api.KVCompareAndSet(expectedKey, expectedValueEmpty, expectedValue3)
require.Nil(t, err)
require.False(t, updated)
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue2, value)
})
}
}
func TestPluginAPIKVCompareAndDelete(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
testCases := []struct {
Description string
ExpectedValue []byte
}{
{
Description: "Testing non-nil, non-empty value",
ExpectedValue: []byte("value1"),
},
{
Description: "Testing empty value",
ExpectedValue: []byte(""),
},
}
for i, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
expectedKey := fmt.Sprintf("Key%d", i)
expectedValue1 := testCase.ExpectedValue
expectedValue2 := []byte("value2")
// Set the value
err := api.KVSet(expectedKey, expectedValue1)
require.Nil(t, err)
// Attempt delete using an incorrect old value
deleted, err := api.KVCompareAndDelete(expectedKey, expectedValue2)
require.Nil(t, err)
require.False(t, deleted)
// Make sure the value is still there
value, err := api.KVGet(expectedKey)
require.Nil(t, err)
require.Equal(t, expectedValue1, value)
// Attempt delete using the proper value
deleted, err = api.KVCompareAndDelete(expectedKey, expectedValue1)
require.Nil(t, err)
require.True(t, deleted)
// Verify it's deleted
value, err = api.KVGet(expectedKey)
require.Nil(t, err)
require.Nil(t, value)
})
}
}
func TestPluginCreateBot(t *testing.T) {
th := Setup(t)
defer th.TearDown()
api := th.SetupPluginAPI()
bot, err := api.CreateBot(&model.Bot{
Username: model.NewRandomString(10),
DisplayName: "bot",
Description: "bot",
})
require.Nil(t, err)
_, err = api.CreateBot(&model.Bot{
Username: model.NewRandomString(10),
OwnerId: bot.UserId,
DisplayName: "bot2",
Description: "bot2",
})
require.NotNil(t, err)
}
func TestPluginCreatePostWithUploadedFile(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
data := []byte("Hello World")
channelId := th.BasicChannel.Id
filename := "testGetFile"
fileInfo, err := api.UploadFile(data, channelId, filename)
require.Nil(t, err)
defer func() {
th.App.Srv().Store.FileInfo().PermanentDelete(fileInfo.Id)
th.App.RemoveFile(fileInfo.Path)
}()
actualData, err := api.GetFile(fileInfo.Id)
require.Nil(t, err)
assert.Equal(t, data, actualData)
userId := th.BasicUser.Id
post, err := api.CreatePost(&model.Post{
Message: "test",
UserId: userId,
ChannelId: channelId,
FileIds: model.StringArray{fileInfo.Id},
})
require.Nil(t, err)
assert.Equal(t, model.StringArray{fileInfo.Id}, post.FileIds)
actualPost, err := api.GetPost(post.Id)
require.Nil(t, err)
assert.Equal(t, model.StringArray{fileInfo.Id}, actualPost.FileIds)
}
func TestPluginAPIGetConfig(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
config := api.GetConfig()
if config.LdapSettings.BindPassword != nil && len(*config.LdapSettings.BindPassword) > 0 {
assert.Equal(t, *config.LdapSettings.BindPassword, model.FAKE_SETTING)
}
assert.Equal(t, *config.FileSettings.PublicLinkSalt, model.FAKE_SETTING)
if len(*config.FileSettings.AmazonS3SecretAccessKey) > 0 {
assert.Equal(t, *config.FileSettings.AmazonS3SecretAccessKey, model.FAKE_SETTING)
}
if config.EmailSettings.SMTPPassword != nil && len(*config.EmailSettings.SMTPPassword) > 0 {
assert.Equal(t, *config.EmailSettings.SMTPPassword, model.FAKE_SETTING)
}
if len(*config.GitLabSettings.Secret) > 0 {
assert.Equal(t, *config.GitLabSettings.Secret, model.FAKE_SETTING)
}
assert.Equal(t, *config.SqlSettings.DataSource, model.FAKE_SETTING)
assert.Equal(t, *config.SqlSettings.AtRestEncryptKey, model.FAKE_SETTING)
assert.Equal(t, *config.ElasticsearchSettings.Password, model.FAKE_SETTING)
for i := range config.SqlSettings.DataSourceReplicas {
assert.Equal(t, config.SqlSettings.DataSourceReplicas[i], model.FAKE_SETTING)
}
for i := range config.SqlSettings.DataSourceSearchReplicas {
assert.Equal(t, config.SqlSettings.DataSourceSearchReplicas[i], model.FAKE_SETTING)
}
}
func TestPluginAPIGetUnsanitizedConfig(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
config := api.GetUnsanitizedConfig()
if config.LdapSettings.BindPassword != nil && len(*config.LdapSettings.BindPassword) > 0 {
assert.NotEqual(t, *config.LdapSettings.BindPassword, model.FAKE_SETTING)
}
assert.NotEqual(t, *config.FileSettings.PublicLinkSalt, model.FAKE_SETTING)
if len(*config.FileSettings.AmazonS3SecretAccessKey) > 0 {
assert.NotEqual(t, *config.FileSettings.AmazonS3SecretAccessKey, model.FAKE_SETTING)
}
if config.EmailSettings.SMTPPassword != nil && len(*config.EmailSettings.SMTPPassword) > 0 {
assert.NotEqual(t, *config.EmailSettings.SMTPPassword, model.FAKE_SETTING)
}
if len(*config.GitLabSettings.Secret) > 0 {
assert.NotEqual(t, *config.GitLabSettings.Secret, model.FAKE_SETTING)
}
assert.NotEqual(t, *config.SqlSettings.DataSource, model.FAKE_SETTING)
assert.NotEqual(t, *config.SqlSettings.AtRestEncryptKey, model.FAKE_SETTING)
assert.NotEqual(t, *config.ElasticsearchSettings.Password, model.FAKE_SETTING)
for i := range config.SqlSettings.DataSourceReplicas {
assert.NotEqual(t, config.SqlSettings.DataSourceReplicas[i], model.FAKE_SETTING)
}
for i := range config.SqlSettings.DataSourceSearchReplicas {
assert.NotEqual(t, config.SqlSettings.DataSourceSearchReplicas[i], model.FAKE_SETTING)
}
}
func TestPluginAddUserToChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
member, err := api.AddUserToChannel(th.BasicChannel.Id, th.BasicUser.Id, th.BasicUser2.Id)
require.Nil(t, err)
require.NotNil(t, member)
require.Equal(t, th.BasicChannel.Id, member.ChannelId)
require.Equal(t, th.BasicUser.Id, member.UserId)
}
func TestInterpluginPluginHTTP(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
setupMultiPluginApiTest(t,
[]string{`
package main
import (
"github.com/mattermost/mattermost-server/v5/plugin"
"bytes"
"net/http"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v2/test" {
return
}
buf := bytes.Buffer{}
buf.ReadFrom(r.Body)
resp := "we got:" + buf.String()
w.WriteHeader(598)
w.Write([]byte(resp))
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
`
package main
import (
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/mattermost/mattermost-server/v5/model"
"bytes"
"net/http"
"io/ioutil"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
buf := bytes.Buffer{}
buf.WriteString("This is the request")
req, err := http.NewRequest("GET", "/testplugininterserver/api/v2/test", &buf)
if err != nil {
return nil, err.Error()
}
req.Header.Add("Mattermost-User-Id", "userid")
resp := p.API.PluginHTTP(req)
if resp == nil {
return nil, "Nil resp"
}
if resp.Body == nil {
return nil, "Nil body"
}
respbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err.Error()
}
if resp.StatusCode != 598 {
return nil, "wrong status " + string(respbody)
}
return nil, string(respbody)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
},
[]string{
`{"id": "testplugininterserver", "backend": {"executable": "backend.exe"}}`,
`{"id": "testplugininterclient", "backend": {"executable": "backend.exe"}}`,
},
[]string{
"testplugininterserver",
"testplugininterclient",
},
th.App,
)
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testplugininterclient")
require.NoError(t, err)
_, ret := hooks.MessageWillBePosted(nil, nil)
assert.Equal(t, "we got:This is the request", ret)
}
func TestApiMetrics(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("", func(t *testing.T) {
metricsMock := &mocks.MetricsInterface{}
pluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
webappPluginDir, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(pluginDir)
defer os.RemoveAll(webappPluginDir)
env, err := plugin.NewEnvironment(th.App.NewPluginAPI, pluginDir, webappPluginDir, th.App.Log(), metricsMock)
require.NoError(t, err)
th.App.SetPluginsEnvironment(env)
pluginId := model.NewId()
backend := filepath.Join(pluginDir, pluginId, "backend.exe")
code :=
`
package main
import (
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
)
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{})
}
`
utils.CompileGo(t, code, backend)
ioutil.WriteFile(filepath.Join(pluginDir, pluginId, "plugin.json"), []byte(`{"id": "`+pluginId+`", "backend": {"executable": "backend.exe"}}`), 0600)
// Don't care about these mocks
metricsMock.On("ObservePluginHookDuration", 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()
// Setup mocks
metricsMock.On("ObservePluginApiDuration", pluginId, "UpdateUser", true, mock.Anything).Return()
_, _, activationErr := env.Activate(pluginId)
require.NoError(t, activationErr)
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginId))
user1 := &model.User{
Email: model.NewId() + "success+test@example.com",
Nickname: "Darth Vader1",
Username: "vader" + model.NewId(),
Password: "passwd1",
AuthService: "",
}
_, appErr := th.App.CreateUser(user1)
require.Nil(t, appErr)
time.Sleep(1 * 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 TestPluginAPIGetPostsForChannel(t *testing.T) {
require := require.New(t)
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
numPosts := 10
// GetPostsForChannel returns posts ordered with the most recent first, so we
// need to invert the expected slice, the oldest post being BasicPost
expectedPosts := make([]*model.Post, numPosts)
expectedPosts[numPosts-1] = th.BasicPost
for i := numPosts - 2; i >= 0; i-- {
expectedPosts[i] = th.CreatePost(th.BasicChannel)
}
// CreatePost does not add Metadata, but initializes the structure. GetPostsForChannel
// returns nil for an empty Metadata, so we need to match that behaviour
for _, post := range expectedPosts {
post.Metadata = nil
}
postList, err := api.GetPostsForChannel(th.BasicChannel.Id, 0, 0)
require.Nil(err)
require.Nil(postList.ToSlice())
postList, err = api.GetPostsForChannel(th.BasicChannel.Id, 0, numPosts/2)
require.Nil(err)
require.Equal(expectedPosts[:numPosts/2], postList.ToSlice())
postList, err = api.GetPostsForChannel(th.BasicChannel.Id, 1, numPosts/2)
require.Nil(err)
require.Equal(expectedPosts[numPosts/2:], postList.ToSlice())
postList, err = api.GetPostsForChannel(th.BasicChannel.Id, 2, numPosts/2)
require.Nil(err)
require.Nil(postList.ToSlice())
postList, err = api.GetPostsForChannel(th.BasicChannel.Id, 0, numPosts+1)
require.Nil(err)
require.Equal(expectedPosts, postList.ToSlice())
}