diff --git a/plugin/helpers.go b/plugin/helpers.go index c3cf910733..71e72ba6e4 100644 --- a/plugin/helpers.go +++ b/plugin/helpers.go @@ -4,9 +4,7 @@ package plugin import ( - "github.com/blang/semver" "github.com/mattermost/mattermost-server/v5/model" - "github.com/pkg/errors" ) // Helpers provide a common patterns plugins use. @@ -75,20 +73,14 @@ type Helpers interface { // // Minimum server version: 5.2 ShouldProcessMessage(post *model.Post, options ...ShouldProcessMessageOption) (bool, error) + + // InstallPluginFromURL installs the plugin from the provided url. + // + // Minimum server version: 5.18 + InstallPluginFromURL(downloadURL string, replace bool) (*model.Manifest, error) } // HelpersImpl implements the helpers interface with an API that retrieves data on behalf of the plugin. type HelpersImpl struct { API API } - -func (p *HelpersImpl) ensureServerVersion(required string) error { - serverVersion := p.API.GetServerVersion() - currentVersion := semver.MustParse(serverVersion) - requiredVersion := semver.MustParse(required) - - if currentVersion.LT(requiredVersion) { - return errors.Errorf("incompatible server version for plugin, minimum required version: %s, current version: %s", required, serverVersion) - } - return nil -} diff --git a/plugin/helpers_plugin.go b/plugin/helpers_plugin.go new file mode 100644 index 0000000000..9c181a2428 --- /dev/null +++ b/plugin/helpers_plugin.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "net/http" + "net/url" + "time" + + "github.com/blang/semver" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/pkg/errors" +) + +// InstallPluginFromURL implements Helpers.InstallPluginFromURL. +func (p *HelpersImpl) InstallPluginFromURL(downloadURL string, replace bool) (*model.Manifest, error) { + err := p.ensureServerVersion("5.18.0") + if err != nil { + return nil, err + } + + parsedURL, err := url.Parse(downloadURL) + if err != nil { + return nil, errors.Wrap(err, "error while parsing url") + } + + client := &http.Client{Timeout: time.Hour} + response, err := client.Get(parsedURL.String()) + if err != nil { + return nil, errors.Wrap(err, "unable to download the plugin") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf("received %d status code while downloading plugin from server", response.StatusCode) + } + + manifest, installError := p.API.InstallPlugin(response.Body, replace) + if installError != nil { + return nil, errors.Wrap(err, "unable to install plugin on server") + } + + return manifest, nil +} + +func (p *HelpersImpl) ensureServerVersion(required string) error { + serverVersion := p.API.GetServerVersion() + currentVersion := semver.MustParse(serverVersion) + requiredVersion := semver.MustParse(required) + + if currentVersion.LT(requiredVersion) { + return errors.Errorf("incompatible server version for plugin, minimum required version: %s, current version: %s", required, serverVersion) + } + return nil +} diff --git a/plugin/helpers_plugin_test.go b/plugin/helpers_plugin_test.go new file mode 100644 index 0000000000..1c026ca846 --- /dev/null +++ b/plugin/helpers_plugin_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin_test + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" + "github.com/mattermost/mattermost-server/v5/plugin/plugintest" + "github.com/mattermost/mattermost-server/v5/plugin/plugintest/mock" + "github.com/mattermost/mattermost-server/v5/utils/fileutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInstallPluginFromURL(t *testing.T) { + replace := true + + t.Run("incompatible server version", func(t *testing.T) { + h := &plugin.HelpersImpl{} + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.1.0") + h.API = api + + _, err := h.InstallPluginFromURL("", true) + + assert.Error(t, err) + assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.18.0, current version: 5.1.0", err.Error()) + }) + + t.Run("error while parsing the download url", func(t *testing.T) { + h := &plugin.HelpersImpl{} + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + h.API = api + _, err := h.InstallPluginFromURL("http://%41:8080/", replace) + + assert.Error(t, err) + assert.Equal(t, "error while parsing url: parse http://%41:8080/: invalid URL escape \"%41\"", err.Error()) + }) + + t.Run("errors out while downloading file", func(t *testing.T) { + h := &plugin.HelpersImpl{} + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + h.API = api + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusInternalServerError) + })) + defer testServer.Close() + url := testServer.URL + + _, err := h.InstallPluginFromURL(url, replace) + + assert.Error(t, err) + assert.Equal(t, "received 500 status code while downloading plugin from server", err.Error()) + }) + + t.Run("downloads the file successfully", func(t *testing.T) { + h := &plugin.HelpersImpl{} + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + h.API = api + path, _ := fileutils.FindDir("tests") + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + expectedManifest := &model.Manifest{Id: "testplugin"} + api.On("InstallPlugin", mock.Anything, false).Return(expectedManifest, nil) + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + _, _ = res.Write(tarData) + })) + defer testServer.Close() + url := testServer.URL + + manifest, err := h.InstallPluginFromURL(url, false) + + assert.NoError(t, err) + assert.Equal(t, "testplugin", manifest.Id) + }) + + t.Run("the url pointing to server is incorrect", func(t *testing.T) { + h := &plugin.HelpersImpl{} + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + h.API = api + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusNotFound) + })) + defer testServer.Close() + url := testServer.URL + + _, err := h.InstallPluginFromURL(url, false) + + assert.Error(t, err) + assert.Equal(t, "received 404 status code while downloading plugin from server", err.Error()) + }) +} diff --git a/plugin/plugintest/helpers.go b/plugin/plugintest/helpers.go index f14602eb51..e0576cd642 100644 --- a/plugin/plugintest/helpers.go +++ b/plugin/plugintest/helpers.go @@ -64,6 +64,29 @@ func (_m *Helpers) EnsureBot(bot *model.Bot, options ...plugin.EnsureBotOption) return r0, r1 } +// InstallPluginFromURL provides a mock function with given fields: downloadURL, replace +func (_m *Helpers) InstallPluginFromURL(downloadURL string, replace bool) (*model.Manifest, error) { + ret := _m.Called(downloadURL, replace) + + var r0 *model.Manifest + if rf, ok := ret.Get(0).(func(string, bool) *model.Manifest); ok { + r0 = rf(downloadURL, replace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Manifest) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, bool) error); ok { + r1 = rf(downloadURL, replace) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // KVCompareAndDeleteJSON provides a mock function with given fields: key, oldValue func (_m *Helpers) KVCompareAndDeleteJSON(key string, oldValue interface{}) (bool, error) { ret := _m.Called(key, oldValue)