Files
mostlymatter/server/channels/api4/plugin_test.go
Jesse Hallam 60a747f975 Always require signatures for prepackaged plugins (#31785)
* Always require signatures for prepackaged plugins

We have always required signatures for packages installed via the marketplace -- whether remotely satisfied, or sourced from the prepackaged plugin cache.

However, prepackaged plugins discovered and automatically installed on
startup did not require a valid signature. Since we already ship
signatures for all Mattermost-authored prepackaged plugins, it's easy to
simply start requiring this.

Distributions of Mattermost that bundle their own prepackaged plugins
will have to include their own signatures. This in turn requires
distributing and configuring Mattermost with a custom public key via
`PluginSettings.SignaturePublicKeyFiles`.

Note that this enhanced security is neutered with a deployment that uses
a file-based `config.json`, as any exploit that allows appending to the
prepackaged plugins cache probably also allows modifying `config.json`
to register a new public key. A [database-based
config](https://docs.mattermost.com/configure/configuration-in-your-database.html)
is recommended.

Finally, we already support an optional setting
`PluginSettings.RequirePluginSignature` to always require a plugin
signature, although this effectively disables plugin uploads and
requires extra effort to deploy the corresponding signature. In
environments where only prepackaged plugins are used, this setting is
ideal.

Fixes: https://mattermost.atlassian.net/browse/MM-64627

* setup dev key, expect no plugins if sig fails

* Fix shadow variable errors in test helpers

Pre-declare signaturePublicKey variable in loops to avoid shadowing
the outer err variable used in error handling.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Replace PrepackagedPlugin.Signature with SignaturePath for memory efficiency

- Changed PrepackagedPlugin struct to use SignaturePath string instead of Signature []byte
- Updated buildPrepackagedPlugin to use file descriptor instead of reading signature into memory
- Modified plugin installation and persistence to read from signature file paths
- Updated all tests to check SignaturePath instead of Signature field
- Removed unused bytes import from plugin.go

This change reduces memory usage by storing file paths instead of signature data
in memory while maintaining the same security verification functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-24 15:11:02 -03:00

2172 строки
74 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)
defer th.TearDown()
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 TestNotifyClusterPluginEvent(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
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)
done := make(chan bool)
go func() {
for {
select {
case resp := <-webSocketClient.EventChannel:
if resp.EventType() == model.WebsocketEventPluginStatusesChanged && len(resp.GetData()["plugin_statuses"].([]any)) == 0 {
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")
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()
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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()
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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)
defer th.TearDown()
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 TestGetPrepackagedPlaybooksPluginIn(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
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()
prepackagePlugins := []*plugin.PrepackagedPlugin{
{
Manifest: &model.Manifest{
Version: "1.40.0",
Id: "playbooks",
},
},
{
Manifest: &model.Manifest{
Version: "2.0.1",
Id: "playbooks",
},
},
}
env := th.App.GetPluginsEnvironment()
env.SetPrepackagedPlugins(prepackagePlugins, nil)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.EnableMarketplace = true
*cfg.PluginSettings.MarketplaceURL = testServer.URL
})
t.Run("playbooks v1 is returned if not licensed", func(t *testing.T) {
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
expectedPlugins := prepackagePlugins[0]
require.Equal(t, expectedPlugins.Manifest.Version, plugins[0].Manifest.Version)
require.Len(t, plugins, 1)
})
t.Run("playbooks v2 is returned if Enterprise licensed", func(t *testing.T) {
lic := th.App.Srv().License()
th.App.Srv().SetLicense(model.NewTestLicenseSKU("enterprise"))
defer th.App.Srv().SetLicense(lic)
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
expectedPlugins := prepackagePlugins[1]
require.Equal(t, expectedPlugins.Manifest.Version, plugins[0].Manifest.Version)
require.Len(t, plugins, 1)
})
t.Run("playbooks v1 is returned if professional licensed", func(t *testing.T) {
lic := th.App.Srv().License()
th.App.Srv().SetLicense(model.NewTestLicenseSKU("professional"))
defer th.App.Srv().SetLicense(lic)
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
require.NoError(t, err)
expectedPlugins := prepackagePlugins[0]
require.Equal(t, expectedPlugins.Manifest.Version, plugins[0].Manifest.Version)
require.Len(t, plugins, 1)
})
}
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()
defer th.TearDown()
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()
defer th.TearDown()
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()
defer th.TearDown()
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()
defer th.TearDown()
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()
defer th.TearDown()
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)
}