Files
mattermost/server/channels/api4/plugin_test.go
T
Jesse Hallam 8a267c8aba MM-69104: Elide plugin_statuses_changed payload (#36966)
* MM-69104: signal plugin_statuses_changed without a payload, refetch on demand

The plugin_statuses_changed websocket event broadcast the entire cluster-wide
plugin status slice over the best-effort (UDP) cluster transport. With enough
installed plugins the serialized payload exceeded the gossip datagram size and
was silently dropped with EMSGSIZE ("message too long"), leaving admin clients
on peer nodes with stale plugin state during plugin reinstall/restart churn.

The event is admin-only and only feeds the System Console plugin views, so
rather than push the whole slice, publish it as a payload-free signal sent
reliably over the cluster. The Plugin Management and Marketplace views subscribe
via the useWebSocket hook and debounce a refetch of a full, cluster-wide status
from the existing REST endpoint, pulling the data on demand only while a relevant
view is mounted.

* MM-69104: keep plugin_statuses as an empty array for client compatibility

Some clients call array methods on plugin_statuses (e.g. the Boards/Focalboard
plugin does data.plugin_statuses.find(...)). Omitting the field entirely makes that
throw, so send a non-nil empty slice that serializes as [] rather than null. The
payload stays effectively empty, so the cluster-broadcast size fix is preserved.

* fixup! MM-69104: signal plugin_statuses_changed without a payload, refetch on demand

* Apply suggestions from code review

Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>

* fixup! MM-69104: signal plugin_statuses_changed without a payload, refetch on demand

use shared useDebounce hook in usePluginStatusesSync

* fixup! MM-69104: signal plugin_statuses_changed without a payload, refetch on demand

trim docstring

* fixup! MM-69104: signal plugin_statuses_changed without a payload, refetch on demand

return pluginStatuses from hook

* fixup! MM-69104: signal plugin_statuses_changed without a payload, refetch on demand

cover hook return value

---------

Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
2026-06-10 13:07:53 -03:00

2185 lines
75 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"time"
svg "github.com/h2non/go-is-svg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/plugin/utils"
"github.com/mattermost/mattermost/server/v8"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
)
func TestPlugin(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
statesJson, err := json.Marshal(th.App.Config().PluginSettings.PluginStates)
require.NoError(t, err)
states := map[string]*model.PluginState{}
err = json.Unmarshal(statesJson, &states)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.AllowInsecureDownloadURL = true
})
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
// Install from URL
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
_, err = res.Write(tarData)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
url := testServer.URL
manifest, _, err := client.InstallPluginFromURL(context.Background(), url, false)
require.NoError(t, err)
assert.Equal(t, "testplugin", manifest.Id)
_, resp, err := client.InstallPluginFromURL(context.Background(), url, false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
manifest, _, err = client.InstallPluginFromURL(context.Background(), url, true)
require.NoError(t, err)
assert.Equal(t, "testplugin", manifest.Id)
// Stored in File Store: Install Plugin from URL case
pluginStored, appErr := th.App.FileExists("./plugins/" + manifest.Id + ".tar.gz")
assert.Nil(t, appErr)
assert.True(t, pluginStored)
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
_, resp, err = client.InstallPluginFromURL(context.Background(), url, false)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true })
_, resp, err = th.Client.InstallPluginFromURL(context.Background(), url, false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.InstallPluginFromURL(context.Background(), "http://nodata", false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.AllowInsecureDownloadURL = false })
_, resp, err = client.InstallPluginFromURL(context.Background(), url, false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Successful upload
manifest, _, err = client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
assert.Equal(t, "testplugin", manifest.Id)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true })
manifest, _, err = client.UploadPluginForced(context.Background(), bytes.NewReader(tarData))
defer os.RemoveAll("plugins/testplugin")
require.NoError(t, err)
assert.Equal(t, "testplugin", manifest.Id)
// Stored in File Store: Upload Plugin case
pluginStored, appErr = th.App.FileExists("./plugins/" + manifest.Id + ".tar.gz")
assert.Nil(t, appErr)
assert.True(t, pluginStored)
// Upload error cases
_, resp, err = client.UploadPlugin(context.Background(), bytes.NewReader([]byte("badfile")))
require.Error(t, err)
CheckBadRequestStatus(t, resp)
plugin_sz := int64(111 * 1024 * 1024)
fd, err := os.Create(filepath.Join(path, "big_testplugin.tar.gz"))
require.NoError(t, err)
_, err = fd.Seek(plugin_sz-1, 0)
require.NoError(t, err)
_, err = fd.Write([]byte{0})
require.NoError(t, err)
err = fd.Close()
require.NoError(t, err)
bigData, err := os.ReadFile(filepath.Join(path, "big_testplugin.tar.gz"))
require.NoError(t, err)
_, resp, err = client.UploadPlugin(context.Background(), bytes.NewReader(bigData))
require.Error(t, err)
CheckRequestEntityTooLargeStatus(t, resp)
err = os.Remove(filepath.Join(path, "big_testplugin.tar.gz"))
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
_, resp, err = client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = false
})
_, resp, err = client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
_, resp, err = client.InstallPluginFromURL(context.Background(), url, false)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true })
_, resp, err = th.Client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Successful gets
pluginsResp, _, err := client.GetPlugins(context.Background())
require.NoError(t, err)
found := false
for _, m := range pluginsResp.Inactive {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
found = false
for _, m := range pluginsResp.Active {
if m.Id == manifest.Id {
found = true
}
}
assert.False(t, found)
// Successful activate
_, err = client.EnablePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
found = false
for _, m := range pluginsResp.Active {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
// Activate error case
resp, err = client.EnablePlugin(context.Background(), "junk")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
resp, err = client.EnablePlugin(context.Background(), "JUNK")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Successful deactivate
_, err = client.DisablePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
found = false
for _, m := range pluginsResp.Inactive {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
// Deactivate error case
resp, err = client.DisablePlugin(context.Background(), "junk")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Get error cases
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
_, resp, err = client.GetPlugins(context.Background())
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true })
_, resp, err = th.Client.GetPlugins(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Successful webapp get
_, err = client.EnablePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
manifests, _, err := th.Client.GetWebappPlugins(context.Background())
require.NoError(t, err)
found = false
for _, m := range manifests {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
// Successful remove
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
// Remove error cases
resp, err = client.RemovePlugin(context.Background(), manifest.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
resp, err = client.RemovePlugin(context.Background(), manifest.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true })
resp, err = th.Client.RemovePlugin(context.Background(), manifest.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = client.RemovePlugin(context.Background(), "bad.id")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
}
func TestPluginInstallDirectoryConflict(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
})
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
t.Run("plugin directory is subdirectory of import directory", func(t *testing.T) {
originalPluginDir := *th.App.Config().PluginSettings.Directory
originalImportDir := *th.App.Config().ImportSettings.Directory
// Use non-existent paths to test conflict detection without filesystem dependencies
importDir := "/nonexistent/conflict-test/data"
pluginDir := "/nonexistent/conflict-test/data/plugins"
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Directory = originalPluginDir
*cfg.ImportSettings.Directory = originalImportDir
})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ImportSettings.Directory = importDir
*cfg.PluginSettings.Directory = pluginDir
})
_, resp, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckForbiddenStatus(t, resp)
CheckErrorID(t, err, "api.plugin.install.directory_conflict.app_error")
})
t.Run("import directory is subdirectory of plugin directory", func(t *testing.T) {
originalPluginDir := *th.App.Config().PluginSettings.Directory
originalImportDir := *th.App.Config().ImportSettings.Directory
// Use non-existent paths to test conflict detection without filesystem dependencies
pluginDir := "/nonexistent/conflict-test/plugins"
importDir := "/nonexistent/conflict-test/plugins/imports"
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Directory = originalPluginDir
*cfg.ImportSettings.Directory = originalImportDir
})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Directory = pluginDir
*cfg.ImportSettings.Directory = importDir
})
_, resp, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckForbiddenStatus(t, resp)
CheckErrorID(t, err, "api.plugin.install.directory_conflict.app_error")
})
t.Run("same directory", func(t *testing.T) {
originalPluginDir := *th.App.Config().PluginSettings.Directory
originalImportDir := *th.App.Config().ImportSettings.Directory
// Use non-existent path to test conflict detection without filesystem dependencies
sharedDir := "/nonexistent/conflict-test/shared"
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Directory = originalPluginDir
*cfg.ImportSettings.Directory = originalImportDir
})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Directory = sharedDir
*cfg.ImportSettings.Directory = sharedDir
})
_, resp, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckForbiddenStatus(t, resp)
CheckErrorID(t, err, "api.plugin.install.directory_conflict.app_error")
})
t.Run("separate directories should succeed", func(t *testing.T) {
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
assert.Equal(t, "testplugin", manifest.Id)
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
}
func TestNotifyClusterPluginEvent(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
testCluster := &testlib.FakeClusterInterface{}
th.Server.Platform().SetCluster(testCluster)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
})
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
testCluster.ClearMessages()
// Successful upload
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
require.Equal(t, "testplugin", manifest.Id)
// Stored in File Store: Upload Plugin case
expectedPath := filepath.Join("./plugins", manifest.Id) + ".tar.gz"
pluginStored, appErr := th.App.FileExists(expectedPath)
require.Nil(t, appErr)
require.True(t, pluginStored)
messages := testCluster.GetMessages()
expectedPluginData := model.PluginEventData{
Id: manifest.Id,
}
buf, _ := json.Marshal(expectedPluginData)
expectedInstallMessage := &model.ClusterMessage{
Event: model.ClusterEventInstallPlugin,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
}
actualMessages := findClusterMessages(model.ClusterEventInstallPlugin, messages)
require.Equal(t, []*model.ClusterMessage{expectedInstallMessage}, actualMessages)
// Upgrade
testCluster.ClearMessages()
manifest, _, err = th.SystemAdminClient.UploadPluginForced(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
require.Equal(t, "testplugin", manifest.Id)
// Successful remove
webSocketClient := th.CreateConnectedWebSocketClientWithClient(t, th.SystemAdminClient)
var statusesPresent bool
var receivedStatuses any
done := make(chan bool)
go func() {
for {
select {
case resp := <-webSocketClient.EventChannel:
// The event only signals admins to refetch statuses; it carries an empty
// plugin_statuses array (compatibility shim), not the full slice.
if resp.EventType() == model.WebsocketEventPluginStatusesChanged {
receivedStatuses, statusesPresent = resp.GetData()["plugin_statuses"]
done <- true
return
}
case <-time.After(5 * time.Second):
done <- false
return
}
}
}()
testCluster.ClearMessages()
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
result := <-done
require.True(t, result, "plugin_statuses_changed websocket event was not received")
require.True(t, statusesPresent, "plugin_statuses field should be present")
assert.Empty(t, receivedStatuses, "plugin_statuses should be an empty array")
messages = testCluster.GetMessages()
expectedRemoveMessage := &model.ClusterMessage{
Event: model.ClusterEventRemovePlugin,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
}
actualMessages = findClusterMessages(model.ClusterEventRemovePlugin, messages)
require.Equal(t, []*model.ClusterMessage{expectedRemoveMessage}, actualMessages)
pluginStored, appErr = th.App.FileExists(expectedPath)
require.Nil(t, appErr)
require.False(t, pluginStored)
}
func TestDisableOnRemove(t *testing.T) {
mainHelper.Parallel(t)
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
testCases := []struct {
Description string
Upgrade bool
}{
{
"Remove without upgrading",
false,
},
{
"Remove after upgrading",
true,
},
}
th := Setup(t).InitBasic(t)
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
})
// Upload
manifest, _, err := client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
require.Equal(t, "testplugin", manifest.Id)
// Check initial status
pluginsResp, _, err := client.GetPlugins(context.Background())
require.NoError(t, err)
require.Empty(t, pluginsResp.Active)
require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{
Manifest: *manifest,
}})
// Enable plugin
_, err = client.EnablePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
// Confirm enabled status
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Empty(t, pluginsResp.Inactive)
require.Equal(t, pluginsResp.Active, []*model.PluginInfo{{
Manifest: *manifest,
}})
if tc.Upgrade {
// Upgrade
manifest, _, err = client.UploadPluginForced(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
require.Equal(t, "testplugin", manifest.Id)
// Plugin should remain active
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Empty(t, pluginsResp.Inactive)
require.Equal(t, pluginsResp.Active, []*model.PluginInfo{{
Manifest: *manifest,
}})
}
// Remove plugin
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
// Plugin should have no status
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Empty(t, pluginsResp.Inactive)
require.Empty(t, pluginsResp.Active)
// Upload same plugin
manifest, _, err = client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
require.Equal(t, "testplugin", manifest.Id)
// Plugin should be inactive
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Empty(t, pluginsResp.Active)
require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{
Manifest: *manifest,
}})
// Clean up
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
})
}
}
func TestGetMarketplacePlugins(t *testing.T) {
th := Setup(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.EnableMarketplace = false
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = false
*cfg.PluginSettings.MarketplaceURL = "invalid.com"
})
plugins, resp, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
require.Nil(t, plugins)
}, "marketplace disabled")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = "invalid.com"
})
plugins, resp, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, plugins)
}, "no server")
t.Run("no permission", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = "invalid.com"
})
plugins, resp, err := th.Client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
require.Nil(t, plugins)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "empty response from server")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
serverVersion, ok := req.URL.Query()["server_version"]
require.True(t, ok)
require.Len(t, serverVersion, 1)
require.Equal(t, model.CurrentVersion, serverVersion[0])
require.NotEqual(t, 0, len(serverVersion[0]))
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "verify server version is passed through")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
licenseType, ok := req.URL.Query()["enterprise_plugins"]
require.True(t, ok)
require.Len(t, licenseType, 1)
require.Equal(t, "false", licenseType[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "verify EnterprisePlugins is false for TE")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
licenseType, ok := req.URL.Query()["enterprise_plugins"]
require.True(t, ok)
require.Len(t, licenseType, 1)
require.Equal(t, "false", licenseType[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
l := model.NewTestLicense()
// model.NewTestLicense generates a E20 license
*l.Features.EnterprisePlugins = false
th.App.Srv().SetLicense(l)
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "verify EnterprisePlugins is false for E10")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
licenseType, ok := req.URL.Query()["enterprise_plugins"]
require.True(t, ok)
require.Len(t, licenseType, 1)
require.Equal(t, "true", licenseType[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
th.App.Srv().SetLicense(model.NewTestLicense("enterprise_plugins"))
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "verify EnterprisePlugins is true for E20")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
cloud, ok := req.URL.Query()["cloud"]
require.True(t, ok)
require.Len(t, cloud, 1)
require.Equal(t, "false", cloud[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "verify EnterprisePlugins is false if there is no license")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
cloud, ok := req.URL.Query()["cloud"]
require.True(t, ok)
require.Len(t, cloud, 1)
require.Equal(t, "true", cloud[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
plugins, _, err := client.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Empty(t, plugins)
}, "verify Cloud is true for cloud license")
}
func TestGetInstalledMarketplacePlugins(t *testing.T) {
samplePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: "https://example.com/mattermost/mattermost-plugin-nps/releases/download/v1.0.4/com.mattermost.nps-1.0.4.tar.gz",
Labels: []model.MarketplaceLabel{
{
Name: "someName",
Description: "some Description",
},
},
Manifest: &model.Manifest{
Id: "com.mattermost.nps",
Name: "User Satisfaction Surveys",
Description: "This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost.",
Version: "1.0.4",
MinServerVersion: "5.14.0",
},
},
InstalledVersion: "",
},
}
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
t.Run("marketplace client returns not-installed plugin", func(t *testing.T) {
th := Setup(t)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal(samplePlugins)
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Equal(t, samplePlugins, plugins)
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
testIcon, err := os.ReadFile(filepath.Join(path, "test.svg"))
require.NoError(t, err)
require.True(t, svg.Is(testIcon))
testIconData := fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(testIcon))
expectedPlugins := append(samplePlugins, &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/homepage",
IconData: testIconData,
DownloadURL: "",
ReleaseNotesURL: "https://example.com/releases/v0.0.1",
Labels: []model.MarketplaceLabel{{
Name: "Local",
Description: "This plugin is not listed in the marketplace",
}},
Manifest: manifest,
},
InstalledVersion: manifest.Version,
})
sort.SliceStable(expectedPlugins, func(i, j int) bool {
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
})
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Equal(t, expectedPlugins, plugins)
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Equal(t, samplePlugins, plugins)
})
t.Run("marketplace client returns installed plugin", func(t *testing.T) {
th := Setup(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.EnableMarketplace = true
})
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
newPlugin := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "HomepageURL",
IconData: "IconData",
DownloadURL: "DownloadURL",
Manifest: manifest,
},
InstalledVersion: manifest.Version,
}
expectedPlugins := append(samplePlugins, newPlugin)
sort.SliceStable(expectedPlugins, func(i, j int) bool {
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
})
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
var out []byte
out, err = json.Marshal([]*model.MarketplacePlugin{samplePlugins[0], newPlugin})
require.NoError(t, err)
_, err = res.Write(out)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Equal(t, expectedPlugins, plugins)
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
newPlugin.InstalledVersion = ""
require.Equal(t, expectedPlugins, plugins)
})
}
func TestSearchGetMarketplacePlugins(t *testing.T) {
samplePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "example.com/mattermost/mattermost-plugin-nps",
IconData: "Cjxzdmcgdmlld0JveD0nMCAwIDEwNSA5MycgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz4KPHBhdGggZD0nTTY2LDBoMzl2OTN6TTM4LDBoLTM4djkzek01MiwzNWwyNSw1OGgtMTZsLTgtMThoLTE4eicgZmlsbD0nI0VEMUMyNCcvPgo8L3N2Zz4K",
DownloadURL: "example.com/mattermost/mattermost-plugin-nps/releases/download/v1.0.4/com.mattermost.nps-1.0.4.tar.gz",
Manifest: &model.Manifest{
Id: "com.mattermost.nps",
Name: "User Satisfaction Surveys",
Description: "This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost.",
Version: "1.0.4",
MinServerVersion: "5.14.0",
},
},
InstalledVersion: "",
},
}
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
tarDataV2, err := os.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
require.NoError(t, err)
testIcon, err := os.ReadFile(filepath.Join(path, "test.svg"))
require.NoError(t, err)
require.True(t, svg.Is(testIcon))
testIconData := fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(testIcon))
t.Run("search installed plugin", func(t *testing.T) {
th := Setup(t).InitBasic(t)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal(samplePlugins)
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Equal(t, samplePlugins, plugins)
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
plugin1 := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/homepage",
IconData: testIconData,
DownloadURL: "",
ReleaseNotesURL: "https://example.com/releases/v0.0.1",
Labels: []model.MarketplaceLabel{{
Name: "Local",
Description: "This plugin is not listed in the marketplace",
}},
Manifest: manifest,
},
InstalledVersion: manifest.Version,
}
expectedPlugins := append(samplePlugins, plugin1)
manifest, _, err = th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarDataV2))
require.NoError(t, err)
plugin2 := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
IconData: testIconData,
HomepageURL: "https://example.com/homepage",
DownloadURL: "",
ReleaseNotesURL: "https://example.com/releases/v1.2.3",
Labels: []model.MarketplaceLabel{{
Name: "Local",
Description: "This plugin is not listed in the marketplace",
}},
Manifest: manifest,
},
InstalledVersion: manifest.Version,
}
expectedPlugins = append(expectedPlugins, plugin2)
sort.SliceStable(expectedPlugins, func(i, j int) bool {
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
})
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Equal(t, expectedPlugins, plugins)
// Search for plugins from the server
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{Filter: "testplugin2"})
require.NoError(t, err)
require.Equal(t, []*model.MarketplacePlugin{plugin2}, plugins)
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{Filter: "a second plugin"})
require.NoError(t, err)
require.Equal(t, []*model.MarketplacePlugin{plugin2}, plugins)
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{Filter: "User Satisfaction Surveys"})
require.NoError(t, err)
require.Equal(t, samplePlugins, plugins)
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{Filter: "NOFILTER"})
require.NoError(t, err)
require.Nil(t, plugins)
// cleanup
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), plugin1.Manifest.Id)
require.NoError(t, err)
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), plugin2.Manifest.Id)
require.NoError(t, err)
})
}
func TestGetLocalPluginInMarketplace(t *testing.T) {
th := Setup(t)
samplePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: "www.github.com/example",
Manifest: &model.Manifest{
Id: "testplugin2",
Name: "testplugin2",
Description: "a second plugin",
Version: "1.2.2",
MinServerVersion: "",
},
},
InstalledVersion: "",
},
}
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
t.Run("Get plugins with EnableRemoteMarketplace enabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = true
})
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Len(t, plugins, len(samplePlugins))
require.Equal(t, samplePlugins, plugins)
})
t.Run("get remote and local plugins", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.EnableUploads = true
})
// Upload one local plugin
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Len(t, plugins, 2)
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
t.Run("EnableRemoteMarketplace disabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = false
*cfg.PluginSettings.EnableUploads = true
})
// No marketplace plugins returned
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Len(t, plugins, 0)
// Upload one local plugin
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
testIcon, err := os.ReadFile(filepath.Join(path, "test.svg"))
require.NoError(t, err)
require.True(t, svg.Is(testIcon))
testIconData := fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(testIcon))
newPlugin := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
IconData: testIconData,
HomepageURL: "https://example.com/homepage",
ReleaseNotesURL: "https://example.com/releases/v0.0.1",
Manifest: manifest,
},
InstalledVersion: manifest.Version,
}
plugins, _, err = th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
// Only get the local plugins
require.Len(t, plugins, 1)
require.Equal(t, newPlugin, plugins[0])
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
t.Run("local_only true", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.EnableUploads = true
})
// Upload one local plugin
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
testIcon, err := os.ReadFile(filepath.Join(path, "test.svg"))
require.NoError(t, err)
require.True(t, svg.Is(testIcon))
testIconData := fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(testIcon))
newPlugin := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
Manifest: manifest,
IconData: testIconData,
HomepageURL: "https://example.com/homepage",
ReleaseNotesURL: "https://example.com/releases/v0.0.1",
Labels: []model.MarketplaceLabel{{
Name: "Local",
Description: "This plugin is not listed in the marketplace",
}},
},
InstalledVersion: manifest.Version,
}
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{LocalOnly: true})
require.NoError(t, err)
require.Len(t, plugins, 1)
require.Equal(t, newPlugin, plugins[0])
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
}
func TestGetRemotePluginInMarketplace(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
samplePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: "www.github.com/example",
Manifest: &model.Manifest{
Id: "testplugin2",
Name: "testplugin2",
Description: "a second plugin",
Version: "1.2.2",
MinServerVersion: "",
},
},
InstalledVersion: "",
},
}
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
// Upload one local plugin
path, _ := fileutils.FindDir("tests")
tarData, err := os.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
manifest, _, err := th.SystemAdminClient.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.NoError(t, err)
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{RemoteOnly: true})
require.NoError(t, err)
require.Len(t, plugins, 1)
require.Equal(t, samplePlugins[0], plugins[0])
_, err = th.SystemAdminClient.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
}
func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
th := Setup(t)
marketplacePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: "www.github.com/example",
Manifest: &model.Manifest{
Id: "marketplace.test",
Name: "marketplacetest",
Description: "a marketplace plugin",
Version: "0.1.2",
MinServerVersion: "",
},
},
InstalledVersion: "",
},
}
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{marketplacePlugins[0]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
prepackagePlugin := &plugin.PrepackagedPlugin{
Manifest: &model.Manifest{
Version: "0.0.1",
Id: "prepackaged.test",
},
}
env := th.App.GetPluginsEnvironment()
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil)
t.Run("prepackaged plugins are shown in Cloud", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.EnableUploads = true
})
lic := th.App.Srv().License()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
defer th.App.Srv().SetLicense(lic)
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
expectedPlugins := marketplacePlugins
expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
Manifest: prepackagePlugin.Manifest,
},
})
require.ElementsMatch(t, expectedPlugins, plugins)
require.Len(t, plugins, 2)
})
t.Run("get remote and prepackaged plugins", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.EnableUploads = true
})
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
expectedPlugins := marketplacePlugins
expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
Manifest: prepackagePlugin.Manifest,
},
})
require.ElementsMatch(t, expectedPlugins, plugins)
require.Len(t, plugins, 2)
})
t.Run("EnableRemoteMarketplace disabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = false
*cfg.PluginSettings.EnableUploads = true
})
// No marketplace plugins returned
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
// Only returns the prepackaged plugins
require.Len(t, plugins, 1)
require.Equal(t, prepackagePlugin.Manifest, plugins[0].Manifest)
})
t.Run("get prepackaged plugin if newer", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.EnableUploads = true
})
manifest := &model.Manifest{
Version: "1.2.3",
Id: "marketplace.test",
}
newerPrepackagePlugin := &plugin.PrepackagedPlugin{
Manifest: manifest,
}
env := th.App.GetPluginsEnvironment()
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin}, nil)
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
require.Len(t, plugins, 1)
require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest)
})
}
func TestInstallMarketplacePlugin(t *testing.T) {
path, _ := fileutils.FindDir("tests")
th := SetupConfig(t, func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.EnableMarketplace = false
cfg.PluginSettings.SignaturePublicKeyFiles = []string{
filepath.Join(path, "development-private-key.asc"),
}
}).InitBasic(t)
signatureFilename := "testplugin2.tar.gz.sig"
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
require.NoError(t, err)
sigFile, err := io.ReadAll(signatureFileReader)
require.NoError(t, err)
pluginSignature := base64.StdEncoding.EncodeToString(sigFile)
tarData, err := os.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
require.NoError(t, err)
pluginServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
_, err = res.Write(tarData)
require.NoError(t, err)
}))
defer pluginServer.Close()
samplePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: pluginServer.URL,
Manifest: &model.Manifest{
Id: "testplugin2",
Name: "testplugin2",
Description: "a second plugin",
Version: "1.2.2",
MinServerVersion: "",
},
},
InstalledVersion: "",
},
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: pluginServer.URL,
Manifest: &model.Manifest{
Id: "testplugin2",
Name: "testplugin2",
Description: "a second plugin",
Version: "1.2.3",
MinServerVersion: "",
},
Signature: pluginSignature,
},
InstalledVersion: "",
},
}
request := &model.InstallMarketplacePluginRequest{Id: ""}
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = false
*cfg.PluginSettings.MarketplaceURL = "invalid.com"
})
plugin, resp, err := client.InstallMarketplacePlugin(context.Background(), request)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
require.Nil(t, plugin)
}, "marketplace disabled")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.RequirePluginSignature = true
})
manifest, resp, err := client.UploadPlugin(context.Background(), bytes.NewReader(tarData))
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
require.Nil(t, manifest)
manifest, resp, err = client.InstallPluginFromURL(context.Background(), "some_url", true)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
require.Nil(t, manifest)
}, "RequirePluginSignature enabled")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = "invalid.com"
})
plugin, resp, err := client.InstallMarketplacePlugin(context.Background(), request)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, plugin)
}, "no server")
t.Run("no permission", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = "invalid.com"
})
plugin, resp, err := th.Client.InstallMarketplacePlugin(context.Background(), request)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
require.Nil(t, plugin)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
pRequest := &model.InstallMarketplacePluginRequest{Id: "some_plugin_id"}
plugin, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, plugin)
}, "plugin not found on the server")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
*cfg.PluginSettings.AllowInsecureDownloadURL = true
})
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2"}
plugin, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, plugin)
}, "plugin not verified")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
serverVersion := req.URL.Query().Get("server_version")
require.NotEmpty(t, serverVersion)
require.Equal(t, model.CurrentVersion, serverVersion)
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2"}
manifest, _, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.NoError(t, err)
require.NotNil(t, manifest)
require.Equal(t, "testplugin2", manifest.Id)
require.Equal(t, "1.2.3", manifest.Version)
filePath := filepath.Join("plugins", "testplugin2.tar.gz.sig")
savedSigFile, appErr := th.App.ReadFile(filePath)
require.Nil(t, appErr)
require.EqualValues(t, sigFile, savedSigFile)
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
exists, appErr := th.App.FileExists(filePath)
require.Nil(t, appErr)
require.False(t, exists)
appErr = th.App.DeletePublicKey("pub_key")
require.Nil(t, appErr)
}, "verify, install and remove plugin")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
serverVersion := req.URL.Query().Get("server_version")
require.NotEmpty(t, serverVersion)
require.Equal(t, model.CurrentVersion, serverVersion)
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "9.9.9"}
manifest, _, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.NoError(t, err)
require.NotNil(t, manifest)
require.Equal(t, "testplugin2", manifest.Id)
require.Equal(t, "1.2.3", manifest.Version)
filePath := filepath.Join("plugins", "testplugin2.tar.gz.sig")
savedSigFile, appErr := th.App.ReadFile(filePath)
require.Nil(t, appErr)
require.EqualValues(t, sigFile, savedSigFile)
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
exists, appErr := th.App.FileExists(filePath)
require.Nil(t, appErr)
require.False(t, exists)
appErr = th.App.DeletePublicKey("pub_key")
require.Nil(t, appErr)
}, "ignore version in Marketplace request")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
requestHandled := false
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
licenseType, ok := req.URL.Query()["enterprise_plugins"]
require.True(t, ok)
require.Len(t, licenseType, 1)
require.Equal(t, "false", licenseType[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0]})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
requestHandled = true
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
assert.True(t, requestHandled)
}, "verify EnterprisePlugins is false for TE")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
requestHandled := false
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
licenseType, ok := req.URL.Query()["enterprise_plugins"]
require.True(t, ok)
require.Len(t, licenseType, 1)
require.Equal(t, "false", licenseType[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
requestHandled = true
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
l := model.NewTestLicense()
// model.NewTestLicense generates a E20 license
*l.Features.EnterprisePlugins = false
th.App.Srv().SetLicense(l)
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
assert.True(t, requestHandled)
}, "verify EnterprisePlugins is false for E10")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
requestHandled := false
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
licenseType, ok := req.URL.Query()["enterprise_plugins"]
require.True(t, ok)
require.Len(t, licenseType, 1)
require.Equal(t, "true", licenseType[0])
res.WriteHeader(http.StatusOK)
json, err := json.Marshal([]*model.MarketplacePlugin{})
require.NoError(t, err)
_, err = res.Write(json)
require.NoError(t, err)
requestHandled = true
}))
defer func() { testServer.Close() }()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
th.App.Srv().SetLicense(model.NewTestLicense("enterprise_plugins"))
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
assert.True(t, requestHandled)
}, "verify EnterprisePlugins is true for E20")
}
func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) {
mainHelper.Parallel(t)
path, _ := fileutils.FindDir("tests")
signatureFilename := "testplugin2.tar.gz.sig"
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
require.NoError(t, err)
sigFile, err := io.ReadAll(signatureFileReader)
require.NoError(t, err)
pluginSignature := base64.StdEncoding.EncodeToString(sigFile)
tarData, err := os.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
require.NoError(t, err)
pluginServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
_, err = res.Write(tarData)
require.NoError(t, err)
}))
defer pluginServer.Close()
samplePlugins := []*model.MarketplacePlugin{
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: pluginServer.URL,
Manifest: &model.Manifest{
Id: "testplugin2",
Name: "testplugin2",
Description: "a second plugin",
Version: "1.2.2",
MinServerVersion: "",
},
},
InstalledVersion: "",
},
{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
IconData: "https://example.com/icon.svg",
DownloadURL: pluginServer.URL,
Manifest: &model.Manifest{
Id: "testplugin2",
Name: "testplugin2",
Description: "a second plugin",
Version: "1.2.3",
MinServerVersion: "",
},
Signature: pluginSignature,
},
InstalledVersion: "",
},
}
t.Run("install prepackaged and remote plugins through marketplace", func(t *testing.T) {
prepackagedPluginsDir := "prepackaged_plugins"
os.RemoveAll(prepackagedPluginsDir)
err := os.Mkdir(prepackagedPluginsDir, os.ModePerm)
require.NoError(t, err)
defer os.RemoveAll(prepackagedPluginsDir)
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsDir)
require.True(t, found, "failed to find prepackaged plugins directory")
err = testlib.CopyFile(filepath.Join(path, "testplugin.tar.gz"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
require.NoError(t, err)
err = testlib.CopyFile(filepath.Join(path, "testplugin.tar.gz.asc"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
require.NoError(t, err)
th := SetupConfig(t, func(cfg *model.Config) {
// Disable auto-installing prepackaged plugins
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
cfg.PluginSettings.SignaturePublicKeyFiles = []string{
filepath.Join(path, "development-private-key.asc"),
}
}).InitBasic(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
expectedSignaturePath := filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig")
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
serverVersion := req.URL.Query().Get("server_version")
require.NotEmpty(t, serverVersion)
require.Equal(t, model.CurrentVersion, serverVersion)
res.WriteHeader(http.StatusOK)
var out []byte
// Return something if testplugin2 or no specific plugin is requested
pluginID := req.URL.Query().Get("plugin_id")
if pluginID == "" || pluginID == samplePlugins[1].Manifest.Id {
out, err = json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]})
require.NoError(t, err)
}
_, err = res.Write(out)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = false
*cfg.PluginSettings.MarketplaceURL = testServer.URL
*cfg.PluginSettings.AllowInsecureDownloadURL = false
})
env := th.App.GetPluginsEnvironment()
pluginsResp, _, err := client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.Len(t, pluginsResp.Inactive, 0)
t.Run("Should fail to install unknown prepackaged plugin", func(t *testing.T) {
pRequest := &model.InstallMarketplacePluginRequest{Id: "testpluginXX"}
manifest, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
plugins := env.PrepackagedPlugins()
require.Len(t, plugins, 1)
require.Equal(t, "testplugin", plugins[0].Manifest.Id)
require.Equal(t, expectedSignaturePath, plugins[0].SignaturePath)
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.Len(t, pluginsResp.Inactive, 0)
})
t.Run("Install prepackaged plugin with Marketplace disabled", func(t *testing.T) {
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest, _, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.NoError(t, err)
require.NotNil(t, manifest)
require.Equal(t, "testplugin", manifest.Id)
require.Equal(t, "0.0.1", manifest.Version)
t.Cleanup(func() {
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{
Manifest: *manifest,
}})
})
t.Run("Try to install remote marketplace plugin while Marketplace is disabled", func(t *testing.T) {
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2"}
manifest, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
})
// Enable remote marketplace
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
*cfg.PluginSettings.AllowInsecureDownloadURL = true
})
t.Run("Install prepackaged, not listed plugin with Marketplace enabled", func(t *testing.T) {
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest, _, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.NoError(t, err)
t.Cleanup(func() {
_, err = client.RemovePlugin(context.Background(), manifest.Id)
require.NoError(t, err)
})
require.NotNil(t, manifest)
assert.Equal(t, "testplugin", manifest.Id)
assert.Equal(t, "0.0.1", manifest.Version)
})
t.Run("Install both a prepackaged and a Marketplace plugin", func(t *testing.T) {
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest1, _, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.NoError(t, err)
require.NotNil(t, manifest1)
assert.Equal(t, "testplugin", manifest1.Id)
assert.Equal(t, "0.0.1", manifest1.Version)
t.Cleanup(func() {
_, err = client.RemovePlugin(context.Background(), manifest1.Id)
require.NoError(t, err)
})
pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"}
manifest2, _, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.NoError(t, err)
require.NotNil(t, manifest2)
require.Equal(t, "testplugin2", manifest2.Id)
require.Equal(t, "1.2.3", manifest2.Version)
t.Cleanup(func() {
_, err = client.RemovePlugin(context.Background(), manifest2.Id)
require.NoError(t, err)
})
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{
{
Manifest: *manifest1,
},
{
Manifest: *manifest2,
},
})
})
})
})
t.Run("missing prepackaged and remote plugin signatures", func(t *testing.T) {
prepackagedPluginsDir := "prepackaged_plugins"
os.RemoveAll(prepackagedPluginsDir)
err := os.Mkdir(prepackagedPluginsDir, os.ModePerm)
require.NoError(t, err)
defer os.RemoveAll(prepackagedPluginsDir)
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsDir)
require.True(t, found, "failed to find prepackaged plugins directory")
err = testlib.CopyFile(filepath.Join(path, "testplugin.tar.gz"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
require.NoError(t, err)
th := SetupConfig(t, func(cfg *model.Config) {
// Disable auto-installing prepackaged plugins
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
cfg.PluginSettings.SignaturePublicKeyFiles = []string{
filepath.Join(path, "development-private-key.asc"),
}
}).InitBasic(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
serverVersion := req.URL.Query().Get("server_version")
require.NotEmpty(t, serverVersion)
require.Equal(t, model.CurrentVersion, serverVersion)
mPlugins := []*model.MarketplacePlugin{samplePlugins[0]}
require.Empty(t, mPlugins[0].Signature)
res.WriteHeader(http.StatusOK)
var out []byte
out, err = json.Marshal(mPlugins)
require.NoError(t, err)
_, err = res.Write(out)
require.NoError(t, err)
}))
defer testServer.Close()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.EnableRemoteMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
*cfg.PluginSettings.AllowInsecureDownloadURL = true
})
env := th.App.GetPluginsEnvironment()
plugins := env.PrepackagedPlugins()
require.Len(t, plugins, 0)
pluginsResp, _, err := client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.Len(t, pluginsResp.Inactive, 0)
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"}
manifest, resp, err := client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.Len(t, pluginsResp.Inactive, 0)
pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"}
manifest, resp, err = client.InstallMarketplacePlugin(context.Background(), pRequest)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
require.Nil(t, manifest)
pluginsResp, _, err = client.GetPlugins(context.Background())
require.NoError(t, err)
require.Len(t, pluginsResp.Active, 0)
require.Len(t, pluginsResp.Inactive, 0)
})
})
}
func findClusterMessages(event model.ClusterEvent, msgs []*model.ClusterMessage) []*model.ClusterMessage {
var result []*model.ClusterMessage
for _, msg := range msgs {
if msg.Event == event {
result = append(result, msg)
}
}
return result
}
func TestPluginWebSocketSession(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
pluginID := "com.mattermost.websocket_session_test"
// Compile plugin
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_websocket_session", "main.go")
pluginCode, err := os.ReadFile(fullPath)
require.NoError(t, err)
require.NotEmpty(t, pluginCode)
pluginDir, err := filepath.Abs(*th.App.Config().PluginSettings.Directory)
require.NoError(t, err)
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGo(t, string(pluginCode), backend)
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
require.NoError(t, err)
// Activate the plugin
manifest, activated, reterr := th.App.GetPluginsEnvironment().Activate(pluginID)
require.NoError(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
// Connect through WebSocket and send a message
reqURL := fmt.Sprintf("ws://localhost:%d", th.Server.ListenAddr.Port)
wsc, err := model.NewWebSocketClient4(reqURL, th.Client.AuthToken)
require.NoError(t, err)
require.NotNil(t, wsc)
wsc.Listen()
defer wsc.Close()
resp := <-wsc.ResponseChannel
require.Equal(t, resp.Status, model.StatusOk)
wsc.SendMessage("custom_action", map[string]any{"value": "test"})
// Get session for user
sessions, _, err := th.Client.GetSessions(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
require.NotEmpty(t, sessions)
// Verify the session has been set correctly. Check plugin code in
// channels/app/plugin_api_tests/manual.test_websocket_session
//
// Here the MessageWillBePosted hook is used purely as a way to
// communicate with the plugin side.
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginID)
require.NoError(t, err)
require.NotNil(t, hooks)
_, sessionID := hooks.MessageWillBePosted(nil, nil)
require.Equal(t, sessions[0].Id, sessionID)
}
func TestPluginWebSocketRemoteAddress(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
pluginID := "com.mattermost.websocket_remote_address_test"
// Compile plugin
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_websocket_remote_address", "main.go")
pluginCode, err := os.ReadFile(fullPath)
require.NoError(t, err)
require.NotEmpty(t, pluginCode)
pluginDir, err := filepath.Abs(*th.App.Config().PluginSettings.Directory)
require.NoError(t, err)
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGo(t, string(pluginCode), backend)
err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
require.NoError(t, err)
// Activate the plugin
manifest, activated, reterr := th.App.GetPluginsEnvironment().Activate(pluginID)
require.NoError(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
// Connect through WebSocket and send a message
reqURL := fmt.Sprintf("ws://localhost:%d", th.Server.ListenAddr.Port)
wsc, err := model.NewWebSocketClient4(reqURL, th.Client.AuthToken)
require.NoError(t, err)
require.NotNil(t, wsc)
wsc.Listen()
defer wsc.Close()
resp := <-wsc.ResponseChannel
require.Equal(t, resp.Status, model.StatusOk)
wsc.SendMessage("custom_action", map[string]any{"value": "test"})
// Verify the remote address has been set correctly. Check plugin code in
// channels/app/plugin_api_tests/manual.test_websocket_remote_address
//
// Here the MessageWillBePosted hook is used purely as a way to
// communicate with the plugin side.
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginID)
require.NoError(t, err)
require.NotNil(t, hooks)
_, remoteAddr := hooks.MessageWillBePosted(nil, nil)
require.NotEmpty(t, remoteAddr)
}