mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The send-mail plugin test calls RetryInbucket only when GetMailBox returns an error. When mail delivery is slow, Inbucket returns an empty mailbox with no error, so the hook fails immediately instead of waiting. Wrap the hook invocation in require.Eventually so the test retries until SendMail delivery and mailbox assertions succeed under CI load. Tests-only change. Verified with go test -run '^TestBasicAPIPlugins$/test_send_mail_plugin$' -race -count=100 ./channels/app. Co-authored-by: mattermost-code <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
3953 lines
109 KiB
Go
3953 lines
109 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/utils"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8"
|
|
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
|
)
|
|
|
|
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]any{
|
|
"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, asMain bool, app *App, rctx request.CTX) string {
|
|
pluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = os.RemoveAll(pluginDir)
|
|
if err != nil {
|
|
t.Logf("Failed to cleanup pluginDir %s", err.Error())
|
|
}
|
|
})
|
|
|
|
webappPluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = os.RemoveAll(webappPluginDir)
|
|
if err != nil {
|
|
t.Logf("Failed to cleanup webappPluginDir %s", err.Error())
|
|
}
|
|
})
|
|
|
|
newPluginAPI := func(manifest *model.Manifest) plugin.API {
|
|
return app.NewPluginAPI(rctx, manifest)
|
|
}
|
|
|
|
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), 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")
|
|
if asMain {
|
|
utils.CompileGo(t, pluginCodes[i], backend)
|
|
} else {
|
|
utils.CompileGoTest(t, pluginCodes[i], backend)
|
|
}
|
|
|
|
err := os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifests[i]), 0600)
|
|
require.NoError(t, err)
|
|
manifest, activated, reterr := env.Activate(pluginID)
|
|
require.NoError(t, reterr)
|
|
require.NotNil(t, manifest)
|
|
require.True(t, activated)
|
|
|
|
app.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{
|
|
Enable: true,
|
|
}
|
|
})
|
|
}
|
|
|
|
app.ch.SetPluginsEnvironment(env)
|
|
|
|
return pluginDir
|
|
}
|
|
|
|
func setupPluginAPITest(t *testing.T, pluginCode string, pluginManifest string, pluginID string, app *App, rctx request.CTX) string {
|
|
asMain := pluginID != "test_db_driver"
|
|
return setupMultiPluginAPITest(t,
|
|
[]string{pluginCode}, []string{pluginManifest}, []string{pluginID},
|
|
asMain, app, rctx)
|
|
}
|
|
|
|
func TestPublicFilesPathConfiguration(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
pluginID := "com.mattermost.sample"
|
|
|
|
pluginDir := setupPluginAPITest(t,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/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, th.Context)
|
|
|
|
publicFilesFolderInTest := filepath.Join(pluginDir, pluginID, "public")
|
|
publicFilesPath, err := th.App.GetPluginsEnvironment().PublicFilesPath(pluginID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, publicFilesPath, publicFilesFolderInTest)
|
|
}
|
|
|
|
func TestPluginAPIGetUserPreference(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should return preferences when called", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
err := api.UpdatePreferencesForUser(th.BasicUser.Id, []model.Preference{
|
|
{
|
|
UserId: th.BasicUser.Id,
|
|
Category: model.PreferenceCategoryDisplaySettings,
|
|
Name: model.PreferenceNameUseMilitaryTime,
|
|
Value: "true",
|
|
},
|
|
{
|
|
UserId: th.BasicUser.Id,
|
|
Category: "test_category",
|
|
Name: "test_key",
|
|
Value: "test_value",
|
|
},
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
preference, err := api.GetPreferenceForUser(th.BasicUser.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime)
|
|
|
|
require.Nil(t, err)
|
|
assert.Equal(t, model.PreferenceCategoryDisplaySettings, preference.Category)
|
|
assert.Equal(t, model.PreferenceNameUseMilitaryTime, preference.Name)
|
|
assert.Equal(t, "true", preference.Value)
|
|
|
|
preference, err = api.GetPreferenceForUser(th.BasicUser.Id, "test_category", "test_key")
|
|
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "test_category", preference.Category)
|
|
assert.Equal(t, "test_key", preference.Name)
|
|
assert.Equal(t, "test_value", preference.Value)
|
|
})
|
|
|
|
t.Run("should return an error when a user doesn't have a preference set", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
_, err := api.GetPreferenceForUser(th.BasicUser.Id, "something", "that doesn't exist")
|
|
assert.NotNil(t, err)
|
|
})
|
|
}
|
|
|
|
func TestPluginAPIGetUserPreferences(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user1" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
preferences, err := api.GetPreferencesForUser(user1.Id)
|
|
require.Nil(t, err)
|
|
require.Len(t, preferences, 3)
|
|
|
|
prefByCategory := make(map[string]model.Preference, len(preferences))
|
|
for _, p := range preferences {
|
|
prefByCategory[p.Category] = p
|
|
}
|
|
require.Len(t, prefByCategory, 3, "default user preferences should use distinct categories")
|
|
|
|
nextSteps, ok := prefByCategory[model.PreferenceRecommendedNextSteps]
|
|
require.True(t, ok, "expected default preference for category %q", model.PreferenceRecommendedNextSteps)
|
|
assert.Equal(t, user1.Id, nextSteps.UserId)
|
|
assert.Equal(t, "hide", nextSteps.Name)
|
|
assert.Equal(t, "false", nextSteps.Value)
|
|
|
|
_, ok = prefByCategory[model.PreferenceCategorySystemNotice]
|
|
require.True(t, ok, "expected default preference for category %q", model.PreferenceCategorySystemNotice)
|
|
|
|
tutorial, ok := prefByCategory[model.PreferenceCategoryTutorialSteps]
|
|
require.True(t, ok, "expected default preference for category %q", model.PreferenceCategoryTutorialSteps)
|
|
assert.Equal(t, user1.Id, tutorial.UserId)
|
|
assert.Equal(t, user1.Id, tutorial.Name)
|
|
assert.Equal(t, "0", tutorial.Value)
|
|
}
|
|
|
|
func TestPluginAPIDeleteUserPreferences(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user1" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
preferences, err := api.GetPreferencesForUser(user1.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 3, len(preferences))
|
|
|
|
err = api.DeletePreferencesForUser(user1.Id, preferences)
|
|
require.Nil(t, err)
|
|
preferences, err = api.GetPreferencesForUser(user1.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 0, len(preferences))
|
|
|
|
user2, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user2" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user2)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
preference := model.Preference{
|
|
Name: user2.Id,
|
|
UserId: user2.Id,
|
|
Category: model.PreferenceCategoryTheme,
|
|
Value: `{"color": "#ff0000", "color2": "#faf"}`,
|
|
}
|
|
err = api.UpdatePreferencesForUser(user2.Id, []model.Preference{preference})
|
|
require.Nil(t, err)
|
|
|
|
preferences, err = api.GetPreferencesForUser(user2.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 4, len(preferences))
|
|
|
|
err = api.DeletePreferencesForUser(user2.Id, []model.Preference{preference})
|
|
require.Nil(t, err)
|
|
preferences, err = api.GetPreferencesForUser(user2.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 3, len(preferences))
|
|
assert.ElementsMatch(t,
|
|
[]string{model.PreferenceRecommendedNextSteps, model.PreferenceCategoryTutorialSteps, model.PreferenceCategorySystemNotice},
|
|
[]string{preferences[0].Category, preferences[1].Category, preferences[2].Category},
|
|
)
|
|
}
|
|
|
|
func TestPluginAPIUpdateUserPreferences(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user1" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
preferences, err := api.GetPreferencesForUser(user1.Id)
|
|
require.Nil(t, err)
|
|
require.Len(t, preferences, 3)
|
|
|
|
prefByCategory := make(map[string]model.Preference, len(preferences))
|
|
for _, p := range preferences {
|
|
prefByCategory[p.Category] = p
|
|
}
|
|
require.Len(t, prefByCategory, 3, "default user preferences should use distinct categories")
|
|
|
|
nextSteps, ok := prefByCategory[model.PreferenceRecommendedNextSteps]
|
|
require.True(t, ok, "expected default preference for category %q", model.PreferenceRecommendedNextSteps)
|
|
assert.Equal(t, user1.Id, nextSteps.UserId)
|
|
assert.Equal(t, "hide", nextSteps.Name)
|
|
assert.Equal(t, "false", nextSteps.Value)
|
|
|
|
_, ok = prefByCategory[model.PreferenceCategorySystemNotice]
|
|
require.True(t, ok, "expected default preference for category %q", model.PreferenceCategorySystemNotice)
|
|
|
|
tutorial, ok := prefByCategory[model.PreferenceCategoryTutorialSteps]
|
|
require.True(t, ok, "expected default preference for category %q", model.PreferenceCategoryTutorialSteps)
|
|
assert.Equal(t, user1.Id, tutorial.UserId)
|
|
assert.Equal(t, user1.Id, tutorial.Name)
|
|
assert.Equal(t, "0", tutorial.Value)
|
|
|
|
preference := model.Preference{
|
|
Name: user1.Id,
|
|
UserId: user1.Id,
|
|
Category: model.PreferenceCategoryTheme,
|
|
Value: `{"color": "#ff0000", "color2": "#faf"}`,
|
|
}
|
|
|
|
err = api.UpdatePreferencesForUser(user1.Id, []model.Preference{preference})
|
|
require.Nil(t, err)
|
|
|
|
preferences, err = api.GetPreferencesForUser(user1.Id)
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, 4, len(preferences))
|
|
expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps, model.PreferenceCategorySystemNotice}
|
|
for _, pref := range preferences {
|
|
assert.Contains(t, expectedCategories, pref.Category)
|
|
assert.Equal(t, user1.Id, pref.UserId)
|
|
}
|
|
}
|
|
|
|
func TestPluginAPIGetUsers(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).DeleteBots(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user1" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user2, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user2" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user2)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user3, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user3" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user3)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user4, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user4" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user4)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
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 TestPluginAPIGetUsersByIds(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).DeleteBots(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user1" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user2, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user2" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user2)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user3, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user3" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user3)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
requestedIDs []string
|
|
}{
|
|
{
|
|
"no users",
|
|
[]string{},
|
|
},
|
|
{
|
|
"getting 1 and 3",
|
|
[]string{user1.Id, user3.Id},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
users, err := api.GetUsersByIds(testCase.requestedIDs)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, len(testCase.requestedIDs), len(users))
|
|
for _, user := range users {
|
|
assert.Contains(t, testCase.requestedIDs, user.Id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginAPIGetUsersInTeam(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
team1 := th.CreateTeam(t)
|
|
team2 := th.CreateTeam(t)
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user1" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user2, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user2" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user2)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user3, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user3" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user3)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
user4, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Password: model.NewTestPassword(),
|
|
Username: "user4" + model.NewId(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user4)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
// Add all users to team 1
|
|
_, appErr := th.App.JoinUserToTeam(th.Context, team1, user1, "")
|
|
require.Nil(t, appErr)
|
|
_, appErr = th.App.JoinUserToTeam(th.Context, team1, user2, "")
|
|
require.Nil(t, appErr)
|
|
_, appErr = th.App.JoinUserToTeam(th.Context, team1, user3, "")
|
|
require.Nil(t, appErr)
|
|
_, appErr = th.App.JoinUserToTeam(th.Context, team1, user4, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Add only user3 and user4 to team 2
|
|
_, appErr = th.App.JoinUserToTeam(th.Context, team2, user3, "")
|
|
require.Nil(t, appErr)
|
|
_, appErr = th.App.JoinUserToTeam(th.Context, team2, user4, "")
|
|
require.Nil(t, appErr)
|
|
|
|
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)
|
|
usersMap := make(map[string]bool)
|
|
for _, user := range testCase.ExpectedUsers {
|
|
usersMap[user.Id] = true
|
|
}
|
|
for _, user := range users {
|
|
delete(usersMap, user.Id)
|
|
}
|
|
assert.Empty(t, usersMap)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginAPIUserCustomStatus(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
user1, err := th.App.CreateUser(th.Context, &model.User{
|
|
Email: strings.ToLower(model.NewId()) + "[email protected]",
|
|
Username: "user_" + model.NewId(),
|
|
Password: model.NewTestPassword(),
|
|
})
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
appErr := th.App.PermanentDeleteUser(th.Context, user1)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
custom := &model.CustomStatus{
|
|
Emoji: "tada",
|
|
Text: "honk",
|
|
}
|
|
|
|
err = api.UpdateUserCustomStatus(user1.Id, custom)
|
|
assert.Nil(t, err)
|
|
userCs, err := th.App.GetCustomStatus(user1.Id)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, custom, userCs)
|
|
|
|
custom.Text = ""
|
|
err = api.UpdateUserCustomStatus(user1.Id, custom)
|
|
assert.Nil(t, err)
|
|
userCs, err = th.App.GetCustomStatus(user1.Id)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, custom, userCs)
|
|
|
|
custom.Text = "honk"
|
|
custom.Emoji = ""
|
|
err = api.UpdateUserCustomStatus(user1.Id, custom)
|
|
assert.Nil(t, err)
|
|
userCs, err = th.App.GetCustomStatus(user1.Id)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, custom, userCs)
|
|
|
|
custom.Text = ""
|
|
err = api.UpdateUserCustomStatus(user1.Id, custom)
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, err.Error(), "SetCustomStatus: Failed to update the custom status. Please add either emoji or custom text status or both.")
|
|
|
|
// Remove custom status
|
|
err = api.RemoveUserCustomStatus(user1.Id)
|
|
assert.Nil(t, err)
|
|
var csClear *model.CustomStatus
|
|
userCs, err = th.App.GetCustomStatus(user1.Id)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, csClear, userCs)
|
|
}
|
|
|
|
func TestPluginAPIGetFile(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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(th.Context, uploadTime, th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, filename, fileData, true)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info.Id)
|
|
require.NoError(t, err)
|
|
appErr := th.App.RemoveFile(info.Path)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
fileInfo1, err := th.App.DoUploadFile(th.Context,
|
|
time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC),
|
|
th.BasicTeam.Id,
|
|
th.BasicChannel.Id,
|
|
th.BasicUser.Id,
|
|
"testFile1",
|
|
[]byte("testfile1 Content"),
|
|
true,
|
|
)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, fileInfo1.Id)
|
|
require.NoError(t, err)
|
|
appErr := th.App.RemoveFile(fileInfo1.Path)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
fileInfo2, err := th.App.DoUploadFile(th.Context,
|
|
time.Date(2020, 1, 2, 1, 1, 1, 1, time.UTC),
|
|
th.BasicTeam.Id,
|
|
th.BasicChannel.Id,
|
|
th.BasicUser2.Id,
|
|
"testFile2",
|
|
[]byte("testfile2 Content"),
|
|
true,
|
|
)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, fileInfo2.Id)
|
|
require.NoError(t, err)
|
|
appErr := th.App.RemoveFile(fileInfo2.Path)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
fileInfo3, err := th.App.DoUploadFile(th.Context,
|
|
time.Date(2020, 1, 3, 1, 1, 1, 1, time.UTC),
|
|
th.BasicTeam.Id,
|
|
th.BasicChannel.Id,
|
|
th.BasicUser.Id,
|
|
"testFile3",
|
|
[]byte("testfile3 Content"),
|
|
true,
|
|
)
|
|
require.Nil(t, err)
|
|
defer func() {
|
|
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, fileInfo3.Id)
|
|
require.NoError(t, err)
|
|
appErr := th.App.RemoveFile(fileInfo3.Path)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
_, 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.FileinfoSortByCreated,
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
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, th.Context, manifest)
|
|
|
|
pluginConfigJsonString := `{"mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true}`
|
|
|
|
var pluginConfig map[string]any
|
|
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 TestPluginAPISavePluginConfigPreservesOtherPlugins(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
otherPluginConfig := map[string]any{
|
|
"setting1": "value1",
|
|
"setting2": "value2",
|
|
}
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.Plugins["otherplugin"] = otherPluginConfig
|
|
})
|
|
|
|
manifest := &model.Manifest{
|
|
Id: "pluginid",
|
|
SettingsSchema: &model.PluginSettingsSchema{
|
|
Settings: []*model.PluginSetting{
|
|
{Key: "MyStringSetting", Type: "text"},
|
|
},
|
|
},
|
|
}
|
|
|
|
api := NewPluginAPI(th.App, th.Context, manifest)
|
|
|
|
pluginConfig := map[string]any{"mystringsetting": "str"}
|
|
appErr := api.SavePluginConfig(pluginConfig)
|
|
require.Nil(t, appErr)
|
|
|
|
cfg := th.App.Config()
|
|
assert.Contains(t, cfg.PluginSettings.Plugins, "otherplugin")
|
|
assert.Equal(t, otherPluginConfig, cfg.PluginSettings.Plugins["otherplugin"])
|
|
}
|
|
|
|
func TestPluginAPILoadPluginConfiguration(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
var pluginJson map[string]any
|
|
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
|
|
})
|
|
|
|
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_load_configuration_plugin", "main.go")
|
|
|
|
err = pluginAPIHookTest(t, th, fullPath, "testloadpluginconfig", `{
|
|
"settings": [
|
|
{
|
|
"key": "MyStringSetting",
|
|
"type": "text"
|
|
},
|
|
{
|
|
"key": "MyIntSetting",
|
|
"type": "text"
|
|
},
|
|
{
|
|
"key": "MyBoolSetting",
|
|
"type": "bool"
|
|
}
|
|
]
|
|
}`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPluginAPILoadPluginConfigurationDefaults(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
var pluginJson map[string]any
|
|
err := json.Unmarshal([]byte(`{"mystringsetting": "override"}`), &pluginJson)
|
|
require.NoError(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.Plugins["testloadpluginconfig"] = pluginJson
|
|
})
|
|
|
|
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "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 TestPluginAPILoadPluginConfigurationDefaultsFromSections(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
var pluginJson map[string]any
|
|
err := json.Unmarshal([]byte(`{"mysectionstringsetting": "override"}`), &pluginJson)
|
|
require.NoError(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.Plugins["testsectiondefaults"] = pluginJson
|
|
})
|
|
|
|
manifest := &model.Manifest{
|
|
Id: "testsectiondefaults",
|
|
SettingsSchema: &model.PluginSettingsSchema{
|
|
Sections: []*model.PluginSettingsSection{
|
|
{
|
|
Key: "section1",
|
|
Settings: []*model.PluginSetting{
|
|
{Key: "MySectionStringSetting", Type: "text", Default: "notthis"},
|
|
{Key: "MySectionIntSetting", Type: "text", Default: float64(42)},
|
|
{Key: "MySectionBoolSetting", Type: "bool", Default: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
api := NewPluginAPI(th.App, th.Context, manifest)
|
|
|
|
var dest struct {
|
|
MySectionStringSetting string
|
|
MySectionIntSetting int
|
|
MySectionBoolSetting bool
|
|
}
|
|
err = api.LoadPluginConfiguration(&dest)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "override", dest.MySectionStringSetting) // saved config overrides default
|
|
assert.Equal(t, 42, dest.MySectionIntSetting) // default applied from section
|
|
assert.True(t, dest.MySectionBoolSetting) // default applied from section
|
|
}
|
|
|
|
func TestPluginAPILoadPluginConfigurationDefaultsMixed(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
var pluginJson map[string]any
|
|
err := json.Unmarshal([]byte(`{"toplevelsetting": "saved_value"}`), &pluginJson)
|
|
require.NoError(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.Plugins["testmixeddefaults"] = pluginJson
|
|
})
|
|
|
|
manifest := &model.Manifest{
|
|
Id: "testmixeddefaults",
|
|
SettingsSchema: &model.PluginSettingsSchema{
|
|
Settings: []*model.PluginSetting{
|
|
{Key: "TopLevelSetting", Type: "text", Default: "top_default"},
|
|
{Key: "TopLevelBool", Type: "bool", Default: true},
|
|
},
|
|
Sections: []*model.PluginSettingsSection{
|
|
{
|
|
Key: "section1",
|
|
Settings: []*model.PluginSetting{
|
|
{Key: "SectionSetting", Type: "text", Default: "section_default"},
|
|
{Key: "SectionInt", Type: "text", Default: float64(99)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
api := NewPluginAPI(th.App, th.Context, manifest)
|
|
|
|
var dest struct {
|
|
TopLevelSetting string
|
|
TopLevelBool bool
|
|
SectionSetting string
|
|
SectionInt int
|
|
}
|
|
err = api.LoadPluginConfiguration(&dest)
|
|
require.NoError(t, err)
|
|
|
|
// Top-level: saved config overrides default
|
|
assert.Equal(t, "saved_value", dest.TopLevelSetting)
|
|
// Top-level: default applied
|
|
assert.True(t, dest.TopLevelBool)
|
|
// Section: default applied
|
|
assert.Equal(t, "section_default", dest.SectionSetting)
|
|
// Section: default applied
|
|
assert.Equal(t, 99, dest.SectionInt)
|
|
}
|
|
|
|
func TestPluginAPIGetPlugins(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
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(), 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)
|
|
|
|
err := os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), fmt.Appendf(nil, `{"id": "%s", "server": {"executable": "backend.exe"}}`, pluginID), 0600)
|
|
require.NoError(t, err)
|
|
manifest, activated, reterr := env.Activate(pluginID)
|
|
|
|
require.NoError(t, reterr)
|
|
require.NotNil(t, manifest)
|
|
require.True(t, activated)
|
|
pluginManifests = append(pluginManifests, manifest)
|
|
}
|
|
th.App.ch.SetPluginsEnvironment(env)
|
|
|
|
// Deactivate the last one for testing
|
|
success := env.Deactivate(pluginIDs[len(pluginIDs)-1])
|
|
require.True(t, success)
|
|
|
|
// check existing user first
|
|
plugins, appErr := api.GetPlugins()
|
|
assert.Nil(t, appErr)
|
|
assert.NotEmpty(t, plugins)
|
|
assert.Equal(t, pluginManifests, plugins)
|
|
}
|
|
|
|
func TestPluginAPIInstallPlugin(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
tarData, err := os.ReadFile(filepath.Join(server.GetPackagePath(), "tests", "testplugin.tar.gz"))
|
|
require.NoError(t, err)
|
|
|
|
_, appErr := api.InstallPlugin(bytes.NewReader(tarData), true)
|
|
assert.NotNil(t, appErr, "should not allow upload if upload disabled")
|
|
assert.Equal(t, appErr.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, appErr := api.InstallPlugin(bytes.NewReader(tarData), true)
|
|
defer os.RemoveAll("plugins/testplugin")
|
|
require.Nil(t, appErr)
|
|
assert.Equal(t, "testplugin", manifest.Id)
|
|
|
|
// Successfully installed
|
|
pluginsResp, appErr := api.GetPlugins()
|
|
require.Nil(t, appErr)
|
|
|
|
found := false
|
|
for _, m := range pluginsResp {
|
|
if m.Id == manifest.Id {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestInstallPlugin(t *testing.T) {
|
|
mainHelper.Parallel(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, rctx request.CTX) (func(), string) {
|
|
pluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
webappPluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
|
|
app.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Directory = pluginDir
|
|
*cfg.PluginSettings.ClientDirectory = webappPluginDir
|
|
})
|
|
|
|
newPluginAPI := func(manifest *model.Manifest) plugin.API {
|
|
return app.NewPluginAPI(rctx, manifest)
|
|
}
|
|
|
|
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil)
|
|
require.NoError(t, err)
|
|
|
|
app.ch.SetPluginsEnvironment(env)
|
|
|
|
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
|
|
utils.CompileGo(t, pluginCode, backend)
|
|
|
|
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
|
|
require.NoError(t, err)
|
|
manifest, activated, reterr := env.Activate(pluginID)
|
|
require.NoError(t, reterr)
|
|
require.NotNil(t, manifest)
|
|
require.True(t, activated)
|
|
|
|
return func() {
|
|
os.RemoveAll(pluginDir)
|
|
os.RemoveAll(webappPluginDir)
|
|
}, pluginDir
|
|
}
|
|
|
|
th := Setup(t)
|
|
|
|
// start an http server to serve plugin's tarball to the test.
|
|
ts := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(server.GetPackagePath(), "tests"))))
|
|
defer ts.Close()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = true
|
|
*cfg.PluginSettings.EnableUploads = true
|
|
cfg.PluginSettings.Plugins["testinstallplugin"] = map[string]any{
|
|
"DownloadURL": ts.URL + "/testplugin.tar.gz",
|
|
}
|
|
})
|
|
|
|
tearDown, _ := setupTest(t,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/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", "server": {"executable": "backend.exe"}, "settings_schema": {
|
|
"settings": [
|
|
{
|
|
"key": "DownloadURL",
|
|
"type": "text"
|
|
}
|
|
]
|
|
}}`, "testinstallplugin", th.App, th.Context)
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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.NoError(t, err)
|
|
dataBytes := buf.Bytes()
|
|
fileReader := bytes.NewReader(dataBytes)
|
|
|
|
// Set the Team Icon
|
|
appErr := th.App.SetTeamIconFromFile(th.Context, th.BasicTeam, fileReader)
|
|
require.Nil(t, appErr)
|
|
|
|
// Get the team icon to check
|
|
teamIcon, appErr := api.GetTeamIcon(th.BasicTeam.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotEmpty(t, teamIcon)
|
|
|
|
colorful := color.NRGBA{255, 0, 0, 255}
|
|
byteReader := bytes.NewReader(teamIcon)
|
|
img2, _, err2 := image.Decode(byteReader)
|
|
require.NoError(t, err2)
|
|
require.Equal(t, img2.At(2, 3), colorful)
|
|
}
|
|
|
|
func TestPluginAPISetTeamIcon(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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.NoError(t, err)
|
|
dataBytes := buf.Bytes()
|
|
|
|
// Set the user profile image
|
|
appErr := api.SetTeamIcon(th.BasicTeam.Id, dataBytes)
|
|
require.Nil(t, appErr)
|
|
|
|
// Get the user profile image to check
|
|
teamIcon, appErr := api.GetTeamIcon(th.BasicTeam.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotEmpty(t, teamIcon)
|
|
|
|
colorful := color.NRGBA{255, 0, 0, 255}
|
|
byteReader := bytes.NewReader(teamIcon)
|
|
img2, _, err2 := image.Decode(byteReader)
|
|
require.NoError(t, err2)
|
|
require.Equal(t, img2.At(2, 3), colorful)
|
|
}
|
|
|
|
func TestPluginAPIRemoveTeamIcon(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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.NoError(t, err1)
|
|
dataBytes := buf.Bytes()
|
|
fileReader := bytes.NewReader(dataBytes)
|
|
|
|
// Set the Team Icon
|
|
err := th.App.SetTeamIconFromFile(th.Context, 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 := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
code := string(data)
|
|
schema := `{"settings": [ ] }`
|
|
if settingsSchema != "" {
|
|
schema = settingsSchema
|
|
}
|
|
th.App.ch.srv.platform.SetSqlStore(th.GetSqlStore()) // TODO: platform: check if necessary
|
|
setupPluginAPITest(t, code,
|
|
fmt.Sprintf(`{"id": "%v", "server": {"executable": "backend.exe"}, "settings_schema": %v}`, id, schema),
|
|
id, th.App, th.Context)
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, hooks)
|
|
|
|
runHook := func() error {
|
|
_, ret := hooks.MessageWillBePosted(nil, nil)
|
|
if ret != "OK" {
|
|
return errors.New(ret)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SendMail + Inbucket delivery is async; under CI load the plugin's internal
|
|
// RetryInbucket window can expire before the message is visible.
|
|
if id == "test_send_mail_plugin" {
|
|
require.Eventually(t, func() bool {
|
|
return runHook() == nil
|
|
}, 90*time.Second, 2*time.Second)
|
|
return nil
|
|
}
|
|
|
|
return runHook()
|
|
}
|
|
|
|
// 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) {
|
|
mainHelper.Parallel(t)
|
|
defaultSchema := getDefaultPluginSettingsSchema()
|
|
testFolder := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests")
|
|
dirs, err := os.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) {
|
|
mainHelper.Parallel(t)
|
|
|
|
mainPath := filepath.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(t).DeleteBots(t)
|
|
|
|
setDefaultPluginConfig(th, dir.Name())
|
|
err = pluginAPIHookTest(t, th, mainPath, dir.Name(), defaultSchema)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPluginAPIKVCompareAndSet(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
bot, err := api.CreateBot(&model.Bot{
|
|
Username: "a" + model.NewRandomString(10),
|
|
DisplayName: "bot",
|
|
Description: "bot",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
_, err = api.CreateBot(&model.Bot{
|
|
Username: "a" + model.NewRandomString(10),
|
|
OwnerId: bot.UserId,
|
|
DisplayName: "bot2",
|
|
Description: "bot2",
|
|
})
|
|
require.NotNil(t, err)
|
|
}
|
|
|
|
func TestPluginCreatePostWithUploadedFile(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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() {
|
|
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, fileInfo.Id)
|
|
require.NoError(t, err)
|
|
appErr := th.App.RemoveFile(fileInfo.Path)
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
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 TestPluginCreatePostAddsFromPluginProp(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
channelID := th.BasicChannel.Id
|
|
userID := th.BasicUser.Id
|
|
post, err := api.CreatePost(&model.Post{
|
|
Message: "test",
|
|
ChannelId: channelID,
|
|
UserId: userID,
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
actualPost, err := api.GetPost(post.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "true", actualPost.GetProp(model.PostPropsFromPlugin))
|
|
}
|
|
|
|
func TestPluginAPIGetConfig(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
config := api.GetConfig()
|
|
if config.LdapSettings.BindPassword != nil && *config.LdapSettings.BindPassword != "" {
|
|
assert.Equal(t, *config.LdapSettings.BindPassword, model.FakeSetting)
|
|
}
|
|
|
|
assert.Equal(t, *config.FileSettings.PublicLinkSalt, model.FakeSetting)
|
|
|
|
if *config.FileSettings.AmazonS3SecretAccessKey != "" {
|
|
assert.Equal(t, *config.FileSettings.AmazonS3SecretAccessKey, model.FakeSetting)
|
|
}
|
|
|
|
if config.EmailSettings.SMTPPassword != nil && *config.EmailSettings.SMTPPassword != "" {
|
|
assert.Equal(t, *config.EmailSettings.SMTPPassword, model.FakeSetting)
|
|
}
|
|
|
|
if *config.GitLabSettings.Secret != "" {
|
|
assert.Equal(t, *config.GitLabSettings.Secret, model.FakeSetting)
|
|
}
|
|
|
|
assert.Equal(t, *config.SqlSettings.DataSource, model.FakeSetting)
|
|
assert.Equal(t, *config.SqlSettings.AtRestEncryptKey, model.FakeSetting)
|
|
assert.Equal(t, *config.ElasticsearchSettings.Password, model.FakeSetting)
|
|
|
|
for i := range config.SqlSettings.DataSourceReplicas {
|
|
assert.Equal(t, config.SqlSettings.DataSourceReplicas[i], model.FakeSetting)
|
|
}
|
|
|
|
for i := range config.SqlSettings.DataSourceSearchReplicas {
|
|
assert.Equal(t, config.SqlSettings.DataSourceSearchReplicas[i], model.FakeSetting)
|
|
}
|
|
}
|
|
|
|
func TestPluginAPIGetUnsanitizedConfig(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
config := api.GetUnsanitizedConfig()
|
|
if config.LdapSettings.BindPassword != nil && *config.LdapSettings.BindPassword != "" {
|
|
assert.NotEqual(t, *config.LdapSettings.BindPassword, model.FakeSetting)
|
|
}
|
|
|
|
assert.NotEqual(t, *config.FileSettings.PublicLinkSalt, model.FakeSetting)
|
|
|
|
if *config.FileSettings.AmazonS3SecretAccessKey != "" {
|
|
assert.NotEqual(t, *config.FileSettings.AmazonS3SecretAccessKey, model.FakeSetting)
|
|
}
|
|
|
|
if config.EmailSettings.SMTPPassword != nil && *config.EmailSettings.SMTPPassword != "" {
|
|
assert.NotEqual(t, *config.EmailSettings.SMTPPassword, model.FakeSetting)
|
|
}
|
|
|
|
if *config.GitLabSettings.Secret != "" {
|
|
assert.NotEqual(t, *config.GitLabSettings.Secret, model.FakeSetting)
|
|
}
|
|
|
|
assert.NotEqual(t, *config.SqlSettings.DataSource, model.FakeSetting)
|
|
assert.NotEqual(t, *config.SqlSettings.AtRestEncryptKey, model.FakeSetting)
|
|
assert.NotEqual(t, *config.ElasticsearchSettings.Password, model.FakeSetting)
|
|
|
|
for i := range config.SqlSettings.DataSourceReplicas {
|
|
assert.NotEqual(t, config.SqlSettings.DataSourceReplicas[i], model.FakeSetting)
|
|
}
|
|
|
|
for i := range config.SqlSettings.DataSourceSearchReplicas {
|
|
assert.NotEqual(t, config.SqlSettings.DataSourceSearchReplicas[i], model.FakeSetting)
|
|
}
|
|
}
|
|
|
|
func TestPluginAddUserToChannel(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
setupMultiPluginAPITest(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"bytes"
|
|
"net/http"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v2/test":
|
|
if r.URL.Query().Get("abc") != "xyz" {
|
|
return
|
|
}
|
|
|
|
if r.Header.Get("Mattermost-Plugin-ID") != "testplugininterclient" {
|
|
return
|
|
}
|
|
|
|
buf := bytes.Buffer{}
|
|
buf.ReadFrom(r.Body)
|
|
resp := "we got:" + buf.String()
|
|
w.WriteHeader(598)
|
|
w.Write([]byte(resp))
|
|
if r.URL.Path != "/api/v2/test" {
|
|
return
|
|
}
|
|
case "/nobody":
|
|
w.WriteHeader(599)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"bytes"
|
|
"net/http"
|
|
"io"
|
|
)
|
|
|
|
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?abc=xyz", &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 := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err.Error()
|
|
}
|
|
if resp.StatusCode != 598 {
|
|
return nil, "wrong status " + string(respbody)
|
|
}
|
|
|
|
if string(respbody) != "we got:This is the request" {
|
|
return nil, "wrong response " + string(respbody)
|
|
}
|
|
|
|
req, err = http.NewRequest("GET", "/testplugininterserver/nobody", nil)
|
|
if err != nil {
|
|
return nil, err.Error()
|
|
}
|
|
|
|
resp = p.API.PluginHTTP(req)
|
|
if resp == nil {
|
|
return nil, "Nil resp"
|
|
}
|
|
|
|
if resp.StatusCode != 599 {
|
|
return nil, "wrong status " + string(respbody)
|
|
}
|
|
|
|
return nil, "ok"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
},
|
|
[]string{
|
|
`{"id": "testplugininterserver", "server": {"executable": "backend.exe"}}`,
|
|
`{"id": "testplugininterclient", "server": {"executable": "backend.exe"}}`,
|
|
},
|
|
[]string{
|
|
"testplugininterserver",
|
|
"testplugininterclient",
|
|
},
|
|
true,
|
|
th.App,
|
|
th.Context,
|
|
)
|
|
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testplugininterclient")
|
|
require.NoError(t, err)
|
|
_, ret := hooks.MessageWillBePosted(nil, nil)
|
|
assert.Equal(t, "ok", ret)
|
|
}
|
|
|
|
func TestInterpluginPluginHTTPWithBodyAfterWriteHeader(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
setupMultiPluginAPITest(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/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" {
|
|
if r.URL.Query().Get("abc") != "xyz" {
|
|
return
|
|
}
|
|
|
|
if r.Header.Get("Mattermost-Plugin-ID") != "testpluginbodyafter" {
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(598)
|
|
buf := bytes.Buffer{}
|
|
buf.ReadFrom(r.Body)
|
|
resp := "we got:" + buf.String()
|
|
w.Write([]byte(resp))
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"bytes"
|
|
"net/http"
|
|
"io"
|
|
)
|
|
|
|
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 body")
|
|
req, err := http.NewRequest("POST", "/testpluginbodyafterserver/api/v2/test?abc=xyz", &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 := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err.Error()
|
|
}
|
|
if resp.StatusCode != 598 {
|
|
return nil, "wrong status " + string(respbody)
|
|
}
|
|
|
|
if string(respbody) != "we got:This is the request body" {
|
|
return nil, "wrong response: " + string(respbody)
|
|
}
|
|
|
|
return nil, "ok"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
},
|
|
[]string{
|
|
`{"id": "testpluginbodyafterserver", "server": {"executable": "backend.exe"}}`,
|
|
`{"id": "testpluginbodyafter", "server": {"executable": "backend.exe"}}`,
|
|
},
|
|
[]string{
|
|
"testpluginbodyafterserver",
|
|
"testpluginbodyafter",
|
|
},
|
|
true,
|
|
th.App,
|
|
th.Context,
|
|
)
|
|
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testpluginbodyafter")
|
|
require.NoError(t, err)
|
|
_, ret := hooks.MessageWillBePosted(nil, nil)
|
|
assert.Equal(t, "ok", ret)
|
|
}
|
|
|
|
func TestInterpluginPluginHTTPStreaming(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("large payload streaming", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
setupMultiPluginAPITest(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"bytes"
|
|
"net/http"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v2/largepayload":
|
|
// Generate 1MB payload in 64KB chunks
|
|
chunkSize := 64 * 1024
|
|
totalChunks := 16
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
for i := 0; i < totalChunks; i++ {
|
|
chunk := bytes.Repeat([]byte("X"), chunkSize)
|
|
w.Write(chunk)
|
|
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"net/http"
|
|
"io"
|
|
"fmt"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
req, err := http.NewRequest("GET", "/testpluginlargepayloadserver/api/v2/largepayload", nil)
|
|
if err != nil {
|
|
return nil, err.Error()
|
|
}
|
|
|
|
resp := p.API.PluginHTTP(req)
|
|
if resp == nil {
|
|
return nil, "Nil resp"
|
|
}
|
|
|
|
if resp.Body == nil {
|
|
return nil, "Nil body"
|
|
}
|
|
|
|
// Read response incrementally
|
|
totalRead := 0
|
|
buf := make([]byte, 32*1024)
|
|
|
|
for {
|
|
n, err := resp.Body.Read(buf)
|
|
totalRead += n
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Sprintf("Read error: %v", err)
|
|
}
|
|
}
|
|
|
|
expectedSize := 1024 * 1024
|
|
if totalRead != expectedSize {
|
|
return nil, fmt.Sprintf("Expected %d bytes, got %d", expectedSize, totalRead)
|
|
}
|
|
|
|
return nil, "ok"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
},
|
|
[]string{
|
|
`{"id": "testpluginlargepayloadserver", "server": {"executable": "backend.exe"}}`,
|
|
`{"id": "testpluginlargepayloadclient", "server": {"executable": "backend.exe"}}`,
|
|
},
|
|
[]string{
|
|
"testpluginlargepayloadserver",
|
|
"testpluginlargepayloadclient",
|
|
},
|
|
true,
|
|
th.App,
|
|
th.Context,
|
|
)
|
|
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testpluginlargepayloadclient")
|
|
require.NoError(t, err)
|
|
_, ret := hooks.MessageWillBePosted(nil, nil)
|
|
assert.Equal(t, "ok", ret)
|
|
})
|
|
|
|
t.Run("incremental delivery", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
setupMultiPluginAPITest(t,
|
|
[]string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v2/incremental":
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
chunks := []string{
|
|
"chunk1|",
|
|
"chunk2|",
|
|
"chunk3|",
|
|
"chunk4|",
|
|
"chunk5|",
|
|
}
|
|
|
|
for i, chunk := range chunks {
|
|
w.Write([]byte(chunk))
|
|
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
|
|
// Delay between chunks (except last)
|
|
if i < len(chunks)-1 {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"net/http"
|
|
"io"
|
|
"fmt"
|
|
"time"
|
|
"strings"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
req, err := http.NewRequest("GET", "/testpluginincrementalserver/api/v2/incremental", nil)
|
|
if err != nil {
|
|
return nil, err.Error()
|
|
}
|
|
|
|
start := time.Now()
|
|
resp := p.API.PluginHTTP(req)
|
|
if resp == nil {
|
|
return nil, "Nil resp"
|
|
}
|
|
|
|
if resp.Body == nil {
|
|
return nil, "Nil body"
|
|
}
|
|
|
|
// Track when chunks arrive
|
|
chunkTimes := []time.Duration{}
|
|
receivedChunks := []string{}
|
|
buf := make([]byte, 7)
|
|
|
|
for {
|
|
n, err := resp.Body.Read(buf)
|
|
if n > 0 {
|
|
chunkTimes = append(chunkTimes, time.Since(start))
|
|
receivedChunks = append(receivedChunks, string(buf[:n]))
|
|
}
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Sprintf("Read error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Verify all chunks received
|
|
expected := "chunk1|chunk2|chunk3|chunk4|chunk5|"
|
|
received := strings.Join(receivedChunks, "")
|
|
if received != expected {
|
|
return nil, fmt.Sprintf("Expected %q, got %q", expected, received)
|
|
}
|
|
|
|
// Verify incremental delivery
|
|
if len(chunkTimes) < 2 {
|
|
return nil, "Not enough chunks for timing verification"
|
|
}
|
|
|
|
// Check that chunks didn't all arrive at once
|
|
timeDiff := chunkTimes[len(chunkTimes)-1] - chunkTimes[0]
|
|
if timeDiff < 200*time.Millisecond {
|
|
return nil, fmt.Sprintf("Chunks arrived too quickly: %v (expected >200ms)", timeDiff)
|
|
}
|
|
|
|
return nil, "ok"
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
},
|
|
[]string{
|
|
`{"id": "testpluginincrementalserver", "server": {"executable": "backend.exe"}}`,
|
|
`{"id": "testpluginincrementalclient", "server": {"executable": "backend.exe"}}`,
|
|
},
|
|
[]string{
|
|
"testpluginincrementalserver",
|
|
"testpluginincrementalclient",
|
|
},
|
|
true,
|
|
th.App,
|
|
th.Context,
|
|
)
|
|
|
|
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testpluginincrementalclient")
|
|
require.NoError(t, err)
|
|
_, ret := hooks.MessageWillBePosted(nil, nil)
|
|
assert.Equal(t, "ok", ret)
|
|
})
|
|
}
|
|
|
|
func TestAPIMetrics(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) 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)
|
|
|
|
// 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() + "[email protected]",
|
|
Nickname: "Darth Vader1",
|
|
Username: "vader" + model.NewId(),
|
|
Password: model.NewTestPassword(),
|
|
AuthService: "",
|
|
}
|
|
_, appErr := th.App.CreateUser(th.Context, 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) {
|
|
mainHelper.Parallel(t)
|
|
require := require.New(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
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(t, 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())
|
|
}
|
|
|
|
func TestPluginHTTPConnHijack(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_http_hijack_plugin", "main.go")
|
|
|
|
pluginCode, err := os.ReadFile(fullPath)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, pluginCode)
|
|
|
|
tearDown, ids, errors := SetAppEnvironmentWithPlugins(t, []string{string(pluginCode)}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.NoError(t, errors[0])
|
|
require.Len(t, ids, 1)
|
|
|
|
pluginID := ids[0]
|
|
require.NotEmpty(t, pluginID)
|
|
|
|
reqURL := fmt.Sprintf("http://localhost:%d/plugins/%s", th.Server.ListenAddr.Port, pluginID)
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
require.NoError(t, err)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "OK", string(body))
|
|
}
|
|
|
|
func makePluginHTTPRequest(t *testing.T, pluginID string, port int, token string) string {
|
|
t.Helper()
|
|
client := &http.Client{}
|
|
reqURL := fmt.Sprintf("http://localhost:%d/plugins/%s", port, pluginID)
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set(model.HeaderAuth, model.HeaderToken+" "+token)
|
|
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
return string(body)
|
|
}
|
|
|
|
func TestPluginMFAEnforcement(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicense("mfa"))
|
|
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
// Simply return the value of Mattermost-User-Id header
|
|
userID := r.Header.Get("Mattermost-User-Id")
|
|
w.Write([]byte(userID))
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
// Create and setup plugin
|
|
tearDown, ids, errs := SetAppEnvironmentWithPlugins(t, []string{pluginCode}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, ids, 1)
|
|
|
|
pluginID := ids[0]
|
|
|
|
// Create user that requires MFA
|
|
user := th.CreateUser(t)
|
|
|
|
// Create session
|
|
session, appErr := th.App.CreateSession(th.Context, &model.Session{
|
|
UserId: user.Id,
|
|
})
|
|
require.Nil(t, appErr)
|
|
|
|
t.Run("MFA not enforced", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
|
|
*cfg.ServiceSettings.EnforceMultifactorAuthentication = false
|
|
})
|
|
|
|
// Should return user ID since MFA is not enforced
|
|
userID := makePluginHTTPRequest(t, pluginID, th.Server.ListenAddr.Port, session.Token)
|
|
assert.Equal(t, user.Id, userID)
|
|
})
|
|
|
|
t.Run("MFA enforced", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
|
|
*cfg.ServiceSettings.EnforceMultifactorAuthentication = true
|
|
})
|
|
|
|
// Should return empty string since MFA is enforced but not active
|
|
userID := makePluginHTTPRequest(t, pluginID, th.Server.ListenAddr.Port, session.Token)
|
|
assert.Empty(t, userID)
|
|
})
|
|
}
|
|
|
|
func TestPluginHTTPUpgradeWebSocket(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_http_upgrade_websocket_plugin", "main.go")
|
|
|
|
pluginCode, err := os.ReadFile(fullPath)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, pluginCode)
|
|
|
|
tearDown, ids, errors := SetAppEnvironmentWithPlugins(t, []string{string(pluginCode)}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.NoError(t, errors[0])
|
|
require.Len(t, ids, 1)
|
|
|
|
pluginID := ids[0]
|
|
require.NotEmpty(t, pluginID)
|
|
|
|
reqURL := fmt.Sprintf("ws://localhost:%d/plugins/%s", th.Server.ListenAddr.Port, pluginID)
|
|
wsc, err := model.NewWebSocketClient(reqURL, "")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, wsc)
|
|
|
|
wsc.Listen()
|
|
defer wsc.Close()
|
|
|
|
resp := <-wsc.ResponseChannel
|
|
require.Equal(t, resp.Status, model.StatusOk)
|
|
|
|
for i := range 10 {
|
|
wsc.SendMessage("custom_action", map[string]any{"value": i})
|
|
var resp *model.WebSocketResponse
|
|
select {
|
|
case resp = <-wsc.ResponseChannel:
|
|
case <-time.After(2 * time.Second):
|
|
}
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, resp.Status, model.StatusOk)
|
|
require.Equal(t, "custom_action", resp.Data["action"])
|
|
require.Equal(t, float64(i), resp.Data["value"])
|
|
}
|
|
}
|
|
|
|
type MockSlashCommandProvider struct {
|
|
Args *model.CommandArgs
|
|
Message string
|
|
}
|
|
|
|
func (*MockSlashCommandProvider) GetTrigger() string {
|
|
return "mock"
|
|
}
|
|
|
|
func (*MockSlashCommandProvider) GetCommand(a *App, T i18n.TranslateFunc) *model.Command {
|
|
return &model.Command{
|
|
Trigger: "mock",
|
|
AutoComplete: true,
|
|
AutoCompleteDesc: "mock",
|
|
AutoCompleteHint: "mock",
|
|
DisplayName: "mock",
|
|
}
|
|
}
|
|
|
|
func (mscp *MockSlashCommandProvider) DoCommand(a *App, rctx request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
|
|
mscp.Args = args
|
|
mscp.Message = message
|
|
return &model.CommandResponse{
|
|
Text: "mock",
|
|
ResponseType: model.CommandResponseTypeEphemeral,
|
|
}
|
|
}
|
|
|
|
func TestPluginExecuteSlashCommand(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
slashCommandMock := &MockSlashCommandProvider{}
|
|
RegisterCommandProvider(slashCommandMock)
|
|
|
|
newUser := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, newUser, th.BasicTeam)
|
|
|
|
t.Run("run invite command", func(t *testing.T) {
|
|
args := &model.CommandArgs{
|
|
Command: "/mock @" + newUser.Username,
|
|
TeamId: th.BasicTeam.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
}
|
|
_, err := api.ExecuteSlashCommand(args)
|
|
require.NoError(t, err)
|
|
require.Equal(t, args, slashCommandMock.Args)
|
|
require.Equal(t, "@"+newUser.Username, slashCommandMock.Message)
|
|
})
|
|
}
|
|
|
|
func TestPluginAPISearchPostsInTeamByUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
basicPostText := &th.BasicPost.Message
|
|
unknownTerm := "Unknown Message"
|
|
|
|
testCases := []struct {
|
|
description string
|
|
teamID string
|
|
userID string
|
|
params model.SearchParameter
|
|
expectedPostsLen int
|
|
}{
|
|
{
|
|
"empty params",
|
|
th.BasicTeam.Id,
|
|
th.BasicUser.Id,
|
|
model.SearchParameter{},
|
|
0,
|
|
},
|
|
{
|
|
"doesn't match any posts",
|
|
th.BasicTeam.Id,
|
|
th.BasicUser.Id,
|
|
model.SearchParameter{Terms: &unknownTerm},
|
|
0,
|
|
},
|
|
{
|
|
"matched posts",
|
|
th.BasicTeam.Id,
|
|
th.BasicUser.Id,
|
|
model.SearchParameter{Terms: basicPostText},
|
|
1,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.description, func(t *testing.T) {
|
|
searchResults, err := api.SearchPostsInTeamForUser(testCase.teamID, testCase.userID, testCase.params)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, testCase.expectedPostsLen, len(searchResults.Posts))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginAPICreateCommandAndListCommands(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
foundCommand := func(listXCommand func(teamID string) ([]*model.Command, error)) bool {
|
|
cmds, appErr := listXCommand(th.BasicTeam.Id)
|
|
require.NoError(t, appErr)
|
|
|
|
for _, cmd := range cmds {
|
|
if cmd.Trigger == "testcmd" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
require.False(t, foundCommand(api.ListCommands))
|
|
|
|
cmd := &model.Command{
|
|
TeamId: th.BasicTeam.Id,
|
|
Trigger: "testcmd",
|
|
Method: "G",
|
|
URL: "http://test.com/testcmd",
|
|
}
|
|
|
|
cmd, appErr := api.CreateCommand(cmd)
|
|
require.NoError(t, appErr)
|
|
|
|
newCmd, appErr := api.GetCommand(cmd.Id)
|
|
require.NoError(t, appErr)
|
|
require.Equal(t, "pluginid", newCmd.PluginId)
|
|
require.Equal(t, "", newCmd.CreatorId)
|
|
require.True(t, foundCommand(api.ListCommands))
|
|
require.True(t, foundCommand(api.ListCustomCommands))
|
|
require.False(t, foundCommand(api.ListPluginCommands))
|
|
}
|
|
|
|
func TestPluginAPIUpdateCommand(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
cmd := &model.Command{
|
|
TeamId: th.BasicTeam.Id,
|
|
Trigger: "testcmd",
|
|
Method: "G",
|
|
URL: "http://test.com/testcmd",
|
|
}
|
|
|
|
cmd, appErr := api.CreateCommand(cmd)
|
|
require.NoError(t, appErr)
|
|
|
|
newCmd, appErr := api.GetCommand(cmd.Id)
|
|
require.NoError(t, appErr)
|
|
require.Equal(t, "pluginid", newCmd.PluginId)
|
|
require.Equal(t, "", newCmd.CreatorId)
|
|
|
|
newCmd.Trigger = "NewTrigger"
|
|
newCmd.PluginId = "CannotChangeMe"
|
|
newCmd2, appErr := api.UpdateCommand(newCmd.Id, newCmd)
|
|
require.NoError(t, appErr)
|
|
require.Equal(t, "pluginid", newCmd2.PluginId)
|
|
require.Equal(t, "newtrigger", newCmd2.Trigger)
|
|
|
|
team1 := th.CreateTeam(t)
|
|
|
|
newCmd2.PluginId = "CannotChangeMe"
|
|
newCmd2.Trigger = "anotherNewTrigger"
|
|
newCmd2.TeamId = team1.Id
|
|
newCmd3, appErr := api.UpdateCommand(newCmd2.Id, newCmd2)
|
|
require.NoError(t, appErr)
|
|
require.Equal(t, "pluginid", newCmd3.PluginId)
|
|
require.Equal(t, "anothernewtrigger", newCmd3.Trigger)
|
|
require.Equal(t, team1.Id, newCmd3.TeamId)
|
|
|
|
newCmd3.Trigger = "anotherNewTriggerAgain"
|
|
newCmd3.TeamId = ""
|
|
newCmd4, appErr := api.UpdateCommand(newCmd2.Id, newCmd2)
|
|
require.NoError(t, appErr)
|
|
require.Equal(t, "anothernewtriggeragain", newCmd4.Trigger)
|
|
require.Equal(t, team1.Id, newCmd4.TeamId)
|
|
|
|
// Updating a command's trigger to one that already exists should fail.
|
|
cmd2 := &model.Command{
|
|
TeamId: team1.Id,
|
|
Trigger: "uniquetrigger",
|
|
Method: "G",
|
|
URL: "http://test.com/uniquetrigger",
|
|
}
|
|
cmd2, appErr = api.CreateCommand(cmd2)
|
|
require.NoError(t, appErr)
|
|
|
|
cmd2.Trigger = "anotherNewTriggerAgain"
|
|
_, appErr = api.UpdateCommand(cmd2.Id, cmd2)
|
|
require.Error(t, appErr)
|
|
var appError *model.AppError
|
|
require.ErrorAs(t, appErr, &appError)
|
|
require.Equal(t, "api.command.duplicate_trigger.app_error", appError.Id)
|
|
}
|
|
|
|
func TestPluginAPIIsEnterpriseReady(t *testing.T) {
|
|
oldValue := model.BuildEnterpriseReady
|
|
defer func() { model.BuildEnterpriseReady = oldValue }()
|
|
|
|
model.BuildEnterpriseReady = "true"
|
|
th := Setup(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
assert.Equal(t, true, api.IsEnterpriseReady())
|
|
}
|
|
|
|
func TestPluginUploadsAPI(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
pluginCode := fmt.Sprintf(`
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"bytes"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type TestPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *TestPlugin) OnActivate() error {
|
|
data := []byte("some content to upload")
|
|
us, err := p.API.CreateUploadSession(&model.UploadSession{
|
|
Id: "%s",
|
|
UserId: "%s",
|
|
ChannelId: "%s",
|
|
Type: model.UploadTypeAttachment,
|
|
FileSize: int64(len(data)),
|
|
Filename: "upload.test",
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create upload session: %%w", err)
|
|
}
|
|
|
|
us2, err := p.API.GetUploadSession(us.Id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get upload session: %%w", err)
|
|
}
|
|
|
|
if us.Id != us2.Id {
|
|
return fmt.Errorf("upload sessions should match")
|
|
}
|
|
|
|
fi, err := p.API.UploadData(us, bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upload data: %%w", err)
|
|
}
|
|
|
|
if fi == nil || fi.Id == "" {
|
|
return fmt.Errorf("fileinfo should be set")
|
|
}
|
|
|
|
fileData, appErr := p.API.GetFile(fi.Id)
|
|
if appErr != nil {
|
|
return fmt.Errorf("failed to get file data: %%w", err)
|
|
}
|
|
|
|
if !bytes.Equal(data, fileData) {
|
|
return fmt.Errorf("file data should match")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&TestPlugin{})
|
|
}
|
|
`, model.NewId(), th.BasicUser.Id, th.BasicChannel.Id)
|
|
|
|
pluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
webappPluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(pluginDir)
|
|
defer os.RemoveAll(webappPluginDir)
|
|
|
|
newPluginAPI := func(manifest *model.Manifest) plugin.API {
|
|
return th.App.NewPluginAPI(th.Context, manifest)
|
|
}
|
|
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil)
|
|
require.NoError(t, err)
|
|
|
|
th.App.ch.SetPluginsEnvironment(env)
|
|
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
|
|
utils.CompileGo(t, pluginCode, backend)
|
|
|
|
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
|
|
require.NoError(t, err)
|
|
manifest, activated, reterr := env.Activate(pluginID)
|
|
require.NoError(t, reterr)
|
|
require.NotNil(t, manifest)
|
|
require.True(t, activated)
|
|
}
|
|
|
|
//go:embed plugin_api_tests/manual.test_configuration_will_be_saved_hook/main.tmpl
|
|
var configurationWillBeSavedHookTemplate string
|
|
|
|
func TestConfigurationWillBeSavedHook(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
getPluginCode := func(hookCode string) string {
|
|
return fmt.Sprintf(configurationWillBeSavedHookTemplate, hookCode)
|
|
}
|
|
|
|
runPlugin := func(t *testing.T, code string) {
|
|
pluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
webappPluginDir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(pluginDir)
|
|
defer os.RemoveAll(webappPluginDir)
|
|
|
|
newPluginAPI := func(manifest *model.Manifest) plugin.API {
|
|
return th.App.NewPluginAPI(th.Context, manifest)
|
|
}
|
|
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil)
|
|
require.NoError(t, err)
|
|
|
|
th.App.ch.SetPluginsEnvironment(env)
|
|
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
|
|
utils.CompileGo(t, code, backend)
|
|
|
|
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
|
|
require.NoError(t, err)
|
|
manifest, activated, reterr := env.Activate(pluginID)
|
|
require.NoError(t, reterr)
|
|
require.NotNil(t, manifest)
|
|
require.True(t, activated)
|
|
}
|
|
|
|
t.Run("error", func(t *testing.T) {
|
|
hookCode := `
|
|
return nil, fmt.Errorf("plugin hook failed")
|
|
`
|
|
|
|
runPlugin(t, getPluginCode(hookCode))
|
|
|
|
cfg := th.App.Config()
|
|
_, _, appErr := th.App.SaveConfig(cfg, false)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "saveConfig: An error occurred running the plugin hook on configuration save., plugin hook failed", appErr.Error())
|
|
|
|
require.Equal(t, cfg, th.App.Config())
|
|
})
|
|
|
|
t.Run("AppError", func(t *testing.T) {
|
|
hookCode := `
|
|
return nil, model.NewAppError("saveConfig", "custom_error", nil, "", 400)
|
|
`
|
|
|
|
runPlugin(t, getPluginCode(hookCode))
|
|
|
|
cfg := th.App.Config()
|
|
_, _, appErr := th.App.SaveConfig(cfg, false)
|
|
require.NotNil(t, appErr)
|
|
require.Equal(t, "custom_error", appErr.Id)
|
|
|
|
require.Equal(t, cfg, th.App.Config())
|
|
})
|
|
|
|
t.Run("no error, no config change", func(t *testing.T) {
|
|
hookCode := `
|
|
return nil, nil
|
|
`
|
|
|
|
runPlugin(t, getPluginCode(hookCode))
|
|
|
|
cfg := th.App.Config()
|
|
_, newCfg, appErr := th.App.SaveConfig(cfg, false)
|
|
require.Nil(t, appErr)
|
|
require.Equal(t, cfg, newCfg)
|
|
})
|
|
|
|
t.Run("config change", func(t *testing.T) {
|
|
hookCode := `
|
|
cfg := newCfg.Clone()
|
|
cfg.PluginSettings.Plugins["custom_plugin"] = map[string]any{
|
|
"custom_key": "custom_val",
|
|
}
|
|
return cfg, nil
|
|
`
|
|
|
|
runPlugin(t, getPluginCode(hookCode))
|
|
|
|
cfg := th.App.Config()
|
|
_, newCfg, appErr := th.App.SaveConfig(cfg, false)
|
|
require.Nil(t, appErr)
|
|
require.NotEqual(t, cfg, newCfg)
|
|
require.Equal(t, map[string]any{
|
|
"custom_key": "custom_val",
|
|
}, newCfg.PluginSettings.Plugins["custom_plugin"])
|
|
})
|
|
}
|
|
|
|
func TestSendPushNotification(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
if testing.Short() {
|
|
t.Skip("skipping TestSendPushNotification test in short mode")
|
|
}
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// 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.FullNotification
|
|
*cfg.EmailSettings.PushNotificationServer = pushServer.URL
|
|
})
|
|
|
|
var wg sync.WaitGroup
|
|
for _, data := range userSessions {
|
|
wg.Add(1)
|
|
go func(user model.User) {
|
|
defer wg.Done()
|
|
post := th.CreatePost(t, th.BasicChannel)
|
|
post.Message = "started a conversation"
|
|
notification := &model.PushNotification{
|
|
Category: model.CategoryCanReply,
|
|
Version: model.PushMessageV2,
|
|
Type: model.PushTypeMessage,
|
|
TeamId: th.BasicChannel.TeamId,
|
|
ChannelId: th.BasicChannel.Id,
|
|
PostId: post.Id,
|
|
RootId: post.RootId,
|
|
SenderId: post.UserId,
|
|
SenderName: "Sender Name",
|
|
PostType: post.Type,
|
|
ChannelType: th.BasicChannel.Type,
|
|
Message: "Custom message",
|
|
}
|
|
appErr := api.SendPushNotification(notification, user.Id)
|
|
require.Nil(t, appErr)
|
|
}(*data.user)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Hack to let the worker goroutines complete.
|
|
time.Sleep(1 * time.Second)
|
|
// Server side verification.
|
|
var numMessages int
|
|
for _, n := range handler.notifications() {
|
|
switch n.Type {
|
|
case model.PushTypeMessage:
|
|
numMessages++
|
|
assert.Equal(t, th.BasicChannel.Id, n.ChannelId)
|
|
assert.Equal(t, "Custom message", n.Message)
|
|
default:
|
|
assert.Fail(t, "should not receive any other push notification types")
|
|
}
|
|
}
|
|
assert.Equal(t, 6, numMessages)
|
|
}
|
|
|
|
func TestPluginServeMetrics(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t, StartMetrics)
|
|
|
|
var prevEnable *bool
|
|
var prevAddress *string
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
prevEnable = cfg.MetricsSettings.Enable
|
|
prevAddress = cfg.MetricsSettings.ListenAddress
|
|
cfg.MetricsSettings.Enable = new(true)
|
|
cfg.MetricsSettings.ListenAddress = new(":30067")
|
|
})
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.MetricsSettings.Enable = prevEnable
|
|
cfg.MetricsSettings.ListenAddress = prevAddress
|
|
})
|
|
|
|
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_serve_metrics_plugin", "main.go")
|
|
|
|
pluginCode, err := os.ReadFile(fullPath)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, pluginCode)
|
|
|
|
tearDown, ids, errors := SetAppEnvironmentWithPlugins(t, []string{string(pluginCode)}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
require.NoError(t, errors[0])
|
|
require.Len(t, ids, 1)
|
|
|
|
pluginID := ids[0]
|
|
require.NotEmpty(t, pluginID)
|
|
|
|
reqURL := fmt.Sprintf("http://localhost%s/plugins/%s/metrics", *th.App.Config().MetricsSettings.ListenAddress, pluginID)
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
require.NoError(t, err)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "METRICS", string(body))
|
|
|
|
reqURL = fmt.Sprintf("http://localhost%s/plugins/%s/metrics/subpath", *th.App.Config().MetricsSettings.ListenAddress, pluginID)
|
|
req, err = http.NewRequest("GET", reqURL, nil)
|
|
require.NoError(t, err)
|
|
|
|
resp, err = client.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err = io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "METRICS SUBPATH", string(body))
|
|
}
|
|
|
|
func TestPluginGetChannelsForTeamForUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
user := th.CreateUser(t)
|
|
|
|
team1 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, user, team1)
|
|
team2 := th.CreateTeam(t)
|
|
th.LinkUserToTeam(t, user, team2)
|
|
|
|
channel1 := th.CreateChannel(t, team1)
|
|
th.AddUserToChannel(t, user, channel1)
|
|
channel2 := th.CreateChannel(t, team2)
|
|
th.AddUserToChannel(t, user, channel2)
|
|
|
|
dmChannel := th.CreateDmChannel(t, user)
|
|
|
|
pluginCode := `
|
|
package main
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
userID = "` + user.Id + `"
|
|
teamID1 = "` + team1.Id + `"
|
|
teamID2 = "` + team2.Id + `"
|
|
channelID1 = "` + channel1.Id + `"
|
|
channelID2 = "` + channel2.Id + `"
|
|
dmChannelID = "` + dmChannel.Id + `"
|
|
)
|
|
|
|
type TestPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func checkForChannels(channels []*model.Channel, expectedLength int, channel1Expected, channel2Expected, dmChannelExpected bool) string {
|
|
if len(channels) != expectedLength {
|
|
return "Returned the wrong number of channels"
|
|
}
|
|
|
|
var channel1Found, channel2Found, dmChannelFound bool
|
|
for _, channel := range channels {
|
|
if channel.Id == channelID1 {
|
|
channel1Found = true
|
|
} else if channel.Id == channelID2 {
|
|
channel2Found = true
|
|
} else if channel.Id == dmChannelID {
|
|
dmChannelFound = true
|
|
}
|
|
}
|
|
|
|
if channel1Found && !channel1Expected {
|
|
return "Channel 1 found"
|
|
} else if !channel1Found && channel1Expected {
|
|
return "Channel 1 not found"
|
|
} else if channel2Found && !channel2Expected {
|
|
return "Channel 2 found"
|
|
} else if !channel2Found && channel2Expected {
|
|
return "Channel 2 not found"
|
|
} else if dmChannelFound && !dmChannelExpected {
|
|
return "DM Channel found"
|
|
} else if !dmChannelFound && dmChannelExpected {
|
|
return "DM Channel not found"
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (p *TestPlugin) OnActivate() error {
|
|
if channels, appErr := p.API.GetChannelsForTeamForUser(teamID1, userID, true); appErr != nil {
|
|
return appErr
|
|
} else if msg := checkForChannels(channels, 4, true, false, true); msg != "" {
|
|
return errors.New(msg + " when called with team ID 1")
|
|
}
|
|
|
|
if channels, appErr := p.API.GetChannelsForTeamForUser(teamID2, userID, true); appErr != nil {
|
|
return appErr
|
|
} else if msg := checkForChannels(channels, 4, false, true, true); msg != "" {
|
|
return errors.New(msg + " when called with team ID 2")
|
|
}
|
|
|
|
if channels, appErr := p.API.GetChannelsForTeamForUser("", userID, true); appErr != nil {
|
|
return appErr
|
|
} else if msg := checkForChannels(channels, 7, true, true, true); msg != "" {
|
|
return errors.New(msg + " when called with empty team ID")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&TestPlugin{})
|
|
}`
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
|
}
|
|
|
|
func TestPluginPatchChannelMembersNotifications(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should be able to set fields for multiple members", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, th.BasicUser, channel)
|
|
th.AddUserToChannel(t, th.BasicUser2, channel)
|
|
|
|
member1, err := th.App.GetChannelMember(th.Context, channel.Id, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "", member1.NotifyProps["test_field"])
|
|
require.Equal(t, model.IgnoreChannelMentionsDefault, member1.NotifyProps[model.IgnoreChannelMentionsNotifyProp])
|
|
member2, err := th.App.GetChannelMember(th.Context, channel.Id, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "", member2.NotifyProps["test_field"])
|
|
require.Equal(t, model.IgnoreChannelMentionsDefault, member2.NotifyProps[model.IgnoreChannelMentionsNotifyProp])
|
|
|
|
pluginCode := `
|
|
package main
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
const (
|
|
channelID = "` + channel.Id + `"
|
|
userID1 = "` + th.BasicUser.Id + `"
|
|
userID2 = "` + th.BasicUser2.Id + `"
|
|
)
|
|
|
|
type TestPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *TestPlugin) OnActivate() error {
|
|
return p.API.PatchChannelMembersNotifications(
|
|
[]*model.ChannelMemberIdentifier{
|
|
{ChannelId: channelID, UserId: userID1},
|
|
{ChannelId: channelID, UserId: userID2},
|
|
},
|
|
map[string]string{
|
|
"test_field": "test_value",
|
|
model.IgnoreChannelMentionsNotifyProp: model.IgnoreChannelMentionsOn,
|
|
},
|
|
)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&TestPlugin{})
|
|
}`
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
updated1, err := th.App.GetChannelMember(th.Context, member1.ChannelId, member1.UserId)
|
|
require.Nil(t, err)
|
|
updated2, err := th.App.GetChannelMember(th.Context, member2.ChannelId, member2.UserId)
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, member1.NotifyProps[model.MarkUnreadNotifyProp], updated1.NotifyProps[model.MarkUnreadNotifyProp])
|
|
assert.Equal(t, "test_value", updated1.NotifyProps["test_field"])
|
|
assert.Equal(t, model.IgnoreChannelMentionsOn, updated1.NotifyProps[model.IgnoreChannelMentionsNotifyProp])
|
|
assert.Equal(t, member2.NotifyProps[model.MarkUnreadNotifyProp], updated2.NotifyProps[model.MarkUnreadNotifyProp])
|
|
assert.Equal(t, "test_value", updated2.NotifyProps["test_field"])
|
|
assert.Equal(t, model.IgnoreChannelMentionsOn, updated2.NotifyProps[model.IgnoreChannelMentionsNotifyProp])
|
|
})
|
|
|
|
t.Run("should be able to clear a field", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
channel := th.CreateChannel(t, th.BasicTeam)
|
|
th.AddUserToChannel(t, th.BasicUser, channel)
|
|
|
|
member, err := th.App.GetChannelMember(th.Context, channel.Id, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
|
|
member.NotifyProps["test_field"] = "test_value"
|
|
_, err = th.App.updateChannelMember(th.Context, member)
|
|
require.Nil(t, err)
|
|
|
|
member, err = th.App.GetChannelMember(th.Context, channel.Id, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "test_value", member.NotifyProps["test_field"])
|
|
|
|
pluginCode := `
|
|
package main
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
const (
|
|
channelID = "` + channel.Id + `"
|
|
userID = "` + th.BasicUser.Id + `"
|
|
)
|
|
|
|
type TestPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *TestPlugin) OnActivate() error {
|
|
return p.API.PatchChannelMembersNotifications(
|
|
[]*model.ChannelMemberIdentifier{
|
|
{ChannelId: channelID, UserId: userID},
|
|
},
|
|
map[string]string{
|
|
"test_field": "",
|
|
},
|
|
)
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&TestPlugin{})
|
|
}`
|
|
pluginID := "testplugin"
|
|
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
|
|
|
setupPluginAPITest(t, pluginCode, pluginManifest, pluginID, th.App, th.Context)
|
|
|
|
updated, err := th.App.GetChannelMember(th.Context, member.ChannelId, member.UserId)
|
|
require.Nil(t, err)
|
|
|
|
assert.Equal(t, "", updated.NotifyProps["test_field"])
|
|
})
|
|
}
|
|
|
|
func TestPluginServeHTTPCompatibility(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
pluginCode := `
|
|
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("plugin response"))
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
for goVersion := range strings.FieldsSeq(os.Getenv("GO_COMPATIBILITY_TEST_VERSIONS")) {
|
|
t.Run(goVersion, func(t *testing.T) {
|
|
tearDown, ids, errs := SetAppEnvironmentWithPluginsGoVersion(t, []string{pluginCode}, th.App, th.NewPluginAPI, goVersion)
|
|
defer tearDown()
|
|
require.NoError(t, errs[0])
|
|
require.Len(t, ids, 1)
|
|
pluginID := ids[0]
|
|
|
|
res := makePluginHTTPRequest(t, pluginID, th.Server.ListenAddr.Port, "")
|
|
assert.Equal(t, "plugin response", res)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginAPICreatePropertyField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("should allow creation after deleting fields", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// Register a property group first so the version check can look it up
|
|
group, err := api.RegisterPropertyGroup("testgroup" + model.NewId())
|
|
require.NoError(t, err)
|
|
|
|
// Create 20 property fields
|
|
var createdFields []*model.PropertyField
|
|
for i := 1; i <= 20; i++ {
|
|
field := &model.PropertyField{
|
|
GroupID: group.ID,
|
|
Name: fmt.Sprintf("field_%d", i),
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
var created *model.PropertyField
|
|
created, err = api.CreatePropertyField(field)
|
|
require.NoError(t, err)
|
|
createdFields = append(createdFields, created)
|
|
}
|
|
|
|
// Delete one field
|
|
err = api.DeletePropertyField(group.ID, createdFields[0].ID)
|
|
require.NoError(t, err)
|
|
|
|
// Should now be able to create another field
|
|
newField := &model.PropertyField{
|
|
GroupID: group.ID,
|
|
Name: "new_field",
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
created, err := api.CreatePropertyField(newField)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, newField.Name, created.Name)
|
|
})
|
|
|
|
t.Run("should not count deleted fields", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// Register a property group first so the version check can look it up
|
|
group, err := api.RegisterPropertyGroup("testgroup" + model.NewId())
|
|
require.NoError(t, err)
|
|
|
|
// Create and delete 5 fields
|
|
for i := 1; i <= 5; i++ {
|
|
field := &model.PropertyField{
|
|
GroupID: group.ID,
|
|
Name: fmt.Sprintf("deleted_field_%d", i),
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
created, err := api.CreatePropertyField(field)
|
|
require.NoError(t, err)
|
|
|
|
err = api.DeletePropertyField(group.ID, created.ID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Should be able to create multiple active fields
|
|
for i := 1; i <= 20; i++ {
|
|
field := &model.PropertyField{
|
|
GroupID: group.ID,
|
|
Name: fmt.Sprintf("active_field_%d", i),
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
created, err := api.CreatePropertyField(field)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, field.Name, created.Name)
|
|
}
|
|
})
|
|
|
|
t.Run("should reject empty or invalid group ID", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// Test with empty group ID - should fail validation
|
|
field := &model.PropertyField{
|
|
GroupID: "",
|
|
Name: "test_field",
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
created, err := api.CreatePropertyField(field)
|
|
require.Error(t, err) // Should fail due to invalid GroupID
|
|
assert.Nil(t, created)
|
|
|
|
// Test with nil field - should fail gracefully
|
|
created, err = api.CreatePropertyField(nil)
|
|
require.Error(t, err) // Should fail when given nil input
|
|
assert.Nil(t, created)
|
|
assert.Contains(t, err.Error(), "property field is required")
|
|
})
|
|
}
|
|
|
|
func TestPluginAPICountPropertyFields(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("should count active property fields only", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
group, groupErr := api.RegisterPropertyGroup(model.NewId())
|
|
require.NoError(t, groupErr)
|
|
|
|
// Create 5 fields
|
|
var createdFields []*model.PropertyField
|
|
for i := 1; i <= 5; i++ {
|
|
field := &model.PropertyField{
|
|
GroupID: group.ID,
|
|
Name: fmt.Sprintf("field_%d", i),
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
created, err := api.CreatePropertyField(field)
|
|
require.NoError(t, err)
|
|
createdFields = append(createdFields, created)
|
|
}
|
|
|
|
// Count active fields
|
|
count, err := api.CountPropertyFields(group.ID, false)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(5), count)
|
|
|
|
// Delete 2 fields
|
|
err = api.DeletePropertyField(group.ID, createdFields[0].ID)
|
|
require.NoError(t, err)
|
|
err = api.DeletePropertyField(group.ID, createdFields[1].ID)
|
|
require.NoError(t, err)
|
|
|
|
// Count should now be 3
|
|
count, err = api.CountPropertyFields(group.ID, false)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count)
|
|
})
|
|
|
|
t.Run("should count all property fields including deleted", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
group, groupErr := api.RegisterPropertyGroup(model.NewId())
|
|
require.NoError(t, groupErr)
|
|
|
|
// Create 5 fields
|
|
var createdFields []*model.PropertyField
|
|
for i := 1; i <= 5; i++ {
|
|
field := &model.PropertyField{
|
|
GroupID: group.ID,
|
|
Name: fmt.Sprintf("field_%d", i),
|
|
Type: model.PropertyFieldTypeText,
|
|
CreateAt: model.GetMillis(),
|
|
UpdateAt: model.GetMillis(),
|
|
}
|
|
|
|
created, err := api.CreatePropertyField(field)
|
|
require.NoError(t, err)
|
|
createdFields = append(createdFields, created)
|
|
}
|
|
|
|
// Count all fields
|
|
count, err := api.CountPropertyFields(group.ID, true)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(5), count)
|
|
|
|
// Delete 2 fields
|
|
err = api.DeletePropertyField(group.ID, createdFields[0].ID)
|
|
require.NoError(t, err)
|
|
err = api.DeletePropertyField(group.ID, createdFields[1].ID)
|
|
require.NoError(t, err)
|
|
|
|
// Count all should still be 5
|
|
count, err = api.CountPropertyFields(group.ID, true)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(5), count)
|
|
|
|
// Count active should be 3
|
|
count, err = api.CountPropertyFields(group.ID, false)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count)
|
|
})
|
|
|
|
t.Run("should return 0 for empty group", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
count, err := api.CountPropertyFields("non-existent-group", false)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
|
|
count, err = api.CountPropertyFields("non-existent-group", true)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
})
|
|
}
|
|
|
|
func TestPluginAPICreateTeamAnonymousURLs(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t)
|
|
api := th.SetupPluginAPI()
|
|
|
|
t.Run("should override team name when UseAnonymousURLs is enabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "original-team-name"
|
|
team := &model.Team{
|
|
DisplayName: "Anonymous URL Team",
|
|
Name: originalName,
|
|
Type: model.TeamOpen,
|
|
}
|
|
|
|
createdTeam, appErr := api.CreateTeam(team)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdTeam)
|
|
|
|
assert.NotEqual(t, originalName, createdTeam.Name, "team name should be overridden by server")
|
|
assert.True(t, model.IsValidId(createdTeam.Name), "team name should be a valid server-generated ID")
|
|
assert.Equal(t, "Anonymous URL Team", createdTeam.DisplayName, "display name should remain unchanged")
|
|
})
|
|
|
|
t.Run("should preserve team name when UseAnonymousURLs is disabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "preserved-team-name"
|
|
team := &model.Team{
|
|
DisplayName: "Normal Team",
|
|
Name: originalName,
|
|
Type: model.TeamOpen,
|
|
}
|
|
|
|
createdTeam, appErr := api.CreateTeam(team)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdTeam)
|
|
|
|
assert.Equal(t, originalName, createdTeam.Name, "team name should not be overridden")
|
|
})
|
|
|
|
t.Run("should not override team name without Enterprise Advanced license", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "original-team-name"
|
|
team := &model.Team{
|
|
DisplayName: "Enterprise Team",
|
|
Name: originalName,
|
|
Type: model.TeamOpen,
|
|
}
|
|
|
|
createdTeam, appErr := api.CreateTeam(team)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdTeam)
|
|
|
|
assert.Equal(t, originalName, createdTeam.Name, "team name should not be overridden")
|
|
})
|
|
}
|
|
|
|
func TestPluginAPICreateChannelManagedCategory(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
t.Cleanup(func() {
|
|
th.ConfigStore.SetReadOnlyFF(true)
|
|
})
|
|
api := th.SetupPluginAPI()
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.ManagedChannelCategories = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.ManagedChannelCategories = false })
|
|
|
|
categoryName := "Operations"
|
|
channel := &model.Channel{
|
|
DisplayName: "Plugin channel with managed category",
|
|
Name: "plugin-managed-cat-" + model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
ManagedCategoryName: categoryName,
|
|
}
|
|
|
|
createdChannel, appErr := api.CreateChannel(channel)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdChannel)
|
|
defer func() {
|
|
_ = th.App.ClearChannelManagedCategory(th.Context, createdChannel.Id)
|
|
}()
|
|
|
|
th.AddUserToChannel(t, th.BasicUser, createdChannel)
|
|
|
|
session := &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}}
|
|
rctx := th.Context.WithSession(session)
|
|
mappings, err := th.App.GetVisibleManagedCategoryMappings(rctx, th.BasicTeam.Id)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, categoryName, mappings[createdChannel.Id])
|
|
}
|
|
|
|
func TestPluginAPICreateChannelAnonymousURLs(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t).InitBasic(t)
|
|
api := th.SetupPluginAPI()
|
|
|
|
t.Run("should override open channel name when UseAnonymousURLs is enabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "original-channel-name"
|
|
channel := &model.Channel{
|
|
DisplayName: "Anonymous URL Channel",
|
|
Name: originalName,
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
}
|
|
|
|
createdChannel, appErr := api.CreateChannel(channel)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdChannel)
|
|
|
|
assert.NotEqual(t, originalName, createdChannel.Name, "open channel name should be overridden")
|
|
assert.True(t, model.IsValidId(createdChannel.Name), "channel name should be a valid server-generated ID")
|
|
assert.Equal(t, "Anonymous URL Channel", createdChannel.DisplayName, "display name should remain unchanged")
|
|
})
|
|
|
|
t.Run("should override private channel name when UseAnonymousURLs is enabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "private-channel-name"
|
|
channel := &model.Channel{
|
|
DisplayName: "Anonymous Private Channel",
|
|
Name: originalName,
|
|
Type: model.ChannelTypePrivate,
|
|
TeamId: th.BasicTeam.Id,
|
|
}
|
|
|
|
createdChannel, appErr := api.CreateChannel(channel)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdChannel)
|
|
|
|
assert.NotEqual(t, originalName, createdChannel.Name, "private channel name should be overridden")
|
|
assert.True(t, model.IsValidId(createdChannel.Name), "channel name should be a valid server-generated ID")
|
|
})
|
|
|
|
t.Run("should preserve channel name when UseAnonymousURLs is disabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "preserved-channel"
|
|
channel := &model.Channel{
|
|
DisplayName: "Normal Channel",
|
|
Name: originalName,
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
}
|
|
|
|
createdChannel, appErr := api.CreateChannel(channel)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdChannel)
|
|
|
|
assert.Equal(t, originalName, createdChannel.Name, "channel name should not be overridden")
|
|
})
|
|
|
|
t.Run("should not override channel name without Enterprise Advanced license", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.UseAnonymousURLs = false })
|
|
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
defer func() {
|
|
appErr := th.App.Srv().RemoveLicense()
|
|
require.Nil(t, appErr)
|
|
}()
|
|
|
|
originalName := "original-channel-name"
|
|
channel := &model.Channel{
|
|
DisplayName: "Normal Channel",
|
|
Name: originalName,
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
}
|
|
|
|
createdChannel, appErr := api.CreateChannel(channel)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdChannel)
|
|
|
|
assert.Equal(t, originalName, createdChannel.Name, "channel name should not be overridden")
|
|
})
|
|
}
|
|
|
|
func TestPluginAPIPropertyGroupDeprecatedName(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("RegisterPropertyGroup rejects deprecated name", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// Register using the deprecated name must fail
|
|
_, err := api.RegisterPropertyGroup(model.DeprecatedCPAPropertyGroupName)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "deprecated")
|
|
|
|
// Register using the canonical name should still work
|
|
group, err := api.RegisterPropertyGroup(model.AccessControlPropertyGroupName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, group)
|
|
assert.Equal(t, model.AccessControlPropertyGroupName, group.Name)
|
|
})
|
|
|
|
t.Run("GetPropertyGroup rejects deprecated name", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// Looking up by the deprecated name must fail with an actionable error
|
|
_, err := api.GetPropertyGroup(model.DeprecatedCPAPropertyGroupName)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "deprecated")
|
|
|
|
// Looking up by the canonical name should still work
|
|
canonical, err := api.GetPropertyGroup(model.AccessControlPropertyGroupName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, canonical)
|
|
assert.Equal(t, model.AccessControlPropertyGroupName, canonical.Name)
|
|
})
|
|
|
|
t.Run("other group names are not affected by the mapping", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
// Register a different group — no mapping should occur
|
|
group, err := api.RegisterPropertyGroup("my_plugin_group")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, group)
|
|
assert.Equal(t, "my_plugin_group", group.Name)
|
|
|
|
// Look it up
|
|
fetched, err := api.GetPropertyGroup("my_plugin_group")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, group.ID, fetched.ID)
|
|
})
|
|
|
|
t.Run("GetPropertyGroup with nonexistent name returns error", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
api := th.SetupPluginAPI()
|
|
|
|
_, err := api.GetPropertyGroup("no_such_group")
|
|
require.Error(t, err)
|
|
})
|
|
}
|