* 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>
1422 строки
44 KiB
Go
1422 строки
44 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"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/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
|
)
|
|
|
|
func getHashedKey(key string) string {
|
|
hash := sha256.New()
|
|
hash.Write([]byte(key))
|
|
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
}
|
|
|
|
func TestPluginKeyValueStore(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
pluginID := "testpluginid"
|
|
|
|
defer func() {
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginID, "key"))
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginID, "key2"))
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginID, "key3"))
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginID, "key4"))
|
|
}()
|
|
|
|
assert.Nil(t, th.App.SetPluginKey(pluginID, "key", []byte("test")))
|
|
ret, err := th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test"), ret)
|
|
|
|
// Test inserting over existing entries
|
|
assert.Nil(t, th.App.SetPluginKey(pluginID, "key", []byte("test2")))
|
|
ret, err = th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test2"), ret)
|
|
|
|
// Test getting non-existent key
|
|
ret, err = th.App.GetPluginKey(pluginID, "notakey")
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, ret)
|
|
|
|
// Test deleting non-existent keys.
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginID, "notrealkey"))
|
|
|
|
// Verify behaviour for the old approach that involved storing the hashed keys.
|
|
hashedKey2 := getHashedKey("key2")
|
|
kv := &model.PluginKeyValue{
|
|
PluginId: pluginID,
|
|
Key: hashedKey2,
|
|
Value: []byte("test"),
|
|
ExpireAt: 0,
|
|
}
|
|
|
|
_, nErr := th.App.Srv().Store().Plugin().SaveOrUpdate(kv)
|
|
assert.NoError(t, nErr)
|
|
|
|
// Test fetch by keyname (this key does not exist but hashed key will be used for lookup)
|
|
ret, err = th.App.GetPluginKey(pluginID, "key2")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, kv.Value, ret)
|
|
|
|
// Test fetch by hashed keyname
|
|
ret, err = th.App.GetPluginKey(pluginID, hashedKey2)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, kv.Value, ret)
|
|
|
|
// Test ListKeys
|
|
assert.Nil(t, th.App.SetPluginKey(pluginID, "key3", []byte("test3")))
|
|
assert.Nil(t, th.App.SetPluginKey(pluginID, "key4", []byte("test4")))
|
|
|
|
list, err := th.App.ListPluginKeys(pluginID, 0, 1)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key"}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, 1, 1)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key3"}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, 0, 4)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key", "key3", "key4", hashedKey2}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, 0, 2)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key", "key3"}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, 1, 2)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key4", hashedKey2}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, 2, 2)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{}, list)
|
|
|
|
// List Keys bad input
|
|
list, err = th.App.ListPluginKeys(pluginID, 0, 0)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key", "key3", "key4", hashedKey2}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, 0, -1)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key", "key3", "key4", hashedKey2}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, -1, 1)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key"}, list)
|
|
|
|
list, err = th.App.ListPluginKeys(pluginID, -1, 0)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []string{"key", "key3", "key4", hashedKey2}, list)
|
|
}
|
|
|
|
func TestPluginKeyValueStoreCompareAndSet(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
pluginID := "testpluginid"
|
|
|
|
defer func() {
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginID, "key"))
|
|
}()
|
|
|
|
// Set using Set api for key2
|
|
assert.Nil(t, th.App.SetPluginKey(pluginID, "key2", []byte("test")))
|
|
ret, err := th.App.GetPluginKey(pluginID, "key2")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test"), ret)
|
|
|
|
// Attempt to insert value for key2
|
|
updated, err := th.App.CompareAndSetPluginKey(pluginID, "key2", nil, []byte("test2"))
|
|
assert.Nil(t, err)
|
|
assert.False(t, updated)
|
|
ret, err = th.App.GetPluginKey(pluginID, "key2")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test"), ret)
|
|
|
|
// Insert new value for key
|
|
updated, err = th.App.CompareAndSetPluginKey(pluginID, "key", nil, []byte("test"))
|
|
assert.Nil(t, err)
|
|
assert.True(t, updated)
|
|
ret, err = th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test"), ret)
|
|
|
|
// Should fail to insert again
|
|
updated, err = th.App.CompareAndSetPluginKey(pluginID, "key", nil, []byte("test3"))
|
|
assert.Nil(t, err)
|
|
assert.False(t, updated)
|
|
ret, err = th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test"), ret)
|
|
|
|
// Test updating using incorrect old value
|
|
updated, err = th.App.CompareAndSetPluginKey(pluginID, "key", []byte("oldvalue"), []byte("test3"))
|
|
assert.Nil(t, err)
|
|
assert.False(t, updated)
|
|
ret, err = th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test"), ret)
|
|
|
|
// Test updating using correct old value
|
|
updated, err = th.App.CompareAndSetPluginKey(pluginID, "key", []byte("test"), []byte("test2"))
|
|
assert.Nil(t, err)
|
|
assert.True(t, updated)
|
|
ret, err = th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("test2"), ret)
|
|
}
|
|
|
|
func TestPluginKeyValueStoreSetWithOptionsJSON(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
pluginID := "testpluginid"
|
|
|
|
t.Run("storing a value without providing options works", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "key", []byte("value-1"), model.PluginKVSetOptions{})
|
|
assert.True(t, result)
|
|
assert.Nil(t, err)
|
|
|
|
// and I can get it back!
|
|
ret, err := th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte(`value-1`), ret)
|
|
})
|
|
|
|
t.Run("test that setting it atomic when it doesn't match doesn't change anything", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
err := th.App.SetPluginKey(pluginID, "key", []byte("value-1"))
|
|
require.Nil(t, err)
|
|
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "key", []byte("value-3"), model.PluginKVSetOptions{
|
|
Atomic: true,
|
|
OldValue: []byte("value-2"),
|
|
})
|
|
assert.False(t, result)
|
|
assert.Nil(t, err)
|
|
|
|
// test that the value didn't change
|
|
ret, err := th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte(`value-1`), ret)
|
|
})
|
|
|
|
t.Run("test the atomic change with the proper old value", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
err := th.App.SetPluginKey(pluginID, "key", []byte("value-2"))
|
|
require.Nil(t, err)
|
|
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "key", []byte("value-3"), model.PluginKVSetOptions{
|
|
Atomic: true,
|
|
OldValue: []byte("value-2"),
|
|
})
|
|
assert.True(t, result)
|
|
assert.Nil(t, err)
|
|
|
|
// test that the value did change
|
|
ret, err := th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte(`value-3`), ret)
|
|
})
|
|
|
|
t.Run("when new value is nil and old value matches with the current, it should delete the currently set value", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
// first set a value.
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "nil-test-key-2", []byte("value-1"), model.PluginKVSetOptions{})
|
|
require.Nil(t, err)
|
|
require.True(t, result)
|
|
|
|
// now it should delete the set value.
|
|
result, err = th.App.SetPluginKeyWithOptions(pluginID, "nil-test-key-2", nil, model.PluginKVSetOptions{
|
|
Atomic: true,
|
|
OldValue: []byte("value-1"),
|
|
})
|
|
assert.Nil(t, err)
|
|
assert.True(t, result)
|
|
|
|
ret, err := th.App.GetPluginKey(pluginID, "nil-test-key-2")
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, ret)
|
|
})
|
|
|
|
t.Run("when new value is nil and there is a value set for the key already, it should delete the currently set value", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
// first set a value.
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "nil-test-key-3", []byte("value-1"), model.PluginKVSetOptions{})
|
|
require.Nil(t, err)
|
|
require.True(t, result)
|
|
|
|
// now it should delete the set value.
|
|
result, err = th.App.SetPluginKeyWithOptions(pluginID, "nil-test-key-3", nil, model.PluginKVSetOptions{})
|
|
assert.Nil(t, err)
|
|
assert.True(t, result)
|
|
|
|
// verify a nil value is returned
|
|
ret, err := th.App.GetPluginKey(pluginID, "nil-test-key-3")
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, ret)
|
|
|
|
// verify the row is actually gone
|
|
list, err := th.App.ListPluginKeys(pluginID, 0, 1)
|
|
assert.Nil(t, err)
|
|
assert.Empty(t, list)
|
|
})
|
|
|
|
t.Run("when old value is nil and there is no value set for the key before, it should set the new value", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "nil-test-key-4", []byte("value-1"), model.PluginKVSetOptions{
|
|
Atomic: true,
|
|
OldValue: nil,
|
|
})
|
|
assert.Nil(t, err)
|
|
assert.True(t, result)
|
|
|
|
ret, err := th.App.GetPluginKey(pluginID, "nil-test-key-4")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte("value-1"), ret)
|
|
})
|
|
|
|
t.Run("test that value is set and unset with ExpireInSeconds", func(t *testing.T) {
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
result, err := th.App.SetPluginKeyWithOptions(pluginID, "key", []byte("value-1"), model.PluginKVSetOptions{
|
|
ExpireInSeconds: 1,
|
|
})
|
|
assert.True(t, result)
|
|
assert.Nil(t, err)
|
|
|
|
// test that the value is set
|
|
ret, err := th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []byte(`value-1`), ret)
|
|
|
|
// test that the value is not longer
|
|
time.Sleep(1500 * time.Millisecond)
|
|
|
|
ret, err = th.App.GetPluginKey(pluginID, "key")
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, ret)
|
|
})
|
|
}
|
|
|
|
func TestServePluginRequest(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
|
|
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/plugins/foo/bar", nil)
|
|
th.App.ch.ServePluginRequest(w, r)
|
|
assert.Equal(t, http.StatusNotImplemented, w.Result().StatusCode)
|
|
}
|
|
|
|
func TestPrivateServePluginRequest(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
ConfigFunc func(cfg *model.Config)
|
|
URL string
|
|
ExpectedURL string
|
|
}{
|
|
{
|
|
"no subpath",
|
|
func(cfg *model.Config) {},
|
|
"/plugins/id/endpoint",
|
|
"/endpoint",
|
|
},
|
|
{
|
|
"subpath",
|
|
func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL += "/subpath" },
|
|
"/subpath/plugins/id/endpoint",
|
|
"/endpoint",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
th.App.UpdateConfig(testCase.ConfigFunc)
|
|
expectedBody := []byte("body")
|
|
request := httptest.NewRequest(http.MethodGet, testCase.URL, bytes.NewReader(expectedBody))
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler := func(context *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, testCase.ExpectedURL, r.URL.Path)
|
|
|
|
body, _ := io.ReadAll(r.Body)
|
|
assert.Equal(t, expectedBody, body)
|
|
}
|
|
|
|
request = mux.SetURLVars(request, map[string]string{"plugin_id": "id"})
|
|
|
|
th.App.ch.servePluginRequest(recorder, request, handler)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlePluginRequest(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = false
|
|
*cfg.ServiceSettings.EnableUserAccessTokens = true
|
|
})
|
|
|
|
token, err := th.App.CreateUserAccessToken(th.Context, &model.UserAccessToken{
|
|
UserId: th.BasicUser.Id,
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
var assertions func(*http.Request)
|
|
router := mux.NewRouter()
|
|
router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", func(_ http.ResponseWriter, r *http.Request) {
|
|
th.App.ch.servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) {
|
|
assertions(r)
|
|
})
|
|
})
|
|
|
|
r := httptest.NewRequest("GET", "/plugins/foo/bar", nil)
|
|
r.Header.Add("Authorization", "Bearer "+token.Token)
|
|
assertions = func(r *http.Request) {
|
|
assert.Equal(t, "/bar", r.URL.Path)
|
|
assert.Equal(t, th.BasicUser.Id, r.Header.Get("Mattermost-User-Id"))
|
|
}
|
|
router.ServeHTTP(nil, r)
|
|
|
|
r = httptest.NewRequest("GET", "/plugins/foo/bar?a=b&access_token="+token.Token+"&c=d", nil)
|
|
assertions = func(r *http.Request) {
|
|
assert.Equal(t, "/bar", r.URL.Path)
|
|
assert.Equal(t, "a=b&c=d", r.URL.RawQuery)
|
|
assert.Equal(t, th.BasicUser.Id, r.Header.Get("Mattermost-User-Id"))
|
|
}
|
|
router.ServeHTTP(nil, r)
|
|
|
|
r = httptest.NewRequest("GET", "/plugins/foo/bar?a=b&access_token=asdf&c=d", nil)
|
|
assertions = func(r *http.Request) {
|
|
assert.Equal(t, "/bar", r.URL.Path)
|
|
assert.Equal(t, "a=b&c=d", r.URL.RawQuery)
|
|
assert.Empty(t, r.Header.Get("Mattermost-User-Id"))
|
|
}
|
|
router.ServeHTTP(nil, r)
|
|
}
|
|
|
|
func TestGetPluginStatusesDisabled(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = false
|
|
})
|
|
|
|
_, err := th.App.GetPluginStatuses()
|
|
require.NotNil(t, err)
|
|
require.EqualError(t, err, "GetPluginStatuses: Plugins have been disabled. Please check your logs for details.")
|
|
}
|
|
|
|
func TestGetPluginStatuses(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = true
|
|
})
|
|
|
|
pluginStatuses, err := th.App.GetPluginStatuses()
|
|
require.Nil(t, err)
|
|
require.NotNil(t, pluginStatuses)
|
|
}
|
|
|
|
func TestPluginSync(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
path, _ := fileutils.FindDir("tests")
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.PluginSettings.SignaturePublicKeyFiles = []string{
|
|
filepath.Join(path, "development-private-key.asc"),
|
|
}
|
|
})
|
|
defer th.TearDown()
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
ConfigFunc func(cfg *model.Config)
|
|
}{
|
|
{
|
|
"local",
|
|
func(cfg *model.Config) {
|
|
cfg.FileSettings.DriverName = model.NewPointer(model.ImageDriverLocal)
|
|
},
|
|
},
|
|
{
|
|
"s3",
|
|
func(cfg *model.Config) {
|
|
s3Host := os.Getenv("CI_MINIO_HOST")
|
|
if s3Host == "" {
|
|
s3Host = "localhost"
|
|
}
|
|
|
|
s3Port := os.Getenv("CI_MINIO_PORT")
|
|
if s3Port == "" {
|
|
s3Port = "9000"
|
|
}
|
|
|
|
s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port)
|
|
cfg.FileSettings.DriverName = model.NewPointer(model.ImageDriverS3)
|
|
cfg.FileSettings.AmazonS3AccessKeyId = model.NewPointer(model.MinioAccessKey)
|
|
cfg.FileSettings.AmazonS3SecretAccessKey = model.NewPointer(model.MinioSecretKey)
|
|
cfg.FileSettings.AmazonS3Bucket = model.NewPointer(model.MinioBucket)
|
|
cfg.FileSettings.AmazonS3PathPrefix = model.NewPointer("")
|
|
cfg.FileSettings.AmazonS3Endpoint = model.NewPointer(s3Endpoint)
|
|
cfg.FileSettings.AmazonS3Region = model.NewPointer("")
|
|
cfg.FileSettings.AmazonS3SSL = model.NewPointer(false)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = true
|
|
testCase.ConfigFunc(cfg)
|
|
})
|
|
|
|
env := th.App.GetPluginsEnvironment()
|
|
require.NotNil(t, env)
|
|
|
|
t.Run("new bundle in the file store", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.RequirePluginSignature = false
|
|
})
|
|
|
|
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
|
require.NoError(t, err)
|
|
defer fileReader.Close()
|
|
|
|
_, appErr := th.App.WriteFile(fileReader, getBundleStorePath("testplugin"))
|
|
checkNoError(t, appErr)
|
|
|
|
appErr = th.App.SyncPlugins()
|
|
checkNoError(t, appErr)
|
|
|
|
// Check if installed
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
|
})
|
|
|
|
t.Run("bundle removed from the file store", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.RequirePluginSignature = false
|
|
})
|
|
|
|
appErr := th.App.RemoveFile(getBundleStorePath("testplugin"))
|
|
checkNoError(t, appErr)
|
|
|
|
appErr = th.App.SyncPlugins()
|
|
checkNoError(t, appErr)
|
|
|
|
// Check if removed
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Empty(t, pluginStatus)
|
|
})
|
|
|
|
t.Run("plugin signatures required, no signature", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.RequirePluginSignature = true
|
|
})
|
|
|
|
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
|
require.NoError(t, err)
|
|
defer pluginFileReader.Close()
|
|
_, appErr := th.App.WriteFile(pluginFileReader, getBundleStorePath("testplugin"))
|
|
checkNoError(t, appErr)
|
|
|
|
appErr = th.App.SyncPlugins()
|
|
checkNoError(t, appErr)
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 0)
|
|
})
|
|
|
|
t.Run("plugin signatures required, wrong signature", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.RequirePluginSignature = true
|
|
})
|
|
|
|
signatureFileReader, err := os.Open(filepath.Join(path, "testplugin2.tar.gz.sig"))
|
|
require.NoError(t, err)
|
|
defer signatureFileReader.Close()
|
|
_, appErr := th.App.WriteFile(signatureFileReader, getSignatureStorePath("testplugin"))
|
|
checkNoError(t, appErr)
|
|
|
|
appErr = th.App.SyncPlugins()
|
|
checkNoError(t, appErr)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 0)
|
|
})
|
|
|
|
t.Run("plugin signatures required, correct signature", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.RequirePluginSignature = true
|
|
})
|
|
|
|
signatureFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
|
|
require.NoError(t, err)
|
|
defer signatureFileReader.Close()
|
|
_, appErr := th.App.WriteFile(signatureFileReader, getSignatureStorePath("testplugin"))
|
|
checkNoError(t, appErr)
|
|
|
|
appErr = th.App.SyncPlugins()
|
|
checkNoError(t, appErr)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
|
|
|
appErr = th.App.DeletePublicKey("pub_key")
|
|
checkNoError(t, appErr)
|
|
|
|
appErr = th.App.ch.RemovePlugin("testplugin")
|
|
checkNoError(t, appErr)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// See https://github.com/mattermost/mattermost-server/issues/19189
|
|
func TestChannelsPluginsInit(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
runNoPanicTest := func(t *testing.T) {
|
|
path, _ := fileutils.FindDir("tests")
|
|
|
|
require.NotPanics(t, func() {
|
|
th.Server.Channels().initPlugins(th.Context, path, path)
|
|
})
|
|
}
|
|
|
|
t.Run("no panics when plugins enabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = true
|
|
})
|
|
|
|
runNoPanicTest(t)
|
|
})
|
|
|
|
t.Run("no panics when plugins disabled", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = false
|
|
})
|
|
|
|
runNoPanicTest(t)
|
|
})
|
|
}
|
|
|
|
func TestSyncPluginsActiveState(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = true
|
|
})
|
|
|
|
env := th.App.GetPluginsEnvironment()
|
|
require.NotNil(t, env)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.RequirePluginSignature = false
|
|
})
|
|
|
|
path, _ := fileutils.FindDir("tests")
|
|
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
|
require.NoError(t, err)
|
|
defer fileReader.Close()
|
|
|
|
_, appErr := th.App.WriteFile(fileReader, getBundleStorePath("testplugin"))
|
|
checkNoError(t, appErr)
|
|
|
|
// Sync with file store so the plugin environment has access to this plugin.
|
|
appErr = th.App.SyncPlugins()
|
|
checkNoError(t, appErr)
|
|
|
|
// Verify the plugin was installed and set to deactivated.
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
|
require.Equal(t, pluginStatus[0].State, model.PluginStateNotRunning)
|
|
|
|
// Enable plugin by setting setting config. This implicitly calls SyncPluginsActiveState through a config listener.
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
// Verify the plugin was activated due to config change.
|
|
pluginStatus, err = env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
|
require.Equal(t, pluginStatus[0].State, model.PluginStateRunning)
|
|
|
|
// Disable plugin by setting config. This implicitly calls SyncPluginsActiveState through a config listener.
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: false}
|
|
})
|
|
|
|
// Verify the plugin was deactivated due to config change.
|
|
pluginStatus, err = env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
|
require.Equal(t, pluginStatus[0].State, model.PluginStateNotRunning)
|
|
}
|
|
|
|
func TestPluginPanicLogs(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should panic", func(t *testing.T) {
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
|
|
`
|
|
package main
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
|
panic("some text from panic")
|
|
return nil, ""
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`,
|
|
}, th.App, th.NewPluginAPI)
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "message_",
|
|
CreateAt: model.GetMillis() - 10000,
|
|
}
|
|
_, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
assert.Nil(t, err)
|
|
|
|
th.TestLogger.Flush()
|
|
|
|
// We shutdown plugins first so that the read on the log buffer is race-free.
|
|
th.App.ch.ShutDownPlugins()
|
|
tearDown()
|
|
|
|
testlib.AssertLog(t, th.LogBuffer, mlog.LvlDebug.Name, "panic: some text from panic")
|
|
})
|
|
}
|
|
|
|
func TestPluginStatusActivateError(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("should return error from OnActivate in plugin statuses", func(t *testing.T) {
|
|
th := Setup(t).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
pluginSource := `
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
)
|
|
|
|
type MyPlugin struct {
|
|
plugin.MattermostPlugin
|
|
}
|
|
|
|
func (p *MyPlugin) OnActivate() error {
|
|
return errors.New("sample error")
|
|
}
|
|
|
|
func main() {
|
|
plugin.ClientMain(&MyPlugin{})
|
|
}
|
|
`
|
|
|
|
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{pluginSource}, th.App, th.NewPluginAPI)
|
|
defer tearDown()
|
|
|
|
env := th.App.GetPluginsEnvironment()
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, "sample error", pluginStatus[0].Error)
|
|
})
|
|
}
|
|
|
|
type byID []*plugin.PrepackagedPlugin
|
|
|
|
func (a byID) Len() int { return len(a) }
|
|
func (a byID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a byID) Less(i, j int) bool { return a[i].Manifest.Id < a[j].Manifest.Id }
|
|
|
|
type pluginStatusById model.PluginStatuses
|
|
|
|
func (a pluginStatusById) Len() int { return len(a) }
|
|
func (a pluginStatusById) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a pluginStatusById) Less(i, j int) bool { return a[i].PluginId < a[j].PluginId }
|
|
|
|
func TestProcessPrepackagedPlugins(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
testsPath, found := fileutils.FindDir("tests")
|
|
require.True(t, found, "failed to find tests directory")
|
|
|
|
setup := func(t *testing.T) *TestHelper {
|
|
t.Helper()
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.PluginSettings.SignaturePublicKeyFiles = []string{
|
|
filepath.Join(testsPath, "development-private-key.asc"),
|
|
}
|
|
})
|
|
t.Cleanup(th.TearDown)
|
|
|
|
// Make a prepackaged_plugins directory for use with the tests.
|
|
err := os.Mkdir(filepath.Join(th.tempWorkspace, prepackagedPluginsDir), os.ModePerm)
|
|
require.NoError(t, err)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = true
|
|
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
|
})
|
|
|
|
return th
|
|
}
|
|
|
|
initPlugins := func(t *testing.T, th *TestHelper) ([]*plugin.PrepackagedPlugin, []*plugin.PrepackagedPlugin) {
|
|
t.Helper()
|
|
|
|
appErr := th.App.ch.syncPlugins()
|
|
require.Nil(t, appErr)
|
|
|
|
err := th.App.ch.processPrepackagedPlugins(filepath.Join(th.tempWorkspace, prepackagedPluginsDir))
|
|
require.NoError(t, err)
|
|
|
|
env := th.App.GetPluginsEnvironment()
|
|
plugins, transitionalPlugins := env.PrepackagedPlugins(), env.TransitionallyPrepackagedPlugins()
|
|
th.App.ch.persistTransitionallyPrepackagedPlugins()
|
|
|
|
return plugins, transitionalPlugins
|
|
}
|
|
|
|
copyAsPrepackagedPlugin := func(t *testing.T, th *TestHelper, filename string) {
|
|
t.Helper()
|
|
|
|
err := testlib.CopyFile(filepath.Join(testsPath, filename), filepath.Join(th.tempWorkspace, prepackagedPluginsDir, filename))
|
|
require.NoError(t, err)
|
|
|
|
err = testlib.CopyFile(filepath.Join(testsPath, fmt.Sprintf("%s.sig", filename)), filepath.Join(th.tempWorkspace, prepackagedPluginsDir, fmt.Sprintf("%s.sig", filename)))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
copyAsFilestorePlugin := func(t *testing.T, th *TestHelper, bundleFilename, pluginID string) {
|
|
t.Helper()
|
|
|
|
err := testlib.CopyFile(filepath.Join(testsPath, bundleFilename), filepath.Join(*th.App.Config().FileSettings.Directory, fmt.Sprintf("plugins/%s.tar.gz", pluginID)))
|
|
require.NoError(t, err)
|
|
err = testlib.CopyFile(filepath.Join(testsPath, fmt.Sprintf("%s.sig", bundleFilename)), filepath.Join(*th.App.Config().FileSettings.Directory, fmt.Sprintf("plugins/%s.tar.gz.sig", pluginID)))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
expectPrepackagedPlugin := func(t *testing.T, pluginID, version string, actual *plugin.PrepackagedPlugin) {
|
|
t.Helper()
|
|
|
|
require.Equal(t, pluginID, actual.Manifest.Id)
|
|
require.NotEmpty(t, actual.SignaturePath, "testplugin has no signature")
|
|
require.Equal(t, version, actual.Manifest.Version)
|
|
}
|
|
|
|
expectPluginInFilestore := func(t *testing.T, th *TestHelper, pluginID, version string) {
|
|
t.Helper()
|
|
|
|
bundlePath := filepath.Join(*th.App.Config().FileSettings.Directory, fmt.Sprintf("plugins/%s.tar.gz", pluginID))
|
|
|
|
require.FileExists(t, bundlePath)
|
|
require.FileExists(t, filepath.Join(*th.App.Config().FileSettings.Directory, fmt.Sprintf("plugins/%s.tar.gz.sig", pluginID)))
|
|
|
|
// Verify the version recorded in the Manifest
|
|
f, err := os.Open(bundlePath)
|
|
require.NoError(t, err)
|
|
|
|
uncompressedStream, err := gzip.NewReader(f)
|
|
require.NoError(t, err)
|
|
|
|
tarReader := tar.NewReader(uncompressedStream)
|
|
|
|
var manifest model.Manifest
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
t.Log(header.Name)
|
|
if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "plugin.json" {
|
|
manifestReader := json.NewDecoder(tarReader)
|
|
err = manifestReader.Decode(&manifest)
|
|
require.NoError(t, err)
|
|
break
|
|
}
|
|
}
|
|
|
|
require.Equal(t, pluginID, manifest.Id, "failed to find manifest")
|
|
require.Equal(t, version, manifest.Version)
|
|
}
|
|
|
|
expectPluginNotInFilestore := func(t *testing.T, th *TestHelper, pluginID string) {
|
|
t.Helper()
|
|
|
|
require.NoFileExists(t, filepath.Join(*th.App.Config().FileSettings.Directory, fmt.Sprintf("plugins/%s.tar.gz", pluginID)))
|
|
require.NoFileExists(t, filepath.Join(*th.App.Config().FileSettings.Directory, fmt.Sprintf("plugins/%s.tar.gz.sig", pluginID)))
|
|
}
|
|
|
|
expectPluginStatus := func(t *testing.T, pluginID, version string, actual *model.PluginStatus) {
|
|
t.Helper()
|
|
|
|
require.Equal(t, pluginID, actual.PluginId)
|
|
require.Equal(t, version, actual.Version)
|
|
}
|
|
|
|
t.Run("single plugin automatically installed since enabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 1, "expected one prepackaged plugin")
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
})
|
|
|
|
t.Run("single plugin, not automatically installed since not enabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 1, "expected one prepackaged plugin")
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Empty(t, pluginStatus, 0)
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
})
|
|
|
|
t.Run("single plugin, not automatically installed despite enabled since automatic prepackaged plugins disabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 1, "expected one prepackaged plugin")
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Empty(t, pluginStatus, 0)
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
})
|
|
|
|
t.Run("multiple plugins, some automatically installed since enabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[0])
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("multiple plugins, one previously installed, all now installed", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
})
|
|
|
|
copyAsFilestorePlugin(t, th, "testplugin.tar.gz", "testplugin")
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
|
|
sort.Sort(pluginStatusById(pluginStatus))
|
|
|
|
require.Len(t, pluginStatus, 2)
|
|
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
|
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1])
|
|
|
|
expectPluginInFilestore(t, th, "testplugin", "0.0.1")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("multiple plugins, one previously installed and now upgraded, all now installed", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
})
|
|
|
|
copyAsFilestorePlugin(t, th, "testplugin.tar.gz", "testplugin")
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin-v0.0.2.tar.gz")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
err := th.App.ch.processPrepackagedPlugins(filepath.Join(th.tempWorkspace, prepackagedPluginsDir))
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.2", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
|
|
sort.Sort(pluginStatusById(pluginStatus))
|
|
|
|
require.Len(t, pluginStatus, 2)
|
|
expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0])
|
|
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1])
|
|
|
|
expectPluginInFilestore(t, th, "testplugin", "0.0.1")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("multiple plugins, one previously installed but prepackaged is older, all now installed", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
})
|
|
|
|
copyAsFilestorePlugin(t, th, "testplugin-v0.0.2.tar.gz", "testplugin")
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
|
|
sort.Sort(pluginStatusById(pluginStatus))
|
|
|
|
require.Len(t, pluginStatus, 2)
|
|
expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0])
|
|
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1])
|
|
|
|
expectPluginInFilestore(t, th, "testplugin", "0.0.2")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("multiple plugins, not automatically installed despite enabled since automatic prepackaged plugins disabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 0)
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("removing a prepackaged plugin leaves it disabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
|
|
sort.Sort(pluginStatusById(pluginStatus))
|
|
|
|
require.Len(t, pluginStatus, 2)
|
|
require.Equal(t, "testplugin", pluginStatus[0].PluginId)
|
|
require.Equal(t, "testplugin2", pluginStatus[1].PluginId)
|
|
|
|
appErr := th.App.ch.RemovePlugin("testplugin")
|
|
checkNoError(t, appErr)
|
|
|
|
pluginStatus, err = env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
require.Equal(t, "testplugin2", pluginStatus[0].PluginId)
|
|
|
|
plugins, transitionalPlugins = initPlugins(t, th)
|
|
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
|
sort.Sort(byID(plugins))
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
|
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err = env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[0])
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("single transitional plugin automatically installed and persisted since enabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
|
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
|
defer func() {
|
|
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
|
}()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Empty(t, plugins)
|
|
require.Len(t, transitionalPlugins, 1)
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", transitionalPlugins[0])
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
|
|
|
expectPluginInFilestore(t, th, "testplugin", "0.0.1")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("single transitional plugin not persisted since already in filestore", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
|
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
|
defer func() {
|
|
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
|
}()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsFilestorePlugin(t, th, "testplugin.tar.gz", "testplugin")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Empty(t, plugins)
|
|
require.Empty(t, transitionalPlugins)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
|
|
|
expectPluginInFilestore(t, th, "testplugin", "0.0.1")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("single transitional plugin persisted since newer than filestore", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
|
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
|
defer func() {
|
|
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
|
}()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsFilestorePlugin(t, th, "testplugin.tar.gz", "testplugin")
|
|
copyAsPrepackagedPlugin(t, th, "testplugin-v0.0.2.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Empty(t, plugins)
|
|
require.Len(t, transitionalPlugins, 1)
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.2", transitionalPlugins[0])
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Len(t, pluginStatus, 1)
|
|
expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0])
|
|
|
|
expectPluginInFilestore(t, th, "testplugin", "0.0.2")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
|
|
t.Run("transitional plugins persisted only once", func(t *testing.T) {
|
|
th := setup(t)
|
|
env := th.App.GetPluginsEnvironment()
|
|
|
|
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
|
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
|
defer func() {
|
|
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
|
}()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
|
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
|
})
|
|
|
|
copyAsPrepackagedPlugin(t, th, "testplugin.tar.gz")
|
|
|
|
plugins, transitionalPlugins := initPlugins(t, th)
|
|
require.Empty(t, plugins)
|
|
require.Len(t, transitionalPlugins, 1)
|
|
expectPrepackagedPlugin(t, "testplugin", "0.0.1", transitionalPlugins[0])
|
|
|
|
appErr := th.App.ch.RemovePlugin("testplugin")
|
|
require.Nil(t, appErr)
|
|
|
|
pluginStatus, err := env.Statuses()
|
|
require.NoError(t, err)
|
|
require.Empty(t, pluginStatus, 0)
|
|
|
|
th.App.ch.persistTransitionallyPrepackagedPlugins()
|
|
|
|
expectPluginNotInFilestore(t, th, "testplugin")
|
|
expectPluginNotInFilestore(t, th, "testplugin2")
|
|
})
|
|
}
|
|
|
|
func TestGetPluginStateOverride(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
defer th.TearDown()
|
|
|
|
t.Run("no override", func(t *testing.T) {
|
|
overrides, value := th.App.ch.getPluginStateOverride("focalboard")
|
|
require.False(t, overrides)
|
|
require.False(t, value)
|
|
})
|
|
|
|
t.Run("apps override", func(t *testing.T) {
|
|
t.Run("without enabled flag", func(t *testing.T) {
|
|
overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.apps")
|
|
require.True(t, overrides)
|
|
require.False(t, value)
|
|
})
|
|
|
|
t.Run("with enabled flag set to true", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th2 := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.AppsEnabled = true
|
|
})
|
|
defer th2.TearDown()
|
|
|
|
overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.apps")
|
|
require.False(t, overrides)
|
|
require.False(t, value)
|
|
})
|
|
|
|
t.Run("with enabled flag set to false", func(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th2 := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.AppsEnabled = false
|
|
})
|
|
defer th2.TearDown()
|
|
|
|
overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.apps")
|
|
require.True(t, overrides)
|
|
require.False(t, value)
|
|
})
|
|
})
|
|
}
|