mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
* allow `Wait()`ing on the supervisor In the event the plugin supervisor shuts down a plugin for crashing too many times, the new `Wait()` interface allows the `ActivatePlugin` to accept a callback function to trigger when `supervisor.Wait()` returns. If the supervisor shuts down normally, this callback is invoked with a nil error, otherwise any error reported by the supervisor is passed along. * improve plugin activation/deactivation logic Avoid triggering activation of previously failed-to-start plugins just becase something in the configuration changed. Now, intelligently compare the global enable bit as well as the each individual plugin's enabled bit. * expose store to manipulate PluginStatuses * expose API to fetch plugin statuses * keep track of whether or not plugin sandboxing is supported * transition plugin statuses * restore error on plugin activation if already active * don't initialize test plugins until successfully loaded * emit websocket events when plugin statuses change * skip pruning if already initialized * MM-8622: maintain plugin statuses in memory Switch away from persisting plugin statuses to the database, and maintain in memory instead. This will be followed by a cluster interface to query the in-memory status of plugin statuses from all cluster nodes. At the same time, rename `cluster_discovery_id` on the `PluginStatus` model object to `cluster_id`. * MM-8622: aggregate plugin statuses across cluster * fetch cluster plugin statuses when emitting websocket notification * address unit test fixes after rebasing * relax (poor) racey unit test re: supervisor.Wait() * make store-mocks
276 lines
7.3 KiB
Go
276 lines
7.3 KiB
Go
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost-server/model"
|
|
"github.com/mattermost/mattermost-server/plugin"
|
|
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
|
)
|
|
|
|
func TestPluginKeyValueStore(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
defer th.TearDown()
|
|
|
|
pluginId := "testpluginid"
|
|
|
|
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")))
|
|
|
|
// Test getting non-existent key
|
|
ret, err = th.App.GetPluginKey(pluginId, "notakey")
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, ret)
|
|
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginId, "stringkey"))
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginId, "intkey"))
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginId, "postkey"))
|
|
assert.Nil(t, th.App.DeletePluginKey(pluginId, "notrealkey"))
|
|
}
|
|
|
|
func TestServePluginRequest(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
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.ServePluginRequest(w, r)
|
|
assert.Equal(t, http.StatusNotImplemented, w.Result().StatusCode)
|
|
}
|
|
|
|
func TestHandlePluginRequest(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = false
|
|
*cfg.ServiceSettings.EnableUserAccessTokens = true
|
|
})
|
|
|
|
token, err := th.App.CreateUserAccessToken(&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.servePluginRequest(nil, r, func(_ 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)
|
|
}
|
|
|
|
type testPlugin struct {
|
|
plugintest.Hooks
|
|
}
|
|
|
|
func (p *testPlugin) OnConfigurationChange() error {
|
|
return nil
|
|
}
|
|
|
|
func (p *testPlugin) OnDeactivate() error {
|
|
return nil
|
|
}
|
|
|
|
type pluginCommandTestPlugin struct {
|
|
testPlugin
|
|
|
|
TeamId string
|
|
}
|
|
|
|
func (p *pluginCommandTestPlugin) OnActivate(api plugin.API) error {
|
|
if err := api.RegisterCommand(&model.Command{
|
|
Trigger: "foo",
|
|
TeamId: p.TeamId,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := api.RegisterCommand(&model.Command{
|
|
Trigger: "foo2",
|
|
TeamId: p.TeamId,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return api.UnregisterCommand(p.TeamId, "foo2")
|
|
}
|
|
|
|
func (p *pluginCommandTestPlugin) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
|
if args.Command == "/foo" {
|
|
return &model.CommandResponse{
|
|
Text: "bar",
|
|
}, nil
|
|
}
|
|
return nil, model.NewAppError("ExecuteCommand", "this is an error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
func TestPluginCommands(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.InstallPlugin(&model.Manifest{
|
|
Id: "foo",
|
|
}, &pluginCommandTestPlugin{
|
|
TeamId: th.BasicTeam.Id,
|
|
})
|
|
|
|
require.Nil(t, th.App.EnablePlugin("foo"))
|
|
|
|
// Ideally, we would wait for the websocket activation event instead of just sleeping.
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
pluginStatuses, err := th.App.GetPluginStatuses()
|
|
require.Nil(t, err)
|
|
found := false
|
|
for _, pluginStatus := range pluginStatuses {
|
|
if pluginStatus.PluginId == "foo" {
|
|
require.Equal(t, model.PluginStateRunning, pluginStatus.State)
|
|
found = true
|
|
}
|
|
}
|
|
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
|
|
|
resp, err := th.App.ExecuteCommand(&model.CommandArgs{
|
|
Command: "/foo2",
|
|
TeamId: th.BasicTeam.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
|
|
|
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
|
Command: "/foo",
|
|
TeamId: th.BasicTeam.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "bar", resp.Text)
|
|
|
|
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
|
Command: "/foo baz",
|
|
TeamId: th.BasicTeam.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, "this is an error", err.Message)
|
|
assert.Nil(t, resp)
|
|
|
|
require.Nil(t, th.App.RemovePlugin("foo"))
|
|
|
|
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
|
Command: "/foo",
|
|
TeamId: th.BasicTeam.Id,
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
})
|
|
require.NotNil(t, err)
|
|
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
|
}
|
|
|
|
type pluginBadActivation struct {
|
|
testPlugin
|
|
}
|
|
|
|
func (p *pluginBadActivation) OnActivate(api plugin.API) error {
|
|
return errors.New("won't activate for some reason")
|
|
}
|
|
|
|
func TestPluginBadActivation(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.InstallPlugin(&model.Manifest{
|
|
Id: "foo",
|
|
}, &pluginBadActivation{})
|
|
|
|
t.Run("EnablePlugin bad activation", func(t *testing.T) {
|
|
err := th.App.EnablePlugin("foo")
|
|
assert.Nil(t, err)
|
|
|
|
// Ideally, we would wait for the websocket activation event instead of just
|
|
// sleeping.
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
pluginStatuses, err := th.App.GetPluginStatuses()
|
|
require.Nil(t, err)
|
|
found := false
|
|
for _, pluginStatus := range pluginStatuses {
|
|
if pluginStatus.PluginId == "foo" {
|
|
require.Equal(t, model.PluginStateFailedToStart, pluginStatus.State)
|
|
found = true
|
|
}
|
|
}
|
|
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
|
})
|
|
}
|
|
|
|
func TestGetPluginStatusesDisabled(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.PluginSettings.Enable = false
|
|
})
|
|
|
|
_, err := th.App.GetPluginStatuses()
|
|
require.EqualError(t, err, "GetPluginStatuses: Plugins have been disabled. Please check your logs for details., ")
|
|
}
|
|
|
|
func TestGetPluginStatuses(t *testing.T) {
|
|
th := Setup().InitBasic()
|
|
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)
|
|
}
|