2019-11-29 06:59:40 -05:00
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
2018-07-16 15:49:26 -04:00
package app
import (
2018-11-15 15:23:03 -05:00
"bytes"
2023-06-12 20:23:02 -04:00
_ "embed"
2018-07-18 19:35:12 -04:00
"encoding/json"
2020-01-21 03:41:28 -05:00
"errors"
2018-11-08 13:17:07 -05:00
"fmt"
2018-11-15 15:23:03 -05:00
"image"
"image/color"
"image/png"
2022-08-09 07:25:46 -04:00
"io"
2019-11-29 06:41:17 -05:00
"net/http"
"net/http/httptest"
2018-07-18 19:35:12 -04:00
"os"
"path/filepath"
2018-11-29 13:02:06 -05:00
"strings"
2023-08-18 13:05:26 -04:00
"sync"
2018-07-16 15:49:26 -04:00
"testing"
2018-12-13 04:46:42 -05:00
"time"
2018-07-16 15:49:26 -04:00
2021-01-07 12:12:43 -05:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
2023-06-11 01:24:35 -04:00
"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"
2023-09-05 03:47:30 -04:00
"github.com/mattermost/mattermost/server/public/shared/request"
2024-05-15 11:05:13 -04:00
"github.com/mattermost/mattermost/server/v8"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
2018-07-16 15:49:26 -04:00
)
2020-01-21 03:41:28 -05:00
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 )
}
2021-02-25 14:22:27 -05:00
func setDefaultPluginConfig ( th * TestHelper , pluginID string ) {
2020-01-21 03:41:28 -05:00
th . App . UpdateConfig ( func ( cfg * model . Config ) {
2022-07-05 02:46:50 -04:00
cfg . PluginSettings . Plugins [ pluginID ] = map [ string ] any {
2020-01-21 03:41:28 -05:00
"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 ,
}
} )
}
2025-09-10 09:11:32 -04:00
func setupMultiPluginAPITest ( t * testing . T , pluginCodes [ ] string , pluginManifests [ ] string , pluginIDs [ ] string , asMain bool , app * App , rctx request . CTX ) string {
2022-08-09 07:25:46 -04:00
pluginDir , err := os . MkdirTemp ( "" , "" )
2018-07-18 19:35:12 -04:00
require . NoError ( t , err )
2020-09-22 04:57:08 -04:00
t . Cleanup ( func ( ) {
err = os . RemoveAll ( pluginDir )
if err != nil {
t . Logf ( "Failed to cleanup pluginDir %s" , err . Error ( ) )
}
} )
2022-08-09 07:25:46 -04:00
webappPluginDir , err := os . MkdirTemp ( "" , "" )
2018-07-18 19:35:12 -04:00
require . NoError ( t , err )
2020-09-22 04:57:08 -04:00
t . Cleanup ( func ( ) {
err = os . RemoveAll ( webappPluginDir )
if err != nil {
t . Logf ( "Failed to cleanup webappPluginDir %s" , err . Error ( ) )
}
} )
2018-07-18 19:35:12 -04:00
2021-05-11 06:00:44 -04:00
newPluginAPI := func ( manifest * model . Manifest ) plugin . API {
2025-09-10 09:11:32 -04:00
return app . NewPluginAPI ( rctx , manifest )
2021-05-11 06:00:44 -04:00
}
2023-05-03 15:04:10 -04:00
env , err := plugin . NewEnvironment ( newPluginAPI , NewDriverImpl ( app . Srv ( ) ) , pluginDir , webappPluginDir , app . Log ( ) , nil )
2018-07-18 19:35:12 -04:00
require . NoError ( t , err )
2021-02-25 14:22:27 -05:00
require . Equal ( t , len ( pluginCodes ) , len ( pluginIDs ) )
require . Equal ( t , len ( pluginManifests ) , len ( pluginIDs ) )
2019-11-04 20:35:58 -05:00
2021-02-25 14:22:27 -05:00
for i , pluginID := range pluginIDs {
backend := filepath . Join ( pluginDir , pluginID , "backend.exe" )
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-16 23:23:52 -04:00
if asMain {
utils . CompileGo ( t , pluginCodes [ i ] , backend )
} else {
utils . CompileGoTest ( t , pluginCodes [ i ] , backend )
}
2018-07-18 19:35:12 -04:00
2024-11-12 15:56:35 -05:00
err := os . WriteFile ( filepath . Join ( pluginDir , pluginID , "plugin.json" ) , [ ] byte ( pluginManifests [ i ] ) , 0600 )
require . NoError ( t , err )
2021-02-25 14:22:27 -05:00
manifest , activated , reterr := env . Activate ( pluginID )
2021-02-16 06:00:01 -05:00
require . NoError ( t , reterr )
2019-11-04 20:35:58 -05:00
require . NotNil ( t , manifest )
require . True ( t , activated )
2020-09-22 04:57:08 -04:00
app . UpdateConfig ( func ( cfg * model . Config ) {
2021-02-25 14:22:27 -05:00
cfg . PluginSettings . PluginStates [ pluginID ] = & model . PluginState {
2020-09-22 04:57:08 -04:00
Enable : true ,
}
} )
2019-11-04 20:35:58 -05:00
}
2018-07-18 19:35:12 -04:00
2022-12-21 14:10:26 -05:00
app . ch . SetPluginsEnvironment ( env )
2019-04-05 10:35:51 -04:00
return pluginDir
2018-07-18 19:35:12 -04:00
}
2025-09-10 09:11:32 -04:00
func setupPluginAPITest ( t * testing . T , pluginCode string , pluginManifest string , pluginID string , app * App , rctx request . CTX ) string {
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-16 23:23:52 -04:00
asMain := pluginID != "test_db_driver"
2021-08-16 13:46:44 -04:00
return setupMultiPluginAPITest ( t ,
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-16 23:23:52 -04:00
[ ] string { pluginCode } , [ ] string { pluginManifest } , [ ] string { pluginID } ,
2025-09-10 09:11:32 -04:00
asMain , app , rctx )
2019-11-04 20:35:58 -05:00
}
2019-04-05 10:35:51 -04:00
func TestPublicFilesPathConfiguration ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-02-10 13:31:41 -05:00
th := Setup ( t )
2019-04-05 10:35:51 -04:00
pluginID := "com.mattermost.sample"
2021-08-16 13:46:44 -04:00
pluginDir := setupPluginAPITest ( t ,
2019-04-05 10:35:51 -04:00
`
package main
import (
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/plugin"
2019-04-05 10:35:51 -04:00
)
type MyPlugin struct {
plugin . MattermostPlugin
}
func main ( ) {
plugin . ClientMain ( & MyPlugin { } )
}
` ,
2021-05-11 06:00:44 -04:00
` { "id": "com.mattermost.sample", "server": { "executable": "backend.exe"}, "settings_schema": { "settings": []}} ` , pluginID , th . App , th . Context )
2019-04-05 10:35:51 -04:00
publicFilesFolderInTest := filepath . Join ( pluginDir , pluginID , "public" )
publicFilesPath , err := th . App . GetPluginsEnvironment ( ) . PublicFilesPath ( pluginID )
assert . NoError ( t , err )
assert . Equal ( t , publicFilesPath , publicFilesFolderInTest )
}
2020-05-28 13:15:47 -04:00
2024-01-04 13:50:19 -05:00
func TestPluginAPIGetUserPreference ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2024-02-12 06:12:05 -05:00
t . Run ( "should return preferences when called" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2024-02-12 06:12:05 -05:00
api := th . SetupPluginAPI ( )
2024-01-04 13:50:19 -05:00
2024-02-12 06:12:05 -05:00
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 )
2024-01-04 13:50:19 -05:00
2024-02-12 06:12:05 -05:00
preference , err := api . GetPreferenceForUser ( th . BasicUser . Id , model . PreferenceCategoryDisplaySettings , model . PreferenceNameUseMilitaryTime )
2024-01-04 13:50:19 -05:00
2024-02-12 06:12:05 -05:00
require . Nil ( t , err )
assert . Equal ( t , model . PreferenceCategoryDisplaySettings , preference . Category )
assert . Equal ( t , model . PreferenceNameUseMilitaryTime , preference . Name )
assert . Equal ( t , "true" , preference . Value )
2024-01-04 13:50:19 -05:00
2024-02-12 06:12:05 -05:00
preference , err = api . GetPreferenceForUser ( th . BasicUser . Id , "test_category" , "test_key" )
2024-01-04 13:50:19 -05:00
2024-02-12 06:12:05 -05:00
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 ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2024-02-12 06:12:05 -05:00
api := th . SetupPluginAPI ( )
_ , err := api . GetPreferenceForUser ( th . BasicUser . Id , "something" , "that doesn't exist" )
assert . NotNil ( t , err )
} )
2024-01-04 13:50:19 -05:00
}
2020-05-28 13:15:47 -04:00
func TestPluginAPIGetUserPreferences ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-05-28 13:15:47 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2020-05-28 13:15:47 -04:00
api := th . SetupPluginAPI ( )
2021-05-11 06:00:44 -04:00
user1 , err := th . App . CreateUser ( th . Context , & model . User {
2020-05-28 13:15:47 -04:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user1" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2020-05-28 13:15:47 -04:00
preferences , err := api . GetPreferencesForUser ( user1 . Id )
require . Nil ( t , err )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , 3 , len ( preferences ) )
2020-05-28 13:15:47 -04:00
assert . Equal ( t , user1 . Id , preferences [ 0 ] . UserId )
2023-07-25 03:04:38 -04:00
assert . Equal ( t , model . PreferenceRecommendedNextSteps , preferences [ 0 ] . Category )
assert . Equal ( t , "hide" , preferences [ 0 ] . Name )
assert . Equal ( t , "false" , preferences [ 0 ] . Value )
2021-07-23 12:54:51 -04:00
2023-09-19 09:29:57 -04:00
assert . Equal ( t , model . PreferenceCategorySystemNotice , preferences [ 1 ] . Category )
assert . Equal ( t , user1 . Id , preferences [ 2 ] . UserId )
assert . Equal ( t , model . PreferenceCategoryTutorialSteps , preferences [ 2 ] . Category )
assert . Equal ( t , user1 . Id , preferences [ 2 ] . Name )
assert . Equal ( t , "0" , preferences [ 2 ] . Value )
2020-05-28 13:15:47 -04:00
}
func TestPluginAPIDeleteUserPreferences ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-05-28 13:15:47 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2020-05-28 13:15:47 -04:00
api := th . SetupPluginAPI ( )
2021-05-11 06:00:44 -04:00
user1 , err := th . App . CreateUser ( th . Context , & model . User {
2020-05-28 13:15:47 -04:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user1" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2020-05-28 13:15:47 -04:00
preferences , err := api . GetPreferencesForUser ( user1 . Id )
require . Nil ( t , err )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , 3 , len ( preferences ) )
2020-05-28 13:15:47 -04:00
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 ) )
2021-05-11 06:00:44 -04:00
user2 , err := th . App . CreateUser ( th . Context , & model . User {
2020-05-28 13:15:47 -04:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user2" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user2 )
require . Nil ( t , appErr )
} ( )
2020-05-28 13:15:47 -04:00
preference := model . Preference {
Name : user2 . Id ,
UserId : user2 . Id ,
2021-07-12 14:05:36 -04:00
Category : model . PreferenceCategoryTheme ,
2020-05-28 13:15:47 -04:00
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 )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , 4 , len ( preferences ) )
2020-05-28 13:15:47 -04:00
err = api . DeletePreferencesForUser ( user2 . Id , [ ] model . Preference { preference } )
require . Nil ( t , err )
preferences , err = api . GetPreferencesForUser ( user2 . Id )
require . Nil ( t , err )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , 3 , len ( preferences ) )
2023-09-04 04:41:05 -04:00
assert . ElementsMatch ( t ,
2023-09-19 09:29:57 -04:00
[ ] string { model . PreferenceRecommendedNextSteps , model . PreferenceCategoryTutorialSteps , model . PreferenceCategorySystemNotice } ,
[ ] string { preferences [ 0 ] . Category , preferences [ 1 ] . Category , preferences [ 2 ] . Category } ,
2023-09-04 04:41:05 -04:00
)
2020-05-28 13:15:47 -04:00
}
func TestPluginAPIUpdateUserPreferences ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-05-28 13:15:47 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2020-05-28 13:15:47 -04:00
api := th . SetupPluginAPI ( )
2021-05-11 06:00:44 -04:00
user1 , err := th . App . CreateUser ( th . Context , & model . User {
2020-05-28 13:15:47 -04:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user1" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2020-05-28 13:15:47 -04:00
preferences , err := api . GetPreferencesForUser ( user1 . Id )
require . Nil ( t , err )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , 3 , len ( preferences ) )
2021-07-23 12:54:51 -04:00
2020-05-28 13:15:47 -04:00
assert . Equal ( t , user1 . Id , preferences [ 0 ] . UserId )
2023-07-25 03:04:38 -04:00
assert . Equal ( t , model . PreferenceRecommendedNextSteps , preferences [ 0 ] . Category )
assert . Equal ( t , "hide" , preferences [ 0 ] . Name )
assert . Equal ( t , "false" , preferences [ 0 ] . Value )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , model . PreferenceCategorySystemNotice , preferences [ 1 ] . Category )
assert . Equal ( t , user1 . Id , preferences [ 2 ] . UserId )
assert . Equal ( t , model . PreferenceCategoryTutorialSteps , preferences [ 2 ] . Category )
assert . Equal ( t , user1 . Id , preferences [ 2 ] . Name )
assert . Equal ( t , "0" , preferences [ 2 ] . Value )
2020-05-28 13:15:47 -04:00
preference := model . Preference {
Name : user1 . Id ,
UserId : user1 . Id ,
2021-07-12 14:05:36 -04:00
Category : model . PreferenceCategoryTheme ,
2020-05-28 13:15:47 -04:00
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 )
2023-09-19 09:29:57 -04:00
assert . Equal ( t , 4 , len ( preferences ) )
expectedCategories := [ ] string { model . PreferenceCategoryTutorialSteps , model . PreferenceCategoryTheme , model . PreferenceRecommendedNextSteps , model . PreferenceCategorySystemNotice }
2020-05-28 13:15:47 -04:00
for _ , pref := range preferences {
assert . Contains ( t , expectedCategories , pref . Category )
assert . Equal ( t , user1 . Id , pref . UserId )
}
}
2019-01-30 12:55:02 -05:00
func TestPluginAPIGetUsers ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . DeleteBots ( t )
2019-01-30 12:55:02 -05:00
api := th . SetupPluginAPI ( )
2021-05-11 06:00:44 -04:00
user1 , err := th . App . CreateUser ( th . Context , & model . User {
2019-01-30 12:55:02 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user1" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2019-01-30 12:55:02 -05:00
2021-05-11 06:00:44 -04:00
user2 , err := th . App . CreateUser ( th . Context , & model . User {
2019-01-30 12:55:02 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user2" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user2 )
require . Nil ( t , appErr )
} ( )
2019-01-30 12:55:02 -05:00
2021-05-11 06:00:44 -04:00
user3 , err := th . App . CreateUser ( th . Context , & model . User {
2019-01-30 12:55:02 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user3" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user3 )
require . Nil ( t , appErr )
} ( )
2019-01-30 12:55:02 -05:00
2021-05-11 06:00:44 -04:00
user4 , err := th . App . CreateUser ( th . Context , & model . User {
2019-01-30 12:55:02 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user4" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user4 )
require . Nil ( t , appErr )
} ( )
2019-01-30 12:55:02 -05:00
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 )
} )
}
}
2024-05-15 10:06:40 -04:00
func TestPluginAPIGetUsersByIds ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . DeleteBots ( t )
2024-05-15 10:06:40 -04:00
api := th . SetupPluginAPI ( )
user1 , err := th . App . CreateUser ( th . Context , & model . User {
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user1" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2024-05-15 10:06:40 -04:00
user2 , err := th . App . CreateUser ( th . Context , & model . User {
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user2" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user2 )
require . Nil ( t , appErr )
} ( )
2024-05-15 10:06:40 -04:00
user3 , err := th . App . CreateUser ( th . Context , & model . User {
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user3" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user3 )
require . Nil ( t , appErr )
} ( )
2024-05-15 10:06:40 -04:00
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 )
}
} )
}
}
2018-11-29 13:02:06 -05:00
func TestPluginAPIGetUsersInTeam ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2018-11-29 13:02:06 -05:00
api := th . SetupPluginAPI ( )
2025-11-12 07:00:51 -05:00
team1 := th . CreateTeam ( t )
team2 := th . CreateTeam ( t )
2018-11-29 13:02:06 -05:00
2021-05-11 06:00:44 -04:00
user1 , err := th . App . CreateUser ( th . Context , & model . User {
2018-11-29 13:02:06 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user1" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2018-11-29 13:02:06 -05:00
2021-05-11 06:00:44 -04:00
user2 , err := th . App . CreateUser ( th . Context , & model . User {
2018-11-29 13:02:06 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user2" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user2 )
require . Nil ( t , appErr )
} ( )
2018-11-29 13:02:06 -05:00
2021-05-11 06:00:44 -04:00
user3 , err := th . App . CreateUser ( th . Context , & model . User {
2018-11-29 13:02:06 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user3" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user3 )
require . Nil ( t , appErr )
} ( )
2018-11-29 13:02:06 -05:00
2021-05-11 06:00:44 -04:00
user4 , err := th . App . CreateUser ( th . Context , & model . User {
2018-11-29 13:02:06 -05:00
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Password : "password" ,
Username : "user4" + model . NewId ( ) ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user4 )
require . Nil ( t , appErr )
} ( )
2018-11-29 13:02:06 -05:00
// Add all users to team 1
2021-10-13 08:20:40 -04:00
_ , 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 )
2018-11-29 13:02:06 -05:00
// Add only user3 and user4 to team 2
2021-10-13 08:20:40 -04:00
_ , appErr = th . App . JoinUserToTeam ( th . Context , team2 , user3 , "" )
require . Nil ( t , appErr )
_ , appErr = th . App . JoinUserToTeam ( th . Context , team2 , user4 , "" )
require . Nil ( t , appErr )
2018-11-29 13:02:06 -05:00
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 )
2021-10-13 08:20:40 -04:00
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 )
2018-11-29 13:02:06 -05:00
} )
}
}
2021-11-10 11:11:20 -05:00
func TestPluginAPIUserCustomStatus ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2021-11-10 11:11:20 -05:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2021-11-10 11:11:20 -05:00
api := th . SetupPluginAPI ( )
user1 , err := th . App . CreateUser ( th . Context , & model . User {
Email : strings . ToLower ( model . NewId ( ) ) + "success+test@example.com" ,
Username : "user_" + model . NewId ( ) ,
Password : "password" ,
} )
require . Nil ( t , err )
2024-11-12 15:56:35 -05:00
defer func ( ) {
appErr := th . App . PermanentDeleteUser ( th . Context , user1 )
require . Nil ( t , appErr )
} ( )
2021-11-10 11:11:20 -05:00
custom := & model . CustomStatus {
2023-12-08 10:35:15 -05:00
Emoji : "tada" ,
2021-11-10 11:11:20 -05:00
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 )
2022-07-28 11:05:03 -04:00
assert . Equal ( t , err . Error ( ) , "SetCustomStatus: Failed to update the custom status. Please add either emoji or custom text status or both." )
2021-11-10 11:11:20 -05:00
// 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 )
}
2018-12-13 04:46:42 -05:00
func TestPluginAPIGetFile ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2018-12-13 04:46:42 -05:00
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" )
2024-04-02 23:34:33 -04:00
info , err := th . App . DoUploadFile ( th . Context , uploadTime , th . BasicTeam . Id , th . BasicChannel . Id , th . BasicUser . Id , filename , fileData , true )
2018-12-13 04:46:42 -05:00
require . Nil ( t , err )
defer func ( ) {
2024-11-12 15:56:35 -05:00
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 )
2018-12-13 04:46:42 -05:00
} ( )
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 )
}
2019-08-12 17:35:46 -04:00
2020-02-14 15:21:54 -05:00
func TestPluginAPIGetFileInfos ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2020-02-14 15:21:54 -05:00
api := th . SetupPluginAPI ( )
2021-05-11 06:00:44 -04:00
fileInfo1 , err := th . App . DoUploadFile ( th . Context ,
2020-02-14 15:21:54 -05:00
time . Date ( 2020 , 1 , 1 , 1 , 1 , 1 , 1 , time . UTC ) ,
th . BasicTeam . Id ,
th . BasicChannel . Id ,
th . BasicUser . Id ,
"testFile1" ,
[ ] byte ( "testfile1 Content" ) ,
2024-04-02 23:34:33 -04:00
true ,
2020-02-14 15:21:54 -05:00
)
require . Nil ( t , err )
defer func ( ) {
2024-11-12 15:56:35 -05:00
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 )
2020-02-14 15:21:54 -05:00
} ( )
2021-05-11 06:00:44 -04:00
fileInfo2 , err := th . App . DoUploadFile ( th . Context ,
2020-02-14 15:21:54 -05:00
time . Date ( 2020 , 1 , 2 , 1 , 1 , 1 , 1 , time . UTC ) ,
th . BasicTeam . Id ,
th . BasicChannel . Id ,
th . BasicUser2 . Id ,
"testFile2" ,
[ ] byte ( "testfile2 Content" ) ,
2024-04-02 23:34:33 -04:00
true ,
2020-02-14 15:21:54 -05:00
)
require . Nil ( t , err )
defer func ( ) {
2024-11-12 15:56:35 -05:00
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 )
2020-02-14 15:21:54 -05:00
} ( )
2021-05-11 06:00:44 -04:00
fileInfo3 , err := th . App . DoUploadFile ( th . Context ,
2020-02-14 15:21:54 -05:00
time . Date ( 2020 , 1 , 3 , 1 , 1 , 1 , 1 , time . UTC ) ,
th . BasicTeam . Id ,
th . BasicChannel . Id ,
th . BasicUser . Id ,
"testFile3" ,
[ ] byte ( "testfile3 Content" ) ,
2024-04-02 23:34:33 -04:00
true ,
2020-02-14 15:21:54 -05:00
)
require . Nil ( t , err )
defer func ( ) {
2024-11-12 15:56:35 -05:00
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 )
2020-02-14 15:21:54 -05:00
} ( )
_ , 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 } ,
2021-07-12 14:05:36 -04:00
SortBy : model . FileinfoSortByCreated ,
2020-02-14 15:21:54 -05:00
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 )
} )
}
2018-11-19 12:27:15 -05:00
func TestPluginAPISavePluginConfig ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-02-10 13:31:41 -05:00
th := Setup ( t )
2018-11-19 12:27:15 -05:00
manifest := & model . Manifest {
Id : "pluginid" ,
SettingsSchema : & model . PluginSettingsSchema {
Settings : [ ] * model . PluginSetting {
{ Key : "MyStringSetting" , Type : "text" } ,
{ Key : "MyIntSetting" , Type : "text" } ,
{ Key : "MyBoolSetting" , Type : "bool" } ,
} ,
} ,
}
2021-05-11 06:00:44 -04:00
api := NewPluginAPI ( th . App , th . Context , manifest )
2018-11-19 12:27:15 -05:00
pluginConfigJsonString := ` { "mystringsetting": "str", "MyIntSetting": 32, "myboolsetting": true} `
2022-07-05 02:46:50 -04:00
var pluginConfig map [ string ] any
2019-09-19 18:33:24 -04:00
err := json . Unmarshal ( [ ] byte ( pluginConfigJsonString ) , & pluginConfig )
require . NoError ( t , err )
2018-11-19 12:27:15 -05:00
2019-09-19 18:33:24 -04:00
appErr := api . SavePluginConfig ( pluginConfig )
require . Nil ( t , appErr )
2018-11-19 12:27:15 -05:00
type Configuration struct {
MyStringSetting string
2018-11-20 09:43:42 -05:00
MyIntSetting int
MyBoolSetting bool
2018-11-19 12:27:15 -05:00
}
savedConfiguration := new ( Configuration )
2019-09-19 18:33:24 -04:00
err = api . LoadPluginConfiguration ( savedConfiguration )
require . NoError ( t , err )
2018-11-19 12:27:15 -05:00
expectedConfiguration := new ( Configuration )
2019-09-19 18:33:24 -04:00
err = json . Unmarshal ( [ ] byte ( pluginConfigJsonString ) , & expectedConfiguration )
require . NoError ( t , err )
2018-11-19 12:27:15 -05:00
assert . Equal ( t , expectedConfiguration , savedConfiguration )
}
2025-12-12 15:45:51 -05:00
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" ] )
}
2018-07-18 19:35:12 -04:00
func TestPluginAPILoadPluginConfiguration ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-02-10 13:31:41 -05:00
th := Setup ( t )
2018-07-18 19:35:12 -04:00
2022-07-05 02:46:50 -04:00
var pluginJson map [ string ] any
2024-09-12 13:23:57 -04:00
err := json . Unmarshal ( [ ] byte ( ` { "mystringsetting": "str", "MyIntSetting": 32, "myBoolsetting": true} ` ) , & pluginJson )
2019-09-19 18:33:24 -04:00
require . NoError ( t , err )
2018-07-18 19:35:12 -04:00
th . App . UpdateConfig ( func ( cfg * model . Config ) {
cfg . PluginSettings . Plugins [ "testloadpluginconfig" ] = pluginJson
} )
2018-10-03 13:13:19 -04:00
2024-05-15 11:05:13 -04:00
fullPath := filepath . Join ( server . GetPackagePath ( ) , "channels" , "app" , "plugin_api_tests" , "manual.test_load_configuration_plugin" , "main.go" )
2019-07-29 09:07:00 -04:00
2025-11-11 07:41:18 -05:00
err = pluginAPIHookTest ( t , th , fullPath , "testloadpluginconfig" , ` {
2018-07-18 19:35:12 -04:00
"settings" : [
{
"key" : "MyStringSetting" ,
"type" : "text"
} ,
{
"key" : "MyIntSetting" ,
"type" : "text"
} ,
{
"key" : "MyBoolSetting" ,
"type" : "bool"
}
]
2025-11-11 07:41:18 -05:00
} ` )
2020-01-21 03:41:28 -05:00
require . NoError ( t , err )
2018-07-18 19:35:12 -04:00
}
func TestPluginAPILoadPluginConfigurationDefaults ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-02-10 13:31:41 -05:00
th := Setup ( t )
2018-07-18 19:35:12 -04:00
2022-07-05 02:46:50 -04:00
var pluginJson map [ string ] any
2019-09-19 18:33:24 -04:00
err := json . Unmarshal ( [ ] byte ( ` { "mystringsetting": "override"} ` ) , & pluginJson )
require . NoError ( t , err )
2018-07-18 19:35:12 -04:00
th . App . UpdateConfig ( func ( cfg * model . Config ) {
cfg . PluginSettings . Plugins [ "testloadpluginconfig" ] = pluginJson
} )
2019-07-29 09:07:00 -04:00
2024-05-15 11:05:13 -04:00
fullPath := filepath . Join ( server . GetPackagePath ( ) , "channels" , "app" , "plugin_api_tests" , "manual.test_load_configuration_defaults_plugin" , "main.go" )
2019-07-29 09:07:00 -04:00
2020-01-21 03:41:28 -05:00
err = pluginAPIHookTest ( t , th , fullPath , "testloadpluginconfig" , ` {
2018-07-18 19:35:12 -04:00
"settings" : [
{
"key" : "MyStringSetting" ,
"type" : "text" ,
"default" : "notthis"
} ,
{
"key" : "MyIntSetting" ,
"type" : "text" ,
"default" : 35
} ,
{
"key" : "MyBoolSetting" ,
"type" : "bool" ,
"default" : true
}
]
2020-01-21 03:41:28 -05:00
} ` )
2019-07-29 09:07:00 -04:00
2020-01-21 03:41:28 -05:00
require . NoError ( t , err )
2018-11-15 15:23:03 -05:00
}
2018-11-08 13:17:07 -05:00
func TestPluginAPIGetPlugins ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-02-10 13:31:41 -05:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2018-11-08 13:17:07 -05:00
api := th . SetupPluginAPI ( )
pluginCode := `
package main
import (
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/plugin"
2018-11-08 13:17:07 -05:00
)
type MyPlugin struct {
plugin . MattermostPlugin
}
func main ( ) {
plugin . ClientMain ( & MyPlugin { } )
}
`
2022-08-09 07:25:46 -04:00
pluginDir , err := os . MkdirTemp ( "" , "" )
2018-11-08 13:17:07 -05:00
require . NoError ( t , err )
2022-08-09 07:25:46 -04:00
webappPluginDir , err := os . MkdirTemp ( "" , "" )
2018-11-08 13:17:07 -05:00
require . NoError ( t , err )
defer os . RemoveAll ( pluginDir )
defer os . RemoveAll ( webappPluginDir )
2023-05-03 15:04:10 -04:00
env , err := plugin . NewEnvironment ( th . NewPluginAPI , NewDriverImpl ( th . Server ) , pluginDir , webappPluginDir , th . App . Log ( ) , nil )
2018-11-08 13:17:07 -05:00
require . NoError ( t , err )
pluginIDs := [ ] string { "pluginid1" , "pluginid2" , "pluginid3" }
var pluginManifests [ ] * model . Manifest
for _ , pluginID := range pluginIDs {
backend := filepath . Join ( pluginDir , pluginID , "backend.exe" )
2019-04-05 10:35:51 -04:00
utils . CompileGo ( t , pluginCode , backend )
2018-11-08 13:17:07 -05:00
2025-07-18 06:54:51 -04:00
err := os . WriteFile ( filepath . Join ( pluginDir , pluginID , "plugin.json" ) , fmt . Appendf ( nil , ` { "id": "%s", "server": { "executable": "backend.exe"}} ` , pluginID ) , 0600 )
2024-11-12 15:56:35 -05:00
require . NoError ( t , err )
2018-11-08 13:17:07 -05:00
manifest , activated , reterr := env . Activate ( pluginID )
2021-02-16 06:00:01 -05:00
require . NoError ( t , reterr )
2018-11-08 13:17:07 -05:00
require . NotNil ( t , manifest )
require . True ( t , activated )
pluginManifests = append ( pluginManifests , manifest )
}
2022-12-21 14:10:26 -05:00
th . App . ch . SetPluginsEnvironment ( env )
2018-11-08 13:17:07 -05:00
2019-05-09 16:40:05 -04:00
// Deactivate the last one for testing
success := env . Deactivate ( pluginIDs [ len ( pluginIDs ) - 1 ] )
require . True ( t , success )
2018-11-08 13:17:07 -05:00
// check existing user first
2021-02-16 06:00:01 -05:00
plugins , appErr := api . GetPlugins ( )
assert . Nil ( t , appErr )
2018-11-08 13:17:07 -05:00
assert . NotEmpty ( t , plugins )
assert . Equal ( t , pluginManifests , plugins )
}
2018-11-16 08:17:42 -05:00
2019-10-17 14:57:55 -04:00
func TestPluginAPIInstallPlugin ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-02-10 13:31:41 -05:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2019-10-17 14:57:55 -04:00
api := th . SetupPluginAPI ( )
2024-05-15 11:05:13 -04:00
tarData , err := os . ReadFile ( filepath . Join ( server . GetPackagePath ( ) , "tests" , "testplugin.tar.gz" ) )
2019-10-17 14:57:55 -04:00
require . NoError ( t , err )
2021-02-16 06:00:01 -05:00
_ , appErr := api . InstallPlugin ( bytes . NewReader ( tarData ) , true )
assert . NotNil ( t , appErr , "should not allow upload if upload disabled" )
2022-07-28 11:05:03 -04:00
assert . Equal ( t , appErr . Error ( ) , "installPlugin: Plugins and/or plugin uploads have been disabled." )
2019-10-17 14:57:55 -04:00
th . App . UpdateConfig ( func ( cfg * model . Config ) {
* cfg . PluginSettings . Enable = true
* cfg . PluginSettings . EnableUploads = true
} )
2021-02-16 06:00:01 -05:00
manifest , appErr := api . InstallPlugin ( bytes . NewReader ( tarData ) , true )
2019-10-17 14:57:55 -04:00
defer os . RemoveAll ( "plugins/testplugin" )
2021-02-16 06:00:01 -05:00
require . Nil ( t , appErr )
2019-10-17 14:57:55 -04:00
assert . Equal ( t , "testplugin" , manifest . Id )
// Successfully installed
2021-02-16 06:00:01 -05:00
pluginsResp , appErr := api . GetPlugins ( )
require . Nil ( t , appErr )
2019-10-17 14:57:55 -04:00
found := false
for _ , m := range pluginsResp {
if m . Id == manifest . Id {
found = true
}
}
assert . True ( t , found )
}
2019-11-29 06:41:17 -05:00
func TestInstallPlugin ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2021-08-16 13:46:44 -04:00
// 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
2019-11-29 06:41:17 -05:00
// 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.
2025-09-10 09:11:32 -04:00
setupTest := func ( t * testing . T , pluginCode string , pluginManifest string , pluginID string , app * App , rctx request . CTX ) ( func ( ) , string ) {
2022-08-09 07:25:46 -04:00
pluginDir , err := os . MkdirTemp ( "" , "" )
2019-11-29 06:41:17 -05:00
require . NoError ( t , err )
2022-08-09 07:25:46 -04:00
webappPluginDir , err := os . MkdirTemp ( "" , "" )
2019-11-29 06:41:17 -05:00
require . NoError ( t , err )
app . UpdateConfig ( func ( cfg * model . Config ) {
* cfg . PluginSettings . Directory = pluginDir
* cfg . PluginSettings . ClientDirectory = webappPluginDir
} )
2021-05-11 06:00:44 -04:00
newPluginAPI := func ( manifest * model . Manifest ) plugin . API {
2025-09-10 09:11:32 -04:00
return app . NewPluginAPI ( rctx , manifest )
2021-05-11 06:00:44 -04:00
}
2023-05-03 15:04:10 -04:00
env , err := plugin . NewEnvironment ( newPluginAPI , NewDriverImpl ( app . Srv ( ) ) , pluginDir , webappPluginDir , app . Log ( ) , nil )
2019-11-29 06:41:17 -05:00
require . NoError ( t , err )
2022-12-21 14:10:26 -05:00
app . ch . SetPluginsEnvironment ( env )
2019-11-29 06:41:17 -05:00
backend := filepath . Join ( pluginDir , pluginID , "backend.exe" )
utils . CompileGo ( t , pluginCode , backend )
2024-11-12 15:56:35 -05:00
err = os . WriteFile ( filepath . Join ( pluginDir , pluginID , "plugin.json" ) , [ ] byte ( pluginManifest ) , 0600 )
require . NoError ( t , err )
2019-11-29 06:41:17 -05:00
manifest , activated , reterr := env . Activate ( pluginID )
2021-02-16 06:00:01 -05:00
require . NoError ( t , reterr )
2019-11-29 06:41:17 -05:00
require . NotNil ( t , manifest )
require . True ( t , activated )
return func ( ) {
os . RemoveAll ( pluginDir )
os . RemoveAll ( webappPluginDir )
} , pluginDir
}
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2019-11-29 06:41:17 -05:00
// start an http server to serve plugin's tarball to the test.
2024-05-15 11:05:13 -04:00
ts := httptest . NewServer ( http . FileServer ( http . Dir ( filepath . Join ( server . GetPackagePath ( ) , "tests" ) ) ) )
2019-11-29 06:41:17 -05:00
defer ts . Close ( )
th . App . UpdateConfig ( func ( cfg * model . Config ) {
* cfg . PluginSettings . Enable = true
* cfg . PluginSettings . EnableUploads = true
2022-07-05 02:46:50 -04:00
cfg . PluginSettings . Plugins [ "testinstallplugin" ] = map [ string ] any {
2019-11-29 06:41:17 -05:00
"DownloadURL" : ts . URL + "/testplugin.tar.gz" ,
}
} )
tearDown , _ := setupTest ( t ,
`
package main
import (
"net/http"
2020-05-28 13:15:47 -04:00
2019-11-29 06:41:17 -05:00
"github.com/pkg/errors"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/plugin"
2019-11-29 06:41:17 -05:00
)
type configuration struct {
DownloadURL string
}
2020-05-28 13:15:47 -04:00
2019-11-29 06:41:17 -05:00
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
}
2020-05-28 13:15:47 -04:00
2019-11-29 06:41:17 -05:00
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 { } )
}
2020-05-28 13:15:47 -04:00
2019-11-29 06:41:17 -05:00
` ,
2021-08-13 13:41:32 -04:00
` { "id" : "testinstallplugin" , "server" : { "executable" : "backend.exe" } , "settings_schema" : {
2019-11-29 06:41:17 -05:00
"settings" : [
{
"key" : "DownloadURL" ,
"type" : "text"
}
]
2021-05-11 06:00:44 -04:00
} } ` , "testinstallplugin" , th . App , th . Context )
2019-11-29 06:41:17 -05:00
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 )
}
2018-11-16 08:17:42 -05:00
func TestPluginAPIGetTeamIcon ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2018-11-16 08:17:42 -05:00
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 )
2021-02-16 06:00:01 -05:00
require . NoError ( t , err )
2018-11-16 08:17:42 -05:00
dataBytes := buf . Bytes ( )
fileReader := bytes . NewReader ( dataBytes )
// Set the Team Icon
2025-04-01 15:57:43 -04:00
appErr := th . App . SetTeamIconFromFile ( th . Context , th . BasicTeam , fileReader )
2021-02-16 06:00:01 -05:00
require . Nil ( t , appErr )
2018-11-16 08:17:42 -05:00
// Get the team icon to check
2021-02-16 06:00:01 -05:00
teamIcon , appErr := api . GetTeamIcon ( th . BasicTeam . Id )
require . Nil ( t , appErr )
2018-11-16 10:52:07 -05:00
require . NotEmpty ( t , teamIcon )
2018-11-16 08:17:42 -05:00
colorful := color . NRGBA { 255 , 0 , 0 , 255 }
2018-11-16 10:52:07 -05:00
byteReader := bytes . NewReader ( teamIcon )
img2 , _ , err2 := image . Decode ( byteReader )
2021-02-16 06:00:01 -05:00
require . NoError ( t , err2 )
2018-11-16 10:52:07 -05:00
require . Equal ( t , img2 . At ( 2 , 3 ) , colorful )
}
func TestPluginAPISetTeamIcon ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2018-11-16 10:52:07 -05:00
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 )
2021-02-16 06:00:01 -05:00
require . NoError ( t , err )
2018-11-16 10:52:07 -05:00
dataBytes := buf . Bytes ( )
// Set the user profile image
2021-02-16 06:00:01 -05:00
appErr := api . SetTeamIcon ( th . BasicTeam . Id , dataBytes )
require . Nil ( t , appErr )
2018-11-16 10:52:07 -05:00
// Get the user profile image to check
2021-02-16 06:00:01 -05:00
teamIcon , appErr := api . GetTeamIcon ( th . BasicTeam . Id )
require . Nil ( t , appErr )
2018-11-16 10:52:07 -05:00
require . NotEmpty ( t , teamIcon )
colorful := color . NRGBA { 255 , 0 , 0 , 255 }
byteReader := bytes . NewReader ( teamIcon )
2018-11-16 08:17:42 -05:00
img2 , _ , err2 := image . Decode ( byteReader )
2021-02-16 06:00:01 -05:00
require . NoError ( t , err2 )
2018-11-16 08:17:42 -05:00
require . Equal ( t , img2 . At ( 2 , 3 ) , colorful )
}
2018-11-20 08:50:34 -05:00
2018-11-20 09:43:42 -05:00
func TestPluginAPIRemoveTeamIcon ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2018-11-20 09:43:42 -05:00
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 )
2021-02-16 06:00:01 -05:00
require . NoError ( t , err1 )
2018-11-20 09:43:42 -05:00
dataBytes := buf . Bytes ( )
fileReader := bytes . NewReader ( dataBytes )
// Set the Team Icon
2025-04-01 15:57:43 -04:00
err := th . App . SetTeamIconFromFile ( th . Context , th . BasicTeam , fileReader )
2018-11-20 09:43:42 -05:00
require . Nil ( t , err )
err = api . RemoveTeamIcon ( th . BasicTeam . Id )
require . Nil ( t , err )
}
2018-11-21 05:36:02 -05:00
2020-01-21 03:41:28 -05:00
func pluginAPIHookTest ( t * testing . T , th * TestHelper , fileName string , id string , settingsSchema string ) error {
2022-08-09 07:25:46 -04:00
data , err := os . ReadFile ( fileName )
2020-01-21 03:41:28 -05:00
if err != nil {
2019-07-29 09:07:00 -04:00
return err
2020-01-21 03:41:28 -05:00
}
code := string ( data )
schema := ` { "settings": [ ] } `
if settingsSchema != "" {
schema = settingsSchema
}
2022-10-06 04:04:21 -04:00
th . App . ch . srv . platform . SetSqlStore ( th . GetSqlStore ( ) ) // TODO: platform: check if necessary
2021-08-16 13:46:44 -04:00
setupPluginAPITest ( t , code ,
2021-08-13 13:41:32 -04:00
fmt . Sprintf ( ` { "id": "%v", "server": { "executable": "backend.exe"}, "settings_schema": %v} ` , id , schema ) ,
2021-05-11 06:00:44 -04:00
id , th . App , th . Context )
2020-01-21 03:41:28 -05:00
hooks , err := th . App . GetPluginsEnvironment ( ) . HooksForPlugin ( id )
require . NoError ( t , err )
require . NotNil ( t , hooks )
_ , ret := hooks . MessageWillBePosted ( nil , nil )
if ret != "OK" {
return errors . New ( ret )
}
2021-11-10 11:11:20 -05:00
2020-01-21 03:41:28 -05:00
return nil
2019-07-29 09:07:00 -04:00
}
2020-01-21 03:41:28 -05:00
// 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
2020-03-02 11:10:41 -05:00
// 5. Successfully running test should return nil, "OK". Any other returned string is considered and error
2020-01-21 03:41:28 -05:00
func TestBasicAPIPlugins ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-01-21 03:41:28 -05:00
defaultSchema := getDefaultPluginSettingsSchema ( )
2024-05-15 11:05:13 -04:00
testFolder := filepath . Join ( server . GetPackagePath ( ) , "channels" , "app" , "plugin_api_tests" )
2022-08-09 07:25:46 -04:00
dirs , err := os . ReadDir ( testFolder )
2020-01-21 03:41:28 -05:00
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 ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2024-05-15 11:05:13 -04:00
mainPath := filepath . Join ( testFolder , d , "main.go" )
2020-01-21 03:41:28 -05:00
_ , err := os . Stat ( mainPath )
require . NoError ( t , err , "Cannot find plugin main file at %v" , mainPath )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t ) . DeleteBots ( t )
2020-01-21 03:41:28 -05:00
setDefaultPluginConfig ( th , dir . Name ( ) )
err = pluginAPIHookTest ( t , th , mainPath , dir . Name ( ) , defaultSchema )
require . NoError ( t , err )
MM-12393 Server side of bot accounts. (#10378)
* bots model, store and api (#9903)
* bots model, store and api
Fixes: MM-13100, MM-13101, MM-13103, MM-13105, MMM-13119
* uncomment tests incorrectly commented, and fix merge issues
* add etags support
* add missing licenses
* remove unused sqlbuilder.go (for now...)
* rejig permissions
* split out READ_BOTS into READ_BOTS and READ_OTHERS_BOTS, the latter
implicitly allowing the former
* make MANAGE_OTHERS_BOTS imply MANAGE_BOTS
* conform to general rest api pattern
* eliminate redundant http.StatusOK
* Update api4/bot.go
Co-Authored-By: lieut-data <jesse.hallam@gmail.com>
* s/model.UserFromBotModel/model.UserFromBot/g
* Update model/bot.go
Co-Authored-By: lieut-data <jesse.hallam@gmail.com>
* Update model/client4.go
Co-Authored-By: lieut-data <jesse.hallam@gmail.com>
* move sessionHasPermissionToManageBot to app/authorization.go
* use api.ApiSessionRequired for createBot
* introduce BOT_DESCRIPTION_MAX_RUNES constant
* MM-13512 Prevent getting a user by email based on privacy settings (#10021)
* MM-13512 Prevent getting a user by email based on privacy settings
* Add additional config settings to tests
* upgrade db to 5.7 (#10019)
* MM-13526 Add validation when setting a user's Locale field (#10022)
* Fix typos (#10024)
* Fixing first user being created with system admin privilages without being explicity specified. (#10014)
* Revert "Support for Embeded chat (#9129)" (#10017)
This reverts commit 3fcecd521a5c6ccfdb52fb4c3fb1f8c6ea528a4e.
* s/DisableBot/UpdateBotActive
* add permissions on upgrade
* Update NOTICE.txt (#10054)
- add new dependency (text)
- handle switch to forked dependency (go-gomail -> go-mail)
- misc copyright owner updates
* avoid leaking bot knowledge without permission
* [GH-6798] added a new api endpoint to get the bulk reactions for posts (#10049)
* 6798 added a new api to get the bulk reactions for posts
* 6798 added the permsission check before getting the reactions
* GH-6798 added a new app function for the new endpoint
* 6798 added a store method to get reactions for multiple posts
* 6798 connected the app function with the new store function
* 6798 fixed the review comments
* MM-13559 Update model.post.is_valid.file_ids.app_error text per report (#10055)
Ticket: https://mattermost.atlassian.net/browse/MM-13559
Report: https://github.com/mattermost/mattermost-server/issues/10023
* Trigger Login Hooks with OAuth (#10061)
* make BotStore.GetAll deterministic even on duplicate CreateAt
* fix spurious TestMuteCommandSpecificChannel test failure
See
https://community-daily.mattermost.com/core/pl/px9p8s3dzbg1pf3ddrm5cr36uw
* fix race in TestExportUserChannels
* TestExportUserChannels: remove SaveMember call, as it is redundant and used to be silently failing anyway
* MM-13117: bot tokens (#10111)
* eliminate redundant Client/AdminClient declarations
* harden TestUpdateChannelScheme to API failures
* eliminate unnecessary config restoration
* minor cleanup
* make TestGenerateMfaSecret config dependency explicit
* TestCreateUserAccessToken for bots
* TestGetUserAccessToken* for bots
* leverage SessionHasPermissionToUserOrBot for user token APIs
* Test(Revoke|Disable|Enable)UserAccessToken
* make EnableUserAccessTokens explicit, so as to not rely on local config.json
* uncomment TestResetPassword, but still skip
* mark assert(Invalid)Token as helper
* fix whitespace issues
* fix mangled comments
* MM-13116: bot plugin api (#10113)
* MM-13117: expose bot API to plugins
This also changes the `CreatorId` column definition to allow for plugin
ids, as the default unless the plugin overrides is to use the plugin id
here. This branch hasn't hit master yet, so no migration needed.
* gofmt issues
* expunge use of BotList in plugin/client API
* introduce model.BotGetOptions
* use botUserId term for clarity
* MM-13129 Adding functionality to deal with orphaned bots (#10238)
* Add way to list orphaned bots.
* Add /assign route to modify ownership of bot accounts.
* Apply suggestions from code review
Co-Authored-By: crspeller <crspeller@gmail.com>
* MM-13120: add IsBot field to returned user objects (#10103)
* MM-13104: forbid bot login (#10251)
* MM-13104: disallow bot login
* fix shadowing
* MM-13136 Disable user bots when user is disabled. (#10293)
* Disable user bots when user is disabled.
* Grammer.
Co-Authored-By: crspeller <crspeller@gmail.com>
* Fixing bot branch for test changes.
* Don't use external dependancies in bot plugin tests.
* Rename bot CreatorId to OwnerId
* Adding ability to re-enable bots
* Fixing IsBot to not attempt to be saved to DB.
* Adding diagnostics and licencing counting for bot accounts.
* Modifying gorp to allow reading of '-' fields.
* Removing unnessisary nil values from UserCountOptions.
* Changing comment to GoDoc format
* Improving user count SQL
* Some improvments from feedback.
* Omit empty on User.IsBot
2019-03-05 10:06:45 -05:00
} )
}
2020-01-21 03:41:28 -05:00
}
2019-02-23 14:41:19 -05:00
}
2019-04-23 13:35:17 -04:00
func TestPluginAPIKVCompareAndSet ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2019-04-23 13:35:17 -04:00
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 )
} )
}
}
2019-05-14 16:15:23 -04:00
2019-08-21 22:25:38 -04:00
func TestPluginAPIKVCompareAndDelete ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2019-08-21 22:25:38 -04:00
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 )
} )
}
}
2019-05-14 16:15:23 -04:00
func TestPluginCreateBot ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2019-05-14 16:15:23 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2019-05-14 16:15:23 -04:00
api := th . SetupPluginAPI ( )
bot , err := api . CreateBot ( & model . Bot {
2024-07-31 10:27:52 -04:00
Username : "a" + model . NewRandomString ( 10 ) ,
2019-05-14 16:15:23 -04:00
DisplayName : "bot" ,
Description : "bot" ,
} )
require . Nil ( t , err )
_ , err = api . CreateBot ( & model . Bot {
2024-07-31 10:27:52 -04:00
Username : "a" + model . NewRandomString ( 10 ) ,
2019-05-14 16:15:23 -04:00
OwnerId : bot . UserId ,
DisplayName : "bot2" ,
Description : "bot2" ,
} )
require . NotNil ( t , err )
}
2019-08-12 17:35:46 -04:00
func TestPluginCreatePostWithUploadedFile ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2019-08-12 17:35:46 -04:00
api := th . SetupPluginAPI ( )
data := [ ] byte ( "Hello World" )
2021-02-25 14:22:27 -05:00
channelID := th . BasicChannel . Id
2019-08-12 17:35:46 -04:00
filename := "testGetFile"
2021-02-25 14:22:27 -05:00
fileInfo , err := api . UploadFile ( data , channelID , filename )
2019-08-12 17:35:46 -04:00
require . Nil ( t , err )
defer func ( ) {
2024-11-12 15:56:35 -05:00
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 )
2019-08-12 17:35:46 -04:00
} ( )
actualData , err := api . GetFile ( fileInfo . Id )
require . Nil ( t , err )
assert . Equal ( t , data , actualData )
2021-02-05 05:22:27 -05:00
userID := th . BasicUser . Id
2019-08-12 17:35:46 -04:00
post , err := api . CreatePost ( & model . Post {
Message : "test" ,
2021-02-05 05:22:27 -05:00
UserId : userID ,
2021-02-25 14:22:27 -05:00
ChannelId : channelID ,
2019-08-12 17:35:46 -04:00
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 )
}
2019-09-03 18:41:52 -04:00
2022-09-30 04:12:15 -04:00
func TestPluginCreatePostAddsFromPluginProp ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2022-09-30 04:12:15 -04:00
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 )
2025-03-20 07:53:50 -04:00
assert . Equal ( t , "true" , actualPost . GetProp ( model . PostPropsFromPlugin ) )
2022-09-30 04:12:15 -04:00
}
2019-09-03 18:41:52 -04:00
func TestPluginAPIGetConfig ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2019-09-03 18:41:52 -04:00
api := th . SetupPluginAPI ( )
config := api . GetConfig ( )
2021-01-25 05:15:17 -05:00
if config . LdapSettings . BindPassword != nil && * config . LdapSettings . BindPassword != "" {
2021-07-12 14:05:36 -04:00
assert . Equal ( t , * config . LdapSettings . BindPassword , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-07-12 14:05:36 -04:00
assert . Equal ( t , * config . FileSettings . PublicLinkSalt , model . FakeSetting )
2019-09-03 18:41:52 -04:00
2021-01-25 05:15:17 -05:00
if * config . FileSettings . AmazonS3SecretAccessKey != "" {
2021-07-12 14:05:36 -04:00
assert . Equal ( t , * config . FileSettings . AmazonS3SecretAccessKey , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-01-25 05:15:17 -05:00
if config . EmailSettings . SMTPPassword != nil && * config . EmailSettings . SMTPPassword != "" {
2021-07-12 14:05:36 -04:00
assert . Equal ( t , * config . EmailSettings . SMTPPassword , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-01-25 05:15:17 -05:00
if * config . GitLabSettings . Secret != "" {
2021-07-12 14:05:36 -04:00
assert . Equal ( t , * config . GitLabSettings . Secret , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-07-12 14:05:36 -04:00
assert . Equal ( t , * config . SqlSettings . DataSource , model . FakeSetting )
assert . Equal ( t , * config . SqlSettings . AtRestEncryptKey , model . FakeSetting )
assert . Equal ( t , * config . ElasticsearchSettings . Password , model . FakeSetting )
2019-09-03 18:41:52 -04:00
for i := range config . SqlSettings . DataSourceReplicas {
2021-07-12 14:05:36 -04:00
assert . Equal ( t , config . SqlSettings . DataSourceReplicas [ i ] , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
for i := range config . SqlSettings . DataSourceSearchReplicas {
2021-07-12 14:05:36 -04:00
assert . Equal ( t , config . SqlSettings . DataSourceSearchReplicas [ i ] , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
}
func TestPluginAPIGetUnsanitizedConfig ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2019-09-03 18:41:52 -04:00
api := th . SetupPluginAPI ( )
config := api . GetUnsanitizedConfig ( )
2021-01-25 05:15:17 -05:00
if config . LdapSettings . BindPassword != nil && * config . LdapSettings . BindPassword != "" {
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , * config . LdapSettings . BindPassword , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , * config . FileSettings . PublicLinkSalt , model . FakeSetting )
2019-09-03 18:41:52 -04:00
2021-01-25 05:15:17 -05:00
if * config . FileSettings . AmazonS3SecretAccessKey != "" {
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , * config . FileSettings . AmazonS3SecretAccessKey , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-01-25 05:15:17 -05:00
if config . EmailSettings . SMTPPassword != nil && * config . EmailSettings . SMTPPassword != "" {
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , * config . EmailSettings . SMTPPassword , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-01-25 05:15:17 -05:00
if * config . GitLabSettings . Secret != "" {
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , * config . GitLabSettings . Secret , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , * config . SqlSettings . DataSource , model . FakeSetting )
assert . NotEqual ( t , * config . SqlSettings . AtRestEncryptKey , model . FakeSetting )
assert . NotEqual ( t , * config . ElasticsearchSettings . Password , model . FakeSetting )
2019-09-03 18:41:52 -04:00
for i := range config . SqlSettings . DataSourceReplicas {
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , config . SqlSettings . DataSourceReplicas [ i ] , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
for i := range config . SqlSettings . DataSourceSearchReplicas {
2021-07-12 14:05:36 -04:00
assert . NotEqual ( t , config . SqlSettings . DataSourceSearchReplicas [ i ] , model . FakeSetting )
2019-09-03 18:41:52 -04:00
}
}
2019-10-17 19:11:26 -04:00
func TestPluginAddUserToChannel ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2019-10-17 19:11:26 -04:00
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 )
}
2019-11-04 20:35:58 -05:00
func TestInterpluginPluginHTTP ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2019-11-04 20:35:58 -05:00
2021-08-16 13:46:44 -04:00
setupMultiPluginAPITest ( t ,
2025-03-11 13:44:42 -04:00
[ ] string {
`
2019-11-04 20:35:58 -05:00
package main
import (
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/plugin"
2019-11-04 20:35:58 -05:00
"bytes"
"net/http"
)
type MyPlugin struct {
plugin . MattermostPlugin
}
func ( p * MyPlugin ) ServeHTTP ( c * plugin . Context , w http . ResponseWriter , r * http . Request ) {
2021-07-14 09:07:31 -04:00
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 )
2019-11-04 20:35:58 -05:00
}
}
func main ( ) {
plugin . ClientMain ( & MyPlugin { } )
}
` ,
`
package main
import (
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
2019-11-04 20:35:58 -05:00
"bytes"
"net/http"
2022-08-09 07:25:46 -04:00
"io"
2019-11-04 20:35:58 -05:00
)
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" )
2020-06-30 04:13:27 -04:00
req , err := http . NewRequest ( "GET" , "/testplugininterserver/api/v2/test?abc=xyz" , & buf )
2019-11-04 20:35:58 -05:00
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"
}
2022-08-09 07:25:46 -04:00
respbody , err := io . ReadAll ( resp . Body )
2019-11-04 20:35:58 -05:00
if err != nil {
return nil , err . Error ( )
}
if resp . StatusCode != 598 {
return nil , "wrong status " + string ( respbody )
}
2021-07-14 09:07:31 -04:00
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"
2019-11-04 20:35:58 -05:00
}
func main ( ) {
plugin . ClientMain ( & MyPlugin { } )
}
` ,
} ,
[ ] string {
2021-08-13 13:41:32 -04:00
` { "id": "testplugininterserver", "server": { "executable": "backend.exe"}} ` ,
` { "id": "testplugininterclient", "server": { "executable": "backend.exe"}} ` ,
2019-11-04 20:35:58 -05:00
} ,
[ ] string {
"testplugininterserver" ,
"testplugininterclient" ,
} ,
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-16 23:23:52 -04:00
true ,
2019-11-04 20:35:58 -05:00
th . App ,
2021-05-11 06:00:44 -04:00
th . Context ,
2019-11-04 20:35:58 -05:00
)
hooks , err := th . App . GetPluginsEnvironment ( ) . HooksForPlugin ( "testplugininterclient" )
require . NoError ( t , err )
_ , ret := hooks . MessageWillBePosted ( nil , nil )
2025-12-01 11:26:33 -05:00
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 )
2021-07-14 09:07:31 -04:00
assert . Equal ( t , "ok" , ret )
2019-11-04 20:35:58 -05:00
}
2020-02-14 15:47:43 -05:00
2025-11-05 09:21:13 -05:00
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 )
} )
}
2021-08-16 13:46:44 -04:00
func TestAPIMetrics ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2020-02-14 15:47:43 -05:00
t . Run ( "" , func ( t * testing . T ) {
metricsMock := & mocks . MetricsInterface { }
2022-08-09 07:25:46 -04:00
pluginDir , err := os . MkdirTemp ( "" , "" )
2020-02-14 15:47:43 -05:00
require . NoError ( t , err )
2022-08-09 07:25:46 -04:00
webappPluginDir , err := os . MkdirTemp ( "" , "" )
2020-02-14 15:47:43 -05:00
require . NoError ( t , err )
defer os . RemoveAll ( pluginDir )
defer os . RemoveAll ( webappPluginDir )
2023-05-03 15:04:10 -04:00
env , err := plugin . NewEnvironment ( th . NewPluginAPI , NewDriverImpl ( th . Server ) , pluginDir , webappPluginDir , th . App . Log ( ) , metricsMock )
2020-02-14 15:47:43 -05:00
require . NoError ( t , err )
2022-12-21 14:10:26 -05:00
th . App . ch . SetPluginsEnvironment ( env )
2020-02-14 15:47:43 -05:00
2021-02-25 14:22:27 -05:00
pluginID := model . NewId ( )
backend := filepath . Join ( pluginDir , pluginID , "backend.exe" )
2025-03-11 13:44:42 -04:00
code := `
2020-02-14 15:47:43 -05:00
package main
import (
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
2020-02-14 15:47:43 -05:00
)
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 )
2024-11-12 15:56:35 -05:00
err = os . WriteFile ( filepath . Join ( pluginDir , pluginID , "plugin.json" ) , [ ] byte ( ` { "id": " ` + pluginID + ` ", "server": { "executable": "backend.exe"}} ` ) , 0600 )
require . NoError ( t , err )
2020-02-14 15:47:43 -05:00
// 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
2021-08-16 13:46:44 -04:00
metricsMock . On ( "ObservePluginAPIDuration" , pluginID , "UpdateUser" , true , mock . Anything ) . Return ( )
2020-02-14 15:47:43 -05:00
2021-02-25 14:22:27 -05:00
_ , _ , activationErr := env . Activate ( pluginID )
2020-02-14 15:47:43 -05:00
require . NoError ( t , activationErr )
2021-02-25 14:22:27 -05:00
require . True ( t , th . App . GetPluginsEnvironment ( ) . IsActive ( pluginID ) )
2020-02-14 15:47:43 -05:00
user1 := & model . User {
Email : model . NewId ( ) + "success+test@example.com" ,
Nickname : "Darth Vader1" ,
Username : "vader" + model . NewId ( ) ,
Password : "passwd1" ,
AuthService : "" ,
}
2021-05-11 06:00:44 -04:00
_ , appErr := th . App . CreateUser ( th . Context , user1 )
2020-02-14 15:47:43 -05:00
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
2021-02-25 14:22:27 -05:00
require . True ( t , th . App . GetPluginsEnvironment ( ) . Deactivate ( pluginID ) )
require . False ( t , th . App . GetPluginsEnvironment ( ) . IsActive ( pluginID ) )
2020-02-14 15:47:43 -05:00
metricsMock . AssertExpectations ( t )
} )
}
2020-03-25 05:24:42 -04:00
func TestPluginAPIGetPostsForChannel ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-03-25 05:24:42 -04:00
require := require . New ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2020-03-25 05:24:42 -04:00
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 -- {
2025-11-12 07:00:51 -05:00
expectedPosts [ i ] = th . CreatePost ( t , th . BasicChannel )
2020-03-25 05:24:42 -04:00
}
// 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 ( ) )
}
2020-06-23 21:58:44 -04:00
2020-06-26 04:51:23 -04:00
func TestPluginHTTPConnHijack ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2020-06-26 04:51:23 -04:00
2024-05-15 11:05:13 -04:00
fullPath := filepath . Join ( server . GetPackagePath ( ) , "channels" , "app" , "plugin_api_tests" , "manual.test_http_hijack_plugin" , "main.go" )
2020-06-26 04:51:23 -04:00
2022-08-09 07:25:46 -04:00
pluginCode , err := os . ReadFile ( fullPath )
2020-06-26 04:51:23 -04:00
require . NoError ( t , err )
require . NotEmpty ( t , pluginCode )
2021-05-11 06:00:44 -04:00
tearDown , ids , errors := SetAppEnvironmentWithPlugins ( t , [ ] string { string ( pluginCode ) } , th . App , th . NewPluginAPI )
2020-06-26 04:51:23 -04:00
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 ( )
2022-08-09 07:25:46 -04:00
body , err := io . ReadAll ( resp . Body )
2020-06-26 04:51:23 -04:00
require . NoError ( t , err )
require . Equal ( t , "OK" , string ( body ) )
}
2025-03-11 13:44:42 -04:00
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 )
}
2025-02-17 01:50:21 -05:00
func TestPluginMFAEnforcement ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-02-17 01:50:21 -05:00
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
2025-11-12 07:00:51 -05:00
user := th . CreateUser ( t )
2025-02-17 01:50:21 -05:00
// 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
2025-03-11 13:44:42 -04:00
userID := makePluginHTTPRequest ( t , pluginID , th . Server . ListenAddr . Port , session . Token )
2025-02-17 01:50:21 -05:00
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
2025-03-11 13:44:42 -04:00
userID := makePluginHTTPRequest ( t , pluginID , th . Server . ListenAddr . Port , session . Token )
2025-02-17 01:50:21 -05:00
assert . Empty ( t , userID )
} )
}
2020-06-26 04:51:23 -04:00
func TestPluginHTTPUpgradeWebSocket ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2020-07-13 13:34:05 -04:00
th := Setup ( t )
2020-06-26 04:51:23 -04:00
2024-05-15 11:05:13 -04:00
fullPath := filepath . Join ( server . GetPackagePath ( ) , "channels" , "app" , "plugin_api_tests" , "manual.test_http_upgrade_websocket_plugin" , "main.go" )
2020-06-26 04:51:23 -04:00
2022-08-09 07:25:46 -04:00
pluginCode , err := os . ReadFile ( fullPath )
2020-06-26 04:51:23 -04:00
require . NoError ( t , err )
require . NotEmpty ( t , pluginCode )
2021-05-11 06:00:44 -04:00
tearDown , ids , errors := SetAppEnvironmentWithPlugins ( t , [ ] string { string ( pluginCode ) } , th . App , th . NewPluginAPI )
2020-06-26 04:51:23 -04:00
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 )
2021-08-13 07:12:16 -04:00
wsc , err := model . NewWebSocketClient ( reqURL , "" )
require . NoError ( t , err )
2020-06-26 04:51:23 -04:00
require . NotNil ( t , wsc )
wsc . Listen ( )
defer wsc . Close ( )
resp := <- wsc . ResponseChannel
2021-07-12 14:05:36 -04:00
require . Equal ( t , resp . Status , model . StatusOk )
2020-06-26 04:51:23 -04:00
2025-07-18 06:54:51 -04:00
for i := range 10 {
2022-07-05 02:46:50 -04:00
wsc . SendMessage ( "custom_action" , map [ string ] any { "value" : i } )
2020-06-26 04:51:23 -04:00
var resp * model . WebSocketResponse
select {
case resp = <- wsc . ResponseChannel :
2024-04-05 09:58:49 -04:00
case <- time . After ( 2 * time . Second ) :
2020-06-26 04:51:23 -04:00
}
require . NotNil ( t , resp )
2021-07-12 14:05:36 -04:00
require . Equal ( t , resp . Status , model . StatusOk )
2020-06-26 04:51:23 -04:00
require . Equal ( t , "custom_action" , resp . Data [ "action" ] )
require . Equal ( t , float64 ( i ) , resp . Data [ "value" ] )
}
}
2021-02-16 06:00:01 -05:00
type MockSlashCommandProvider struct {
Args * model . CommandArgs
Message string
}
func ( * MockSlashCommandProvider ) GetTrigger ( ) string {
return "mock"
}
2021-11-10 11:11:20 -05:00
2021-02-26 02:12:49 -05:00
func ( * MockSlashCommandProvider ) GetCommand ( a * App , T i18n . TranslateFunc ) * model . Command {
2021-02-16 06:00:01 -05:00
return & model . Command {
Trigger : "mock" ,
AutoComplete : true ,
AutoCompleteDesc : "mock" ,
AutoCompleteHint : "mock" ,
DisplayName : "mock" ,
}
}
2021-11-10 11:11:20 -05:00
2025-09-10 09:11:32 -04:00
func ( mscp * MockSlashCommandProvider ) DoCommand ( a * App , rctx request . CTX , args * model . CommandArgs , message string ) * model . CommandResponse {
2021-02-16 06:00:01 -05:00
mscp . Args = args
mscp . Message = message
return & model . CommandResponse {
Text : "mock" ,
2021-07-12 14:05:36 -04:00
ResponseType : model . CommandResponseTypeEphemeral ,
2021-02-16 06:00:01 -05:00
}
}
2020-07-07 12:31:03 -04:00
func TestPluginExecuteSlashCommand ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2020-07-07 12:31:03 -04:00
api := th . SetupPluginAPI ( )
2021-02-16 06:00:01 -05:00
slashCommandMock := & MockSlashCommandProvider { }
RegisterCommandProvider ( slashCommandMock )
2025-11-12 07:00:51 -05:00
newUser := th . CreateUser ( t )
th . LinkUserToTeam ( t , newUser , th . BasicTeam )
2020-07-07 12:31:03 -04:00
t . Run ( "run invite command" , func ( t * testing . T ) {
2021-02-16 06:00:01 -05:00
args := & model . CommandArgs {
Command : "/mock @" + newUser . Username ,
2020-07-07 12:31:03 -04:00
TeamId : th . BasicTeam . Id ,
UserId : th . BasicUser . Id ,
ChannelId : th . BasicChannel . Id ,
2021-02-16 06:00:01 -05:00
}
_ , err := api . ExecuteSlashCommand ( args )
2020-07-07 12:31:03 -04:00
require . NoError ( t , err )
2021-02-16 06:00:01 -05:00
require . Equal ( t , args , slashCommandMock . Args )
require . Equal ( t , "@" + newUser . Username , slashCommandMock . Message )
2020-07-07 12:31:03 -04:00
} )
}
2020-06-23 21:58:44 -04:00
func TestPluginAPISearchPostsInTeamByUser ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2020-06-23 21:58:44 -04:00
api := th . SetupPluginAPI ( )
basicPostText := & th . BasicPost . Message
2022-02-28 04:31:00 -05:00
unknownTerm := "Unknown Message"
2020-06-23 21:58:44 -04:00
testCases := [ ] struct {
description string
2021-02-05 05:22:27 -05:00
teamID string
userID string
2020-06-23 21:58:44 -04:00
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 ,
2022-02-28 04:31:00 -05:00
model . SearchParameter { Terms : & unknownTerm } ,
2020-06-23 21:58:44 -04:00
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 ) {
2021-02-05 05:22:27 -05:00
searchResults , err := api . SearchPostsInTeamForUser ( testCase . teamID , testCase . userID , testCase . params )
2020-06-23 21:58:44 -04:00
assert . Nil ( t , err )
assert . Equal ( t , testCase . expectedPostsLen , len ( searchResults . Posts ) )
} )
}
}
2020-07-31 11:40:15 -04:00
func TestPluginAPICreateCommandAndListCommands ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2020-07-31 11:40:15 -04:00
api := th . SetupPluginAPI ( )
2021-02-05 05:22:27 -05:00
foundCommand := func ( listXCommand func ( teamID string ) ( [ ] * model . Command , error ) ) bool {
2020-07-31 11:40:15 -04:00
cmds , appErr := listXCommand ( th . BasicTeam . Id )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
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 )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
newCmd , appErr := api . GetCommand ( cmd . Id )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
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 ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2020-07-31 11:40:15 -04:00
api := th . SetupPluginAPI ( )
cmd := & model . Command {
TeamId : th . BasicTeam . Id ,
Trigger : "testcmd" ,
Method : "G" ,
URL : "http://test.com/testcmd" ,
}
cmd , appErr := api . CreateCommand ( cmd )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
newCmd , appErr := api . GetCommand ( cmd . Id )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
require . Equal ( t , "pluginid" , newCmd . PluginId )
require . Equal ( t , "" , newCmd . CreatorId )
newCmd . Trigger = "NewTrigger"
newCmd . PluginId = "CannotChangeMe"
newCmd2 , appErr := api . UpdateCommand ( newCmd . Id , newCmd )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
require . Equal ( t , "pluginid" , newCmd2 . PluginId )
require . Equal ( t , "newtrigger" , newCmd2 . Trigger )
2025-11-12 07:00:51 -05:00
team1 := th . CreateTeam ( t )
2020-07-31 11:40:15 -04:00
newCmd2 . PluginId = "CannotChangeMe"
newCmd2 . Trigger = "anotherNewTrigger"
newCmd2 . TeamId = team1 . Id
newCmd3 , appErr := api . UpdateCommand ( newCmd2 . Id , newCmd2 )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
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 )
2021-02-16 06:00:01 -05:00
require . NoError ( t , appErr )
2020-07-31 11:40:15 -04:00
require . Equal ( t , "anothernewtriggeragain" , newCmd4 . Trigger )
require . Equal ( t , team1 . Id , newCmd4 . TeamId )
}
2021-10-20 06:12:58 -04:00
func TestPluginAPIIsEnterpriseReady ( t * testing . T ) {
oldValue := model . BuildEnterpriseReady
defer func ( ) { model . BuildEnterpriseReady = oldValue } ( )
model . BuildEnterpriseReady = "true"
th := Setup ( t )
2025-11-12 07:00:51 -05:00
2021-10-20 06:12:58 -04:00
api := th . SetupPluginAPI ( )
assert . Equal ( t , true , api . IsEnterpriseReady ( ) )
}
2022-11-03 14:43:30 -04:00
2022-11-22 16:26:22 -05:00
func TestPluginUploadsAPI ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2022-11-22 16:26:22 -05:00
pluginCode := fmt . Sprintf ( `
package main
import (
"fmt"
"bytes"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
2022-11-22 16:26:22 -05:00
)
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 )
}
2023-05-03 15:04:10 -04:00
env , err := plugin . NewEnvironment ( newPluginAPI , NewDriverImpl ( th . App . Srv ( ) ) , pluginDir , webappPluginDir , th . App . Log ( ) , nil )
2022-11-22 16:26:22 -05:00
require . NoError ( t , err )
2022-12-21 14:10:26 -05:00
th . App . ch . SetPluginsEnvironment ( env )
2022-11-22 16:26:22 -05:00
pluginID := "testplugin"
pluginManifest := ` { "id": "testplugin", "server": { "executable": "backend.exe"}} `
backend := filepath . Join ( pluginDir , pluginID , "backend.exe" )
utils . CompileGo ( t , pluginCode , backend )
2024-11-12 15:56:35 -05:00
err = os . WriteFile ( filepath . Join ( pluginDir , pluginID , "plugin.json" ) , [ ] byte ( pluginManifest ) , 0600 )
require . NoError ( t , err )
2022-11-22 16:26:22 -05:00
manifest , activated , reterr := env . Activate ( pluginID )
require . NoError ( t , reterr )
require . NotNil ( t , manifest )
require . True ( t , activated )
}
2023-06-12 20:23:02 -04:00
//go:embed plugin_api_tests/manual.test_configuration_will_be_saved_hook/main.tmpl
var configurationWillBeSavedHookTemplate string
func TestConfigurationWillBeSavedHook ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2023-06-12 20:23:02 -04:00
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 )
2024-11-12 15:56:35 -05:00
err = os . WriteFile ( filepath . Join ( pluginDir , pluginID , "plugin.json" ) , [ ] byte ( pluginManifest ) , 0600 )
require . NoError ( t , err )
2023-06-12 20:23:02 -04:00
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" ] )
} )
}
2023-08-18 13:05:26 -04:00
func TestSendPushNotification ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2023-08-18 13:05:26 -04:00
if testing . Short ( ) {
t . Skip ( "skipping TestSendPushNotification test in short mode" )
}
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2023-08-18 13:05:26 -04:00
api := th . SetupPluginAPI ( )
// Create 3 users, each having 2 sessions.
type userSession struct {
user * model . User
session * model . Session
}
var userSessions [ ] userSession
2025-07-18 06:54:51 -04:00
for range 3 {
2025-11-12 07:00:51 -05:00
u := th . CreateUser ( t )
2023-10-11 07:08:55 -04:00
sess , err := th . App . CreateSession ( th . Context , & model . Session {
2023-08-18 13:05:26 -04:00
UserId : u . Id ,
DeviceId : "deviceID" + u . Id ,
ExpiresAt : model . GetMillis ( ) + 100000 ,
} )
require . Nil ( t , err )
// We don't need to track the 2nd session.
2023-10-11 07:08:55 -04:00
_ , err = th . App . CreateSession ( th . Context , & model . Session {
2023-08-18 13:05:26 -04:00
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 )
2025-11-12 07:00:51 -05:00
th . AddUserToChannel ( t , u , th . BasicChannel )
2023-08-18 13:05:26 -04:00
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 ( )
2025-11-12 07:00:51 -05:00
post := th . CreatePost ( t , th . BasicChannel )
2023-08-18 13:05:26 -04:00
post . Message = "started a conversation"
2023-08-24 12:33:53 -04:00
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" ,
2023-08-18 13:05:26 -04:00
}
2023-08-24 12:33:53 -04:00
appErr := api . SendPushNotification ( notification , user . Id )
require . Nil ( t , appErr )
2023-08-18 13:05:26 -04:00
} ( * 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 )
2023-08-24 12:33:53 -04:00
assert . Equal ( t , "Custom message" , n . Message )
2023-08-18 13:05:26 -04:00
default :
assert . Fail ( t , "should not receive any other push notification types" )
}
}
assert . Equal ( t , 6 , numMessages )
}
2023-11-17 15:39:06 -05:00
func TestPluginServeMetrics ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2023-11-17 15:39:06 -05:00
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
2024-08-05 23:45:00 -04:00
cfg . MetricsSettings . Enable = model . NewPointer ( true )
cfg . MetricsSettings . ListenAddress = model . NewPointer ( ":30067" )
2023-11-17 15:39:06 -05:00
} )
defer th . App . UpdateConfig ( func ( cfg * model . Config ) {
cfg . MetricsSettings . Enable = prevEnable
cfg . MetricsSettings . ListenAddress = prevAddress
} )
2024-05-15 11:05:13 -04:00
fullPath := filepath . Join ( server . GetPackagePath ( ) , "channels" , "app" , "plugin_api_tests" , "manual.test_serve_metrics_plugin" , "main.go" )
2023-11-17 15:39:06 -05:00
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 ) )
}
2024-01-11 13:24:52 -05:00
2024-02-12 06:12:05 -05:00
func TestPluginGetChannelsForTeamForUser ( t * testing . T ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2024-01-11 13:24:52 -05:00
2025-11-12 07:00:51 -05:00
user := th . CreateUser ( t )
2024-01-11 13:24:52 -05:00
2025-11-12 07:00:51 -05:00
team1 := th . CreateTeam ( t )
th . LinkUserToTeam ( t , user , team1 )
team2 := th . CreateTeam ( t )
th . LinkUserToTeam ( t , user , team2 )
2024-01-11 13:24:52 -05:00
2025-11-12 07:00:51 -05:00
channel1 := th . CreateChannel ( t , team1 )
th . AddUserToChannel ( t , user , channel1 )
channel2 := th . CreateChannel ( t , team2 )
th . AddUserToChannel ( t , user , channel2 )
2024-01-11 13:24:52 -05:00
2025-11-12 07:00:51 -05:00
dmChannel := th . CreateDmChannel ( t , user )
2024-01-11 13:24:52 -05:00
2024-02-12 06:12:05 -05:00
pluginCode := `
package main
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/pkg/errors"
)
2024-01-11 13:24:52 -05:00
2024-02-12 06:12:05 -05:00
const (
userID = "` + user.Id + `"
teamID1 = "` + team1.Id + `"
teamID2 = "` + team2.Id + `"
channelID1 = "` + channel1.Id + `"
channelID2 = "` + channel2.Id + `"
dmChannelID = "` + dmChannel.Id + `"
)
2024-01-11 13:24:52 -05:00
2024-02-12 06:12:05 -05:00
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"
}
2024-01-11 13:24:52 -05:00
2024-02-12 06:12:05 -05:00
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 ) {
2025-05-30 07:58:26 -04:00
mainHelper . Parallel ( t )
2024-02-12 06:12:05 -05:00
t . Run ( "should be able to set fields for multiple members" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2024-01-11 13:24:52 -05:00
2025-11-12 07:00:51 -05:00
channel := th . CreateChannel ( t , th . BasicTeam )
th . AddUserToChannel ( t , th . BasicUser , channel )
th . AddUserToChannel ( t , th . BasicUser2 , channel )
2024-01-11 13:24:52 -05:00
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 ] )
} )
2024-02-12 06:12:05 -05:00
t . Run ( "should be able to clear a field" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2024-02-12 06:12:05 -05:00
2025-11-12 07:00:51 -05:00
channel := th . CreateChannel ( t , th . BasicTeam )
th . AddUserToChannel ( t , th . BasicUser , channel )
2024-02-12 06:12:05 -05:00
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" ] )
} )
2024-01-11 13:24:52 -05:00
}
2025-03-11 13:44:42 -04:00
func TestPluginServeHTTPCompatibility ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-03-11 13:44:42 -04:00
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 { } )
}
`
2025-07-18 06:54:51 -04:00
for goVersion := range strings . FieldsSeq ( os . Getenv ( "GO_COMPATIBILITY_TEST_VERSIONS" ) ) {
2025-03-11 13:44:42 -04:00
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 )
} )
}
}
2025-09-11 15:49:14 -04:00
func TestPluginAPICreatePropertyField ( t * testing . T ) {
mainHelper . Parallel ( t )
t . Run ( "should allow creation after deleting fields" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-09-11 15:49:14 -04:00
api := th . SetupPluginAPI ( )
// Create 20 property fields
groupID := model . NewId ( )
var createdFields [ ] * model . PropertyField
for i := 1 ; i <= 20 ; i ++ {
field := & model . PropertyField {
GroupID : groupID ,
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 )
}
// Delete one field
err := api . DeletePropertyField ( groupID , createdFields [ 0 ] . ID )
require . NoError ( t , err )
// Should now be able to create another field
newField := & model . PropertyField {
GroupID : groupID ,
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 ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-09-11 15:49:14 -04:00
api := th . SetupPluginAPI ( )
groupID := model . NewId ( )
// Create and delete 5 fields
for i := 1 ; i <= 5 ; i ++ {
field := & model . PropertyField {
GroupID : groupID ,
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 ( groupID , created . ID )
require . NoError ( t , err )
}
// Should be able to create multiple active fields
for i := 1 ; i <= 20 ; i ++ {
field := & model . PropertyField {
GroupID : groupID ,
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 ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-09-11 15:49:14 -04:00
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 )
assert . Contains ( t , err . Error ( ) , "group_id" )
// 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 ( ) , "invalid input: property field parameter is required" )
} )
}
func TestPluginAPICountPropertyFields ( t * testing . T ) {
mainHelper . Parallel ( t )
t . Run ( "should count active property fields only" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-09-11 15:49:14 -04:00
api := th . SetupPluginAPI ( )
groupID := model . NewId ( )
// Create 5 fields
var createdFields [ ] * model . PropertyField
for i := 1 ; i <= 5 ; i ++ {
field := & model . PropertyField {
GroupID : groupID ,
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 ( groupID , false )
require . NoError ( t , err )
assert . Equal ( t , int64 ( 5 ) , count )
// Delete 2 fields
err = api . DeletePropertyField ( groupID , createdFields [ 0 ] . ID )
require . NoError ( t , err )
err = api . DeletePropertyField ( groupID , createdFields [ 1 ] . ID )
require . NoError ( t , err )
// Count should now be 3
count , err = api . CountPropertyFields ( groupID , false )
require . NoError ( t , err )
assert . Equal ( t , int64 ( 3 ) , count )
} )
t . Run ( "should count all property fields including deleted" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-09-11 15:49:14 -04:00
api := th . SetupPluginAPI ( )
groupID := model . NewId ( )
// Create 5 fields
var createdFields [ ] * model . PropertyField
for i := 1 ; i <= 5 ; i ++ {
field := & model . PropertyField {
GroupID : groupID ,
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 ( groupID , true )
require . NoError ( t , err )
assert . Equal ( t , int64 ( 5 ) , count )
// Delete 2 fields
err = api . DeletePropertyField ( groupID , createdFields [ 0 ] . ID )
require . NoError ( t , err )
err = api . DeletePropertyField ( groupID , createdFields [ 1 ] . ID )
require . NoError ( t , err )
// Count all should still be 5
count , err = api . CountPropertyFields ( groupID , true )
require . NoError ( t , err )
assert . Equal ( t , int64 ( 5 ) , count )
// Count active should be 3
count , err = api . CountPropertyFields ( groupID , false )
require . NoError ( t , err )
assert . Equal ( t , int64 ( 3 ) , count )
} )
t . Run ( "should return 0 for empty group" , func ( t * testing . T ) {
2025-11-12 07:00:51 -05:00
th := Setup ( t ) . InitBasic ( t )
2025-09-11 15:49:14 -04:00
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 )
} )
}