[MM-2307] Move plugin helpers from mattermost-server repository into mattermost-plugin-api (#17870)

Этот коммит содержится в:
Ben Schumacher
2021-08-06 11:54:56 +02:00
коммит произвёл GitHub
родитель 28137b3048
Коммит bf1f60d309
19 изменённых файлов: 67 добавлений и 2801 удалений

Просмотреть файл

@@ -303,7 +303,6 @@ plugin-mocks: ## Creates mock files for plugins.
$(GO) get -modfile=go.tools.mod github.com/vektra/mockery/...
$(GOBIN)/mockery -dir plugin -name API -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
$(GOBIN)/mockery -dir plugin -name Hooks -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
$(GOBIN)/mockery -dir plugin -name Helpers -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
$(GOBIN)/mockery -dir plugin -name Driver -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
einterfaces-mocks: ## Creates mock files for einterfaces.

Просмотреть файл

@@ -191,7 +191,7 @@ type Manifest struct {
// RequiredConfig defines any required server configuration fields for the plugin to function properly.
//
// Use the plugin helpers CheckRequiredServerConfiguration method to enforce this.
// Use the pluginapi.Configuration.CheckRequiredServerConfiguration method to enforce this.
RequiredConfig *Config `json:"required_configuration,omitempty" yaml:"required_configuration,omitempty"`
}

Просмотреть файл

@@ -1,137 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/plugin/checker/internal/asthelpers"
"github.com/mattermost/mattermost-server/v6/plugin/checker/internal/version"
)
func checkHelpersVersionComments(pkgPath string) (result, error) {
pkg, err := asthelpers.GetPackage(pkgPath)
if err != nil {
return result{}, err
}
api, apiIdent, err := asthelpers.FindInterfaceWithIdent("API", pkg.Syntax)
if err != nil {
return result{}, err
}
apiObj := pkg.TypesInfo.ObjectOf(apiIdent)
if apiObj == nil {
return result{}, errors.New("could not find type object for API interface")
}
helpers, err := asthelpers.FindInterface("Helpers", pkg.Syntax)
if err != nil {
return result{}, err
}
apiVersions := mapMinimumVersionsByMethodName(api.Methods.List)
helpersPositions := mapPositionsByMethodName(helpers.Methods.List)
helpersVersions := mapMinimumVersionsByMethodName(helpers.Methods.List)
implMethods := asthelpers.FindReceiverMethods("HelpersImpl", pkg.Syntax)
implVersions := mapEffectiveVersionByMethod(pkg.TypesInfo, apiObj.Type(), apiVersions, implMethods)
return validateMethods(pkg.Fset, helpersPositions, helpersVersions, implVersions), nil
}
func validateMethods(
fset *token.FileSet,
helpersPositions map[string]token.Pos,
helpersVersions map[string]version.V,
implVersions map[string]version.V,
) result {
var res result
for name, helperVer := range helpersVersions {
pos := helpersPositions[name]
implVer, ok := implVersions[name]
if !ok {
res.Errors = append(res.Errors, renderWithFilePosition(
fset,
pos,
fmt.Sprintf("missing implementation for method %s", name)),
)
continue
}
if helperVer == "" {
res.Errors = append(res.Errors, renderWithFilePosition(
fset,
pos,
fmt.Sprintf("missing a minimum server version comment on method %s", name)),
)
continue
}
if helperVer == implVer {
continue
}
if helperVer.LessThan(implVer) {
res.Errors = append(res.Errors, renderWithFilePosition(
fset,
pos,
fmt.Sprintf("documented minimum server version too low on method %s", name)),
)
} else {
res.Warnings = append(res.Warnings, renderWithFilePosition(
fset,
pos,
fmt.Sprintf("documented minimum server version too high on method %s", name)),
)
}
}
return res
}
func mapEffectiveVersionByMethod(info *types.Info, apiType types.Type, versions map[string]version.V, methods []*ast.FuncDecl) map[string]version.V {
effectiveVersions := map[string]version.V{}
for _, m := range methods {
apiMethodsCalled := asthelpers.FindMethodsCalledOnType(info, apiType, m)
effectiveVersions[m.Name.Name] = getEffectiveMinimumVersion(versions, apiMethodsCalled)
}
return effectiveVersions
}
func mapMinimumVersionsByMethodName(methods []*ast.Field) map[string]version.V {
versions := map[string]version.V{}
for _, m := range methods {
versions[m.Names[0].Name] = version.V(version.ExtractMinimumVersionFromComment(m.Doc.Text()))
}
return versions
}
func mapPositionsByMethodName(methods []*ast.Field) map[string]token.Pos {
pos := map[string]token.Pos{}
for _, m := range methods {
pos[m.Names[0].Name] = m.Pos()
}
return pos
}
func getEffectiveMinimumVersion(info map[string]version.V, methods []string) version.V {
var highest version.V
for _, m := range methods {
if current, ok := info[m]; ok {
if current.GreaterThanOrEqualTo(highest) {
highest = current
}
}
}
return highest
}

Просмотреть файл

@@ -1,48 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCheckHelpersVersionComments(t *testing.T) {
testCases := []struct {
name, pkgPath string
expected result
err string
}{
{
name: "valid versions",
pkgPath: "github.com/mattermost/mattermost-server/v6/plugin/checker/internal/test/valid",
expected: result{},
},
{
name: "invalid versions",
pkgPath: "github.com/mattermost/mattermost-server/v6/plugin/checker/internal/test/invalid",
expected: result{
Errors: []string{"internal/test/invalid/invalid.go:20:2: documented minimum server version too low on method LowerVersionMethod"},
Warnings: []string{"internal/test/invalid/invalid.go:23:2: documented minimum server version too high on method HigherVersionMethod"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
res, err := checkHelpersVersionComments(tc.pkgPath)
assert.Equal(tc.expected, res)
if tc.err != "" {
assert.EqualError(err, tc.err)
} else {
assert.NoError(err)
}
})
}
}

Просмотреть файл

@@ -21,7 +21,6 @@ type checkFn func(pkgPath string) (result, error)
var checks = []checkFn{
checkAPIVersionComments,
checkHelpersVersionComments,
}
func main() {

Просмотреть файл

@@ -18,13 +18,11 @@ const (
func ClientMain(pluginImplementation interface{}) {
if impl, ok := pluginImplementation.(interface {
SetAPI(api API)
SetHelpers(helpers Helpers)
SetDriver(driver Driver)
}); !ok {
panic("Plugin implementation given must embed plugin.MattermostPlugin")
} else {
impl.SetAPI(nil)
impl.SetHelpers(nil)
impl.SetDriver(nil)
}
@@ -40,9 +38,8 @@ func ClientMain(pluginImplementation interface{}) {
type MattermostPlugin struct {
// API exposes the plugin api, and becomes available just prior to the OnActive hook.
API API
Helpers Helpers
Driver Driver
API API
Driver Driver
}
// SetAPI persists the given API interface to the plugin. It is invoked just prior to the
@@ -51,11 +48,6 @@ func (p *MattermostPlugin) SetAPI(api API) {
p.API = api
}
// SetHelpers does the same thing as SetAPI except for the plugin helpers.
func (p *MattermostPlugin) SetHelpers(helpers Helpers) {
p.Helpers = helpers
}
// SetDriver sets the RPC client implementation to talk with the server.
func (p *MattermostPlugin) SetDriver(driver Driver) {
p.Driver = driver

Просмотреть файл

@@ -284,11 +284,9 @@ func (s *hooksRPCServer) OnActivate(args *Z_OnActivateArgs, returns *Z_OnActivat
if mmplugin, ok := s.impl.(interface {
SetAPI(api API)
SetHelpers(helpers Helpers)
SetDriver(driver Driver)
}); ok {
mmplugin.SetAPI(s.apiRPCClient)
mmplugin.SetHelpers(&HelpersImpl{API: s.apiRPCClient})
mmplugin.SetDriver(dbClient)
}

64
plugin/driver.go Обычный файл
Просмотреть файл

@@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"database/sql/driver"
)
// ResultContainer contains the output from the LastInsertID
// and RowsAffected methods for a given set of rows.
// It is used to embed another round-trip to the server,
// and helping to avoid tracking results on the server.
type ResultContainer struct {
LastID int64
LastIDError error
RowsAffected int64
RowsAffectedError error
}
// Driver is a sql driver interface that is used by plugins to perform
// raw SQL queries without opening DB connections by themselves. This interface
// is not subject to backward compatibility guarantees and is only meant to be
// used by plugins built by the Mattermost team.
type Driver interface {
// Connection
Conn(isMaster bool) (string, error)
ConnPing(connID string) error
ConnClose(connID string) error
ConnQuery(connID, q string, args []driver.NamedValue) (string, error) // rows
ConnExec(connID, q string, args []driver.NamedValue) (ResultContainer, error) // result
// Transaction
Tx(connID string, opts driver.TxOptions) (string, error)
TxCommit(txID string) error
TxRollback(txID string) error
// Statement
Stmt(connID, q string) (string, error)
StmtClose(stID string) error
StmtNumInput(stID string) int
StmtQuery(stID string, args []driver.NamedValue) (string, error) // rows
StmtExec(stID string, args []driver.NamedValue) (ResultContainer, error) // result
// Rows
RowsColumns(rowsID string) []string
RowsClose(rowsID string) error
RowsNext(rowsID string, dest []driver.Value) error
RowsHasNextResultSet(rowsID string) bool
RowsNextResultSet(rowsID string) error
RowsColumnTypeDatabaseTypeName(rowsID string, index int) string
RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool)
// TODO: add this
// RowsColumnScanType(rowsID string, index int) reflect.Type
// Note: the following cannot be implemented because either MySQL or PG
// does not support it. So this implementation has to be a common subset
// of both DB implementations.
// RowsColumnTypeLength(rowsID string, index int) (int64, bool)
// RowsColumnTypeNullable(rowsID string, index int) (bool, bool)
// ResetSession(ctx context.Context) error
// IsValid() bool
}

Просмотреть файл

@@ -1,155 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"database/sql/driver"
"github.com/mattermost/mattermost-server/v6/model"
)
// Helpers provide a common patterns plugins use.
//
// Plugins obtain access to the Helpers by embedding MattermostPlugin.
type Helpers interface {
// EnsureBot either returns an existing bot user matching the given bot, or creates a bot user from the given bot.
// A profile image or icon image may be optionally passed in to be set for the existing or newly created bot.
// Returns the id of the resulting bot.
//
// Minimum server version: 5.10
EnsureBot(bot *model.Bot, options ...EnsureBotOption) (string, error)
// KVSetJSON stores a key-value pair, unique per plugin, marshalling the given value as a JSON string.
//
// Deprecated: Use p.API.KVSetWithOptions instead.
//
// Minimum server version: 5.2
KVSetJSON(key string, value interface{}) error
// KVCompareAndSetJSON updates a key-value pair, unique per plugin, but only if the current value matches the given oldValue after marshalling as a JSON string.
// Inserts a new key if oldValue == nil.
// Returns (false, err) if DB error occurred
// Returns (false, nil) if current value != oldValue or key already exists when inserting
// Returns (true, nil) if current value == oldValue or new key is inserted
//
// Deprecated: Use p.API.KVSetWithOptions instead.
//
// Minimum server version: 5.12
KVCompareAndSetJSON(key string, oldValue interface{}, newValue interface{}) (bool, error)
// KVCompareAndDeleteJSON deletes a key-value pair, unique per plugin, but only if the current value matches the given oldValue after marshalling as a JSON string.
// Returns (false, err) if DB error occurred
// Returns (false, nil) if current value != oldValue or the key was already deleted
// Returns (true, nil) if current value == oldValue
//
// Minimum server version: 5.16
KVCompareAndDeleteJSON(key string, oldValue interface{}) (bool, error)
// KVGetJSON retrieves a value based on the key, unique per plugin, unmarshalling the previously set JSON string into the given value. Returns true if the key exists.
//
// Minimum server version: 5.2
KVGetJSON(key string, value interface{}) (bool, error)
// KVSetWithExpiryJSON stores a key-value pair with an expiry time, unique per plugin, marshalling the given value as a JSON string.
//
// Deprecated: Use p.API.KVSetWithOptions instead.
//
// Minimum server version: 5.6
KVSetWithExpiryJSON(key string, value interface{}, expireInSeconds int64) error
// KVListWithOptions returns all keys that match the given options. If no options are provided then all keys are returned.
//
// Minimum server version: 5.6
KVListWithOptions(options ...KVListOption) ([]string, error)
// CheckRequiredServerConfiguration checks if the server is configured according to
// plugin requirements.
//
// Minimum server version: 5.2
CheckRequiredServerConfiguration(req *model.Config) (bool, error)
// ShouldProcessMessage returns if the message should be processed by a message hook.
//
// Use this method to avoid processing unnecessary messages in a MessageHasBeenPosted
// or MessageWillBePosted hook, and indeed in some cases avoid an infinite loop between
// two automated bots or plugins.
//
// The behaviour is customizable using the given options, since plugin needs may vary.
// By default, system messages and messages from bots will be skipped.
//
// 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)
// GetPluginAssetURL builds a URL to the given asset in the assets directory.
// Use this URL to link to assets from the webapp, or for third-party integrations with your plugin.
//
// Minimum server version: 5.2
GetPluginAssetURL(pluginID, asset string) (string, error)
}
// HelpersImpl implements the helpers interface with an API that retrieves data on behalf of the plugin.
type HelpersImpl struct {
API API
}
// ResultContainer contains the output from the LastInsertID
// and RowsAffected methods for a given set of rows.
// It is used to embed another round-trip to the server,
// and helping to avoid tracking results on the server.
type ResultContainer struct {
LastID int64
LastIDError error
RowsAffected int64
RowsAffectedError error
}
// Driver is a sql driver interface that is used by plugins to perform
// raw SQL queries without opening DB connections by themselves. This interface
// is not subject to backward compatibility guarantees and is only meant to be
// used by plugins built by the Mattermost team.
type Driver interface {
// Connection
Conn(isMaster bool) (string, error)
ConnPing(connID string) error
ConnClose(connID string) error
ConnQuery(connID, q string, args []driver.NamedValue) (string, error) // rows
ConnExec(connID, q string, args []driver.NamedValue) (ResultContainer, error) // result
// Transaction
Tx(connID string, opts driver.TxOptions) (string, error)
TxCommit(txID string) error
TxRollback(txID string) error
// Statement
Stmt(connID, q string) (string, error)
StmtClose(stID string) error
StmtNumInput(stID string) int
StmtQuery(stID string, args []driver.NamedValue) (string, error) // rows
StmtExec(stID string, args []driver.NamedValue) (ResultContainer, error) // result
// Rows
RowsColumns(rowsID string) []string
RowsClose(rowsID string) error
RowsNext(rowsID string, dest []driver.Value) error
RowsHasNextResultSet(rowsID string) bool
RowsNextResultSet(rowsID string) error
RowsColumnTypeDatabaseTypeName(rowsID string, index int) string
RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool)
// TODO: add this
// RowsColumnScanType(rowsID string, index int) reflect.Type
// Note: the following cannot be implemented because either MySQL or PG
// does not support it. So this implementation has to be a common subset
// of both DB implementations.
// RowsColumnTypeLength(rowsID string, index int) (int64, bool)
// RowsColumnTypeNullable(rowsID string, index int) (bool, bool)
// ResetSession(ctx context.Context) error
// IsValid() bool
}

Просмотреть файл

@@ -1,318 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"io/ioutil"
"path/filepath"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/utils"
)
type ensureBotOptions struct {
ProfileImagePath string
IconImagePath string
}
type EnsureBotOption func(*ensureBotOptions)
func ProfileImagePath(path string) EnsureBotOption {
return func(args *ensureBotOptions) {
args.ProfileImagePath = path
}
}
func IconImagePath(path string) EnsureBotOption {
return func(args *ensureBotOptions) {
args.IconImagePath = path
}
}
// EnsureBot implements Helpers.EnsureBot
func (p *HelpersImpl) EnsureBot(bot *model.Bot, options ...EnsureBotOption) (retBotID string, retErr error) {
err := p.ensureServerVersion("5.10.0")
if err != nil {
return "", errors.Wrap(err, "failed to ensure bot")
}
// Default options
o := &ensureBotOptions{
ProfileImagePath: "",
IconImagePath: "",
}
for _, setter := range options {
setter(o)
}
botID, err := p.ensureBot(bot)
if err != nil {
return "", err
}
err = p.setBotImages(botID, o.ProfileImagePath, o.IconImagePath)
if err != nil {
return "", err
}
return botID, nil
}
type ShouldProcessMessageOption func(*shouldProcessMessageOptions)
type shouldProcessMessageOptions struct {
AllowSystemMessages bool
AllowBots bool
AllowWebhook bool
FilterChannelIDs []string
FilterUserIDs []string
OnlyBotDMs bool
BotID string
}
// AllowSystemMessages configures a call to ShouldProcessMessage to return true for system messages.
//
// As it is typically desirable only to consume messages from users of the system, ShouldProcessMessage ignores system messages by default.
func AllowSystemMessages() ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.AllowSystemMessages = true
}
}
// AllowBots configures a call to ShouldProcessMessage to return true for bot posts.
//
// As it is typically desirable only to consume messages from human users of the system, ShouldProcessMessage ignores bot messages by default. When allowing bots, take care to avoid a loop where two plugins respond to each others posts repeatedly.
func AllowBots() ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.AllowBots = true
}
}
// AllowWebhook configures a call to ShouldProcessMessage to return true for posts from webhook.
//
// As it is typically desirable only to consume messages from human users of the system, ShouldProcessMessage ignores webhook messages by default.
func AllowWebhook() ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.AllowWebhook = true
}
}
// FilterChannelIDs configures a call to ShouldProcessMessage to return true only for the given channels.
//
// By default, posts from all channels are allowed to be processed.
func FilterChannelIDs(filterChannelIDs []string) ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.FilterChannelIDs = filterChannelIDs
}
}
// FilterUserIDs configures a call to ShouldProcessMessage to return true only for the given users.
//
// By default, posts from all non-bot users are allowed.
func FilterUserIDs(filterUserIDs []string) ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.FilterUserIDs = filterUserIDs
}
}
// OnlyBotDMs configures a call to ShouldProcessMessage to return true only for direct messages sent to the bot created by EnsureBot.
//
// By default, posts from all channels are allowed.
func OnlyBotDMs() ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.OnlyBotDMs = true
}
}
// If provided, BotID configures ShouldProcessMessage to skip its retrieval from the store.
//
// By default, posts from all non-bot users are allowed.
func BotID(botID string) ShouldProcessMessageOption {
return func(options *shouldProcessMessageOptions) {
options.BotID = botID
}
}
// ShouldProcessMessage implements Helpers.ShouldProcessMessage
func (p *HelpersImpl) ShouldProcessMessage(post *model.Post, options ...ShouldProcessMessageOption) (bool, error) {
messageProcessOptions := &shouldProcessMessageOptions{}
for _, option := range options {
option(messageProcessOptions)
}
var botIDBytes []byte
var kvGetErr *model.AppError
if messageProcessOptions.BotID != "" {
botIDBytes = []byte(messageProcessOptions.BotID)
} else {
botIDBytes, kvGetErr = p.API.KVGet(BotUserKey)
if kvGetErr != nil {
return false, errors.Wrap(kvGetErr, "failed to get bot")
}
}
if botIDBytes != nil {
if post.UserId == string(botIDBytes) {
return false, nil
}
}
if post.IsSystemMessage() && !messageProcessOptions.AllowSystemMessages {
return false, nil
}
if !messageProcessOptions.AllowWebhook && post.GetProp("from_webhook") == "true" {
return false, nil
}
if !messageProcessOptions.AllowBots {
user, appErr := p.API.GetUser(post.UserId)
if appErr != nil {
return false, errors.Wrap(appErr, "unable to get user")
}
if user.IsBot {
return false, nil
}
}
if len(messageProcessOptions.FilterChannelIDs) != 0 && !utils.StringInSlice(post.ChannelId, messageProcessOptions.FilterChannelIDs) {
return false, nil
}
if len(messageProcessOptions.FilterUserIDs) != 0 && !utils.StringInSlice(post.UserId, messageProcessOptions.FilterUserIDs) {
return false, nil
}
if botIDBytes != nil && messageProcessOptions.OnlyBotDMs {
channel, appErr := p.API.GetChannel(post.ChannelId)
if appErr != nil {
return false, errors.Wrap(appErr, "unable to get channel")
}
if !model.IsBotDMChannel(channel, string(botIDBytes)) {
return false, nil
}
}
return true, nil
}
func (p *HelpersImpl) readFile(path string) ([]byte, error) {
bundlePath, err := p.API.GetBundlePath()
if err != nil {
return nil, errors.Wrap(err, "failed to get bundle path")
}
imageBytes, err := ioutil.ReadFile(filepath.Join(bundlePath, path))
if err != nil {
return nil, errors.Wrap(err, "failed to read image")
}
return imageBytes, nil
}
func (p *HelpersImpl) ensureBot(bot *model.Bot) (retBotID string, retErr error) {
// Must provide a bot with a username
if bot == nil || len(bot.Username) < 1 {
return "", errors.New("passed a bad bot, nil or no username")
}
// If we fail for any reason, this could be a race between creation of bot and
// retrieval from another EnsureBot. Just try the basic retrieve existing again.
defer func() {
if retBotID == "" || retErr != nil {
var err error
var botIDBytes []byte
err = utils.ProgressiveRetry(func() error {
var appErr *model.AppError
botIDBytes, appErr = p.API.KVGet(BotUserKey)
if appErr != nil {
return appErr
}
return nil
})
if err == nil && botIDBytes != nil {
retBotID = string(botIDBytes)
retErr = nil
}
}
}()
botIDBytes, kvGetErr := p.API.KVGet(BotUserKey)
if kvGetErr != nil {
return "", errors.Wrap(kvGetErr, "failed to get bot")
}
// If the bot has already been created, use it
if botIDBytes != nil {
botID := string(botIDBytes)
// ensure existing bot is synced with what is being created
botPatch := &model.BotPatch{
Username: &bot.Username,
DisplayName: &bot.DisplayName,
Description: &bot.Description,
}
if _, err := p.API.PatchBot(botID, botPatch); err != nil {
return "", errors.Wrap(err, "failed to patch bot")
}
return botID, nil
}
// Check for an existing bot user with that username. If one exists, then use that.
if user, userGetErr := p.API.GetUserByUsername(bot.Username); userGetErr == nil && user != nil {
if user.IsBot {
if kvSetErr := p.API.KVSet(BotUserKey, []byte(user.Id)); kvSetErr != nil {
p.API.LogWarn("Failed to set claimed bot user id.", "userid", user.Id, "err", kvSetErr)
}
} else {
p.API.LogError("Plugin attempted to use an account that already exists. Convert user to a bot account in the CLI by running 'mattermost user convert <username> --bot'. If the user is an existing user account you want to preserve, change its username and restart the Mattermost server, after which the plugin will create a bot account with that name. For more information about bot accounts, see https://mattermost.com/pl/default-bot-accounts", "username", bot.Username, "user_id", user.Id)
}
return user.Id, nil
}
// Create a new bot user for the plugin
createdBot, createBotErr := p.API.CreateBot(bot)
if createBotErr != nil {
return "", errors.Wrap(createBotErr, "failed to create bot")
}
if kvSetErr := p.API.KVSet(BotUserKey, []byte(createdBot.UserId)); kvSetErr != nil {
p.API.LogWarn("Failed to set created bot user id.", "userid", createdBot.UserId, "err", kvSetErr)
}
return createdBot.UserId, nil
}
func (p *HelpersImpl) setBotImages(botID, profileImagePath, iconImagePath string) error {
if profileImagePath != "" {
imageBytes, err := p.readFile(profileImagePath)
if err != nil {
return errors.Wrap(err, "failed to read profile image")
}
appErr := p.API.SetProfileImage(botID, imageBytes)
if appErr != nil {
return errors.Wrap(appErr, "failed to set profile image")
}
}
if iconImagePath != "" {
imageBytes, err := p.readFile(iconImagePath)
if err != nil {
return errors.Wrap(err, "failed to read icon image")
}
appErr := p.API.SetBotIconImage(botID, imageBytes)
if appErr != nil {
return errors.Wrap(appErr, "failed to set icon image")
}
}
return nil
}

Просмотреть файл

@@ -1,579 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin_test
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
)
func TestEnsureBot(t *testing.T) {
setupAPI := func() *plugintest.API {
return &plugintest.API{}
}
testbot := &model.Bot{
Username: "testbot",
DisplayName: "Test Bot",
Description: "testbotdescription",
}
t.Run("server version incompatible", func(t *testing.T) {
api := setupAPI()
api.On("GetServerVersion").Return("5.9.0")
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
_, retErr := p.EnsureBot(nil)
assert.Error(t, retErr)
assert.Equal(t, "failed to ensure bot: incompatible server version for plugin, minimum required version: 5.10.0, current version: 5.9.0", retErr.Error())
})
t.Run("bad parameters", func(t *testing.T) {
t.Run("no bot", func(t *testing.T) {
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(nil)
assert.Equal(t, "", botID)
assert.Error(t, err)
})
t.Run("bad username", func(t *testing.T) {
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(&model.Bot{
Username: "",
})
assert.Equal(t, "", botID)
assert.Error(t, err)
})
})
t.Run("if bot already exists", func(t *testing.T) {
t.Run("should find and return the existing bot ID", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
api.On("PatchBot", expectedBotID, &model.BotPatch{
Username: &testbot.Username,
DisplayName: &testbot.DisplayName,
Description: &testbot.Description,
}).Return(nil, nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot)
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should return an error if unable to get bot", func(t *testing.T) {
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return(nil, &model.AppError{})
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot)
assert.Equal(t, "", botID)
assert.Error(t, err)
})
t.Run("should set the bot profile image when specified", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetProfileImage", expectedBotID, imageBytes).Return(nil)
api.On("GetServerVersion").Return("5.10.0")
api.On("PatchBot", expectedBotID, &model.BotPatch{
Username: &testbot.Username,
DisplayName: &testbot.DisplayName,
Description: &testbot.Description,
}).Return(nil, nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
assert.NoError(t, err)
botID, err := p.EnsureBot(testbot, plugin.ProfileImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should set the bot icon image when specified", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
assert.NoError(t, err)
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetBotIconImage", expectedBotID, imageBytes).Return(nil)
api.On("GetServerVersion").Return("5.10.0")
api.On("PatchBot", expectedBotID, &model.BotPatch{
Username: &testbot.Username,
DisplayName: &testbot.DisplayName,
Description: &testbot.Description,
}).Return(nil, nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot, plugin.IconImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should set both the profile image and bot icon image when specified", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
assert.NoError(t, err)
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetProfileImage", expectedBotID, imageBytes).Return(nil)
api.On("SetBotIconImage", expectedBotID, imageBytes).Return(nil)
api.On("GetServerVersion").Return("5.10.0")
api.On("PatchBot", expectedBotID, &model.BotPatch{
Username: &testbot.Username,
DisplayName: &testbot.DisplayName,
Description: &testbot.Description,
}).Return(nil, nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot, plugin.ProfileImagePath(testImage), plugin.IconImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should find and update the bot with new bot details", func(t *testing.T) {
expectedBotID := model.NewId()
expectedBotUsername := "updated_testbot"
expectedBotDisplayName := "Updated Test Bot"
expectedBotDescription := "updated testbotdescription"
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
assert.NoError(t, err)
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetProfileImage", expectedBotID, imageBytes).Return(nil)
api.On("SetBotIconImage", expectedBotID, imageBytes).Return(nil)
api.On("PatchBot", expectedBotID, &model.BotPatch{
Username: &expectedBotUsername,
DisplayName: &expectedBotDisplayName,
Description: &expectedBotDescription,
}).Return(nil, nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
updatedTestbot := &model.Bot{
Username: "updated_testbot",
DisplayName: "Updated Test Bot",
Description: "updated testbotdescription",
}
botID, err := p.EnsureBot(updatedTestbot, plugin.ProfileImagePath(testImage), plugin.IconImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
})
t.Run("if bot doesn't exist", func(t *testing.T) {
t.Run("should create the bot and return the ID", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(nil, nil)
api.On("CreateBot", testbot).Return(&model.Bot{
UserId: expectedBotID,
}, nil)
api.On("KVSet", plugin.BotUserKey, []byte(expectedBotID)).Return(nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot)
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should claim existing bot and return the ID", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(&model.User{
Id: expectedBotID,
IsBot: true,
}, nil)
api.On("KVSet", plugin.BotUserKey, []byte(expectedBotID)).Return(nil)
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot)
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should return the non-bot account but log a message if user exists with the same name and is not a bot", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(&model.User{
Id: expectedBotID,
IsBot: false,
}, nil)
api.On("LogError", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot)
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should fail if create bot fails", func(t *testing.T) {
api := setupAPI()
api.On("GetServerVersion").Return("5.10.0")
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(nil, nil)
api.On("CreateBot", testbot).Return(nil, &model.AppError{})
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot)
assert.Equal(t, "", botID)
assert.Error(t, err)
})
t.Run("should create bot and set the bot profile image when specified", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
assert.NoError(t, err)
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(nil, nil)
api.On("CreateBot", testbot).Return(&model.Bot{
UserId: expectedBotID,
}, nil)
api.On("KVSet", plugin.BotUserKey, []byte(expectedBotID)).Return(nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetProfileImage", expectedBotID, imageBytes).Return(nil)
api.On("GetServerVersion").Return("5.10.0")
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot, plugin.ProfileImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should create bot and set the bot icon image when specified", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
assert.NoError(t, err)
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(nil, nil)
api.On("CreateBot", testbot).Return(&model.Bot{
UserId: expectedBotID,
}, nil)
api.On("KVSet", plugin.BotUserKey, []byte(expectedBotID)).Return(nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetBotIconImage", expectedBotID, imageBytes).Return(nil)
api.On("GetServerVersion").Return("5.10.0")
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot, plugin.IconImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
t.Run("should create bot and set both the profile image and bot icon image when specified", func(t *testing.T) {
expectedBotID := model.NewId()
api := setupAPI()
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
imageBytes, err := ioutil.ReadFile(testImage)
assert.NoError(t, err)
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
api.On("GetUserByUsername", testbot.Username).Return(nil, nil)
api.On("CreateBot", testbot).Return(&model.Bot{
UserId: expectedBotID,
}, nil)
api.On("KVSet", plugin.BotUserKey, []byte(expectedBotID)).Return(nil)
api.On("GetBundlePath").Return("", nil)
api.On("SetProfileImage", expectedBotID, imageBytes).Return(nil)
api.On("SetBotIconImage", expectedBotID, imageBytes).Return(nil)
api.On("GetServerVersion").Return("5.10.0")
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
botID, err := p.EnsureBot(testbot, plugin.ProfileImagePath(testImage), plugin.IconImagePath(testImage))
assert.Equal(t, expectedBotID, botID)
assert.NoError(t, err)
})
})
}
func TestShouldProcessMessage(t *testing.T) {
p := &plugin.HelpersImpl{}
expectedBotID := model.NewId()
setupAPI := func() *plugintest.API {
return &plugintest.API{}
}
t.Run("should not respond to itself", func(t *testing.T) {
api := setupAPI()
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
p.API = api
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{Type: model.PostTypeHeaderChange, UserId: expectedBotID}, plugin.AllowSystemMessages(), plugin.AllowBots())
assert.False(t, shouldProcessMessage)
})
t.Run("should not process as the post is generated by system", func(t *testing.T) {
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{Type: model.PostTypeHeaderChange})
assert.False(t, shouldProcessMessage)
})
t.Run("should not process as the post is sent to another channel", func(t *testing.T) {
channelID := "channel-id"
api := setupAPI()
api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil)
p.API = api
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{ChannelId: channelID}, plugin.AllowSystemMessages(), plugin.AllowBots(), plugin.FilterChannelIDs([]string{"another-channel-id"}))
assert.False(t, shouldProcessMessage)
})
t.Run("should not process as the post is created by bot", func(t *testing.T) {
userID := "user-id"
channelID := "1"
api := setupAPI()
p.API = api
api.On("GetUser", userID).Return(&model.User{IsBot: true}, nil)
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{UserId: userID, ChannelId: channelID},
plugin.AllowSystemMessages(), plugin.FilterUserIDs([]string{"another-user-id"}))
assert.False(t, shouldProcessMessage)
})
t.Run("should not process the message as the post is not in bot dm channel", func(t *testing.T) {
userID := "user-id"
channelID := "1"
channel := model.Channel{
Name: "user1__" + expectedBotID,
Type: model.ChannelTypeOpen,
}
api := setupAPI()
api.On("GetChannel", channelID).Return(&channel, nil)
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
p.API = api
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{UserId: userID, ChannelId: channelID}, plugin.AllowSystemMessages(), plugin.AllowBots(), plugin.OnlyBotDMs())
assert.False(t, shouldProcessMessage)
})
t.Run("should process the message", func(t *testing.T) {
channelID := "1"
api := setupAPI()
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
p.API = api
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{UserId: "1", Type: model.PostTypeHeaderChange, ChannelId: channelID},
plugin.AllowSystemMessages(), plugin.FilterChannelIDs([]string{channelID}), plugin.AllowBots(), plugin.FilterUserIDs([]string{"1"}))
assert.True(t, shouldProcessMessage)
})
t.Run("should process the message for plugin without a bot", func(t *testing.T) {
channelID := "1"
api := setupAPI()
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
p.API = api
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{UserId: "1", Type: model.PostTypeHeaderChange, ChannelId: channelID},
plugin.AllowSystemMessages(), plugin.FilterChannelIDs([]string{channelID}), plugin.AllowBots(), plugin.FilterUserIDs([]string{"1"}))
assert.True(t, shouldProcessMessage)
})
t.Run("should process the message when filter channel and filter users list is empty", func(t *testing.T) {
channelID := "1"
api := setupAPI()
channel := model.Channel{
Name: "user1__" + expectedBotID,
Type: model.ChannelTypeDirect,
}
api.On("GetChannel", channelID).Return(&channel, nil)
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
p.API = api
shouldProcessMessage, _ := p.ShouldProcessMessage(&model.Post{UserId: "1", Type: model.PostTypeHeaderChange, ChannelId: channelID},
plugin.AllowSystemMessages(), plugin.AllowBots())
assert.True(t, shouldProcessMessage)
})
t.Run("should not process the message which have from_webhook", func(t *testing.T) {
channelID := "1"
api := setupAPI()
api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil)
p.API = api
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
shouldProcessMessage, err := p.ShouldProcessMessage(&model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "true"}}, plugin.AllowBots())
assert.False(t, shouldProcessMessage)
assert.NoError(t, err)
})
t.Run("should process the message which have from_webhook with allow webhook plugin", func(t *testing.T) {
channelID := "1"
api := setupAPI()
api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil)
p.API = api
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
shouldProcessMessage, err := p.ShouldProcessMessage(&model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "true"}}, plugin.AllowBots(), plugin.AllowWebhook())
assert.NoError(t, err)
assert.True(t, shouldProcessMessage)
})
t.Run("should process the message where from_webhook is not set", func(t *testing.T) {
channelID := "1"
api := setupAPI()
api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil)
p.API = api
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
shouldProcessMessage, err := p.ShouldProcessMessage(&model.Post{ChannelId: channelID}, plugin.AllowBots())
assert.NoError(t, err)
assert.True(t, shouldProcessMessage)
})
t.Run("should process the message which have from_webhook false", func(t *testing.T) {
channelID := "1"
api := setupAPI()
api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil)
p.API = api
api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil)
shouldProcessMessage, err := p.ShouldProcessMessage(&model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "false"}}, plugin.AllowBots())
assert.NoError(t, err)
assert.True(t, shouldProcessMessage)
})
t.Run("should process the message when we pass the botId as input", func(t *testing.T) {
userID := "user-id"
channelID := "1"
api := setupAPI()
api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil)
p.API = api
api.On("GetUser", userID).Return(&model.User{IsBot: false}, nil)
// we should skip the store Get
api.On("KVGet", plugin.BotUserKey).Return(nil, nil)
shouldProcessMessage, err := p.ShouldProcessMessage(&model.Post{ChannelId: channelID, UserId: userID}, plugin.BotID(expectedBotID))
assert.NoError(t, err)
assert.True(t, shouldProcessMessage)
})
}

Просмотреть файл

@@ -1,46 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"bytes"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/utils"
)
// CheckRequiredServerConfiguration implements Helpers.CheckRequiredServerConfiguration
func (p *HelpersImpl) CheckRequiredServerConfiguration(req *model.Config) (bool, error) {
if req == nil {
return true, nil
}
cfg := p.API.GetConfig()
mc, err := utils.Merge(cfg, req, nil)
if err != nil {
return false, errors.Wrap(err, "could not merge configurations")
}
mergedCfg := mc.(model.Config)
cfgBuf, err := json.Marshal(cfg)
if err != nil {
return false, fmt.Errorf("failed to marshal config: %v", err)
}
mergedCfgBuf, err := json.Marshal(mergedCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal merged config: %v", err)
}
if !bytes.Equal(cfgBuf, mergedCfgBuf) {
return false, nil
}
return true, nil
}

Просмотреть файл

@@ -1,123 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
)
func TestCheckRequiredServerConfiguration(t *testing.T) {
for name, test := range map[string]struct {
SetupAPI func(*plugintest.API) *plugintest.API
Input *model.Config
ShouldReturn bool
ShouldError bool
}{
"no required config therefore it should be compatible": {
SetupAPI: func(api *plugintest.API) *plugintest.API {
return api
},
Input: nil,
ShouldReturn: true,
ShouldError: false,
},
"contains required configuration": {
SetupAPI: func(api *plugintest.API) *plugintest.API {
api.On("GetConfig").Return(&model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(true),
},
TeamSettings: model.TeamSettings{
EnableUserCreation: model.NewBool(true),
},
})
return api
},
Input: &model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(true),
},
},
ShouldReturn: true,
ShouldError: false,
},
"does not contain required configuration": {
SetupAPI: func(api *plugintest.API) *plugintest.API {
api.On("GetConfig").Return(&model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(true),
},
})
return api
},
Input: &model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(true),
},
TeamSettings: model.TeamSettings{
EnableUserCreation: model.NewBool(true),
},
},
ShouldReturn: false,
ShouldError: false,
},
"different configurations": {
SetupAPI: func(api *plugintest.API) *plugintest.API {
api.On("GetConfig").Return(&model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(false),
},
})
return api
},
Input: &model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(true),
},
},
ShouldReturn: false,
ShouldError: false,
},
"non-existent configuration": {
SetupAPI: func(api *plugintest.API) *plugintest.API {
api.On("GetConfig").Return(&model.Config{})
return api
},
Input: &model.Config{
ServiceSettings: model.ServiceSettings{
EnableCommands: model.NewBool(true),
},
},
ShouldReturn: false,
ShouldError: false,
},
} {
t.Run(name, func(t *testing.T) {
api := test.SetupAPI(&plugintest.API{})
defer api.AssertExpectations(t)
p := &plugin.HelpersImpl{}
p.API = api
ok, err := p.CheckRequiredServerConfiguration(test.Input)
assert.Equal(t, test.ShouldReturn, ok)
if test.ShouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

Просмотреть файл

@@ -1,222 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"encoding/json"
"strings"
"github.com/pkg/errors"
)
// KVSetJSON implements Helpers.KVSetJSON.
func (p *HelpersImpl) KVSetJSON(key string, value interface{}) error {
err := p.ensureServerVersion("5.2.0")
if err != nil {
return err
}
data, err := json.Marshal(value)
if err != nil {
return err
}
appErr := p.API.KVSet(key, data)
if appErr != nil {
return appErr
}
return nil
}
// KVCompareAndSetJSON implements Helpers.KVCompareAndSetJSON.
func (p *HelpersImpl) KVCompareAndSetJSON(key string, oldValue interface{}, newValue interface{}) (bool, error) {
var err error
err = p.ensureServerVersion("5.12.0")
if err != nil {
return false, err
}
var oldData, newData []byte
if oldValue != nil {
oldData, err = json.Marshal(oldValue)
if err != nil {
return false, errors.Wrap(err, "unable to marshal old value")
}
}
if newValue != nil {
newData, err = json.Marshal(newValue)
if err != nil {
return false, errors.Wrap(err, "unable to marshal new value")
}
}
set, appErr := p.API.KVCompareAndSet(key, oldData, newData)
if appErr != nil {
return set, appErr
}
return set, nil
}
// KVCompareAndDeleteJSON implements Helpers.KVCompareAndDeleteJSON.
func (p *HelpersImpl) KVCompareAndDeleteJSON(key string, oldValue interface{}) (bool, error) {
var err error
err = p.ensureServerVersion("5.16.0")
if err != nil {
return false, err
}
var oldData []byte
if oldValue != nil {
oldData, err = json.Marshal(oldValue)
if err != nil {
return false, errors.Wrap(err, "unable to marshal old value")
}
}
deleted, appErr := p.API.KVCompareAndDelete(key, oldData)
if appErr != nil {
return deleted, appErr
}
return deleted, nil
}
// KVGetJSON implements Helpers.KVGetJSON.
func (p *HelpersImpl) KVGetJSON(key string, value interface{}) (bool, error) {
err := p.ensureServerVersion("5.2.0")
if err != nil {
return false, err
}
data, appErr := p.API.KVGet(key)
if appErr != nil {
return false, appErr
}
if data == nil {
return false, nil
}
err = json.Unmarshal(data, value)
if err != nil {
return false, err
}
return true, nil
}
// KVSetWithExpiryJSON is a wrapper around KVSetWithExpiry to simplify atomically writing a JSON object with expiry to the key value store.
func (p *HelpersImpl) KVSetWithExpiryJSON(key string, value interface{}, expireInSeconds int64) error {
err := p.ensureServerVersion("5.6.0")
if err != nil {
return err
}
data, err := json.Marshal(value)
if err != nil {
return err
}
appErr := p.API.KVSetWithExpiry(key, data, expireInSeconds)
if appErr != nil {
return appErr
}
return nil
}
type kvListOptions struct {
checkers []func(key string) (keep bool, err error)
}
func (o *kvListOptions) checkAll(key string) (keep bool, err error) {
for _, check := range o.checkers {
keep, err := check(key)
if err != nil {
return false, err
}
if !keep {
return false, nil
}
}
// key made it through all checkers
return true, nil
}
// KVListOption represents a single input option for KVListWithOptions
type KVListOption func(*kvListOptions)
// WithPrefix only return keys that start with the given string.
func WithPrefix(prefix string) KVListOption {
return WithChecker(func(key string) (keep bool, err error) {
return strings.HasPrefix(key, prefix), nil
})
}
// WithChecker allows for a custom filter function to determine which keys to return.
// Returning true will keep the key and false will filter it out. Returning an error
// will halt KVListWithOptions immediately and pass the error up (with no other results).
func WithChecker(f func(key string) (keep bool, err error)) KVListOption {
return func(args *kvListOptions) {
args.checkers = append(args.checkers, f)
}
}
// kvListPerPage is the number of keys KVListWithOptions gets per request
const kvListPerPage = 100
// KVListWithOptions implements Helpers.KVListWithOptions.
func (p *HelpersImpl) KVListWithOptions(options ...KVListOption) ([]string, error) {
err := p.ensureServerVersion("5.6.0")
if err != nil {
return nil, err
}
// convert functional options into args struct
args := &kvListOptions{}
for _, opt := range options {
opt(args)
}
ret := make([]string, 0)
// get our keys a batch at a time, filter out the ones we don't want based on our args
// any errors will hault the whole process and return the error raw
for i := 0; ; i++ {
keys, appErr := p.API.KVList(i, kvListPerPage)
if appErr != nil {
return nil, appErr
}
if len(args.checkers) == 0 {
// no checkers, just append the whole block at once
ret = append(ret, keys...)
} else {
// we have a filter, so check each key, all checkers must say key
// for us to keep a key
for _, key := range keys {
keep, err := args.checkAll(key)
if err != nil {
return nil, err
}
if !keep {
continue
}
// didn't get filtered out, add to our return
ret = append(ret, key)
}
}
if len(keys) < kvListPerPage {
break
}
}
return ret, nil
}

Просмотреть файл

@@ -1,639 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin_test
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
)
func TestKVGetJSON(t *testing.T) {
t.Run("incompatible server version", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.1.0")
p := &plugin.HelpersImpl{API: api}
var dat map[string]interface{}
ok, err := p.KVGetJSON("test-key", dat)
api.AssertExpectations(t)
assert.False(t, ok)
assert.Error(t, err)
assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.2.0, current version: 5.1.0", err.Error())
})
t.Run("KVGet error", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.2.0")
api.On("KVGet", "test-key").Return(nil, &model.AppError{})
p.API = api
var dat map[string]interface{}
ok, err := p.KVGetJSON("test-key", dat)
api.AssertExpectations(t)
assert.False(t, ok)
assert.Error(t, err)
assert.Nil(t, dat)
})
t.Run("unknown key", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.2.0")
api.On("KVGet", "test-key").Return(nil, nil)
p.API = api
var dat map[string]interface{}
ok, err := p.KVGetJSON("test-key", dat)
api.AssertExpectations(t)
assert.False(t, ok)
assert.NoError(t, err)
assert.Nil(t, dat)
})
t.Run("malformed JSON", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.2.0")
api.On("KVGet", "test-key").Return([]byte(`{{:}"val-a": 10}`), nil)
p.API = api
var dat map[string]interface{}
ok, err := p.KVGetJSON("test-key", &dat)
api.AssertExpectations(t)
assert.False(t, ok)
assert.Error(t, err)
assert.Nil(t, dat)
})
t.Run("wellformed JSON", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.2.0")
api.On("KVGet", "test-key").Return([]byte(`{"val-a": 10}`), nil)
p.API = api
var dat map[string]interface{}
ok, err := p.KVGetJSON("test-key", &dat)
assert.True(t, ok)
api.AssertExpectations(t)
assert.NoError(t, err)
assert.Equal(t, map[string]interface{}{
"val-a": float64(10),
}, dat)
})
}
func TestKVSetJSON(t *testing.T) {
t.Run("incompatible server version", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.1.0")
p := &plugin.HelpersImpl{API: api}
err := p.KVSetJSON("test-key", map[string]interface{}{
"val-a": float64(10),
})
api.AssertExpectations(t)
assert.Error(t, err)
assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.2.0, current version: 5.1.0", err.Error())
})
t.Run("JSON marshal error", func(t *testing.T) {
api := &plugintest.API{}
api.AssertNotCalled(t, "KVSet")
api.On("GetServerVersion").Return("5.2.0")
p := &plugin.HelpersImpl{API: api}
err := p.KVSetJSON("test-key", func() {})
api.AssertExpectations(t)
assert.Error(t, err)
})
t.Run("KVSet error", func(t *testing.T) {
api := &plugintest.API{}
api.On("KVSet", "test-key", []byte(`{"val-a":10}`)).Return(&model.AppError{})
api.On("GetServerVersion").Return("5.2.0")
p := &plugin.HelpersImpl{API: api}
err := p.KVSetJSON("test-key", map[string]interface{}{
"val-a": float64(10),
})
api.AssertExpectations(t)
assert.Error(t, err)
})
t.Run("marshallable struct", func(t *testing.T) {
api := &plugintest.API{}
api.On("KVSet", "test-key", []byte(`{"val-a":10}`)).Return(nil)
api.On("GetServerVersion").Return("5.2.0")
p := &plugin.HelpersImpl{API: api}
err := p.KVSetJSON("test-key", map[string]interface{}{
"val-a": float64(10),
})
api.AssertExpectations(t)
assert.NoError(t, err)
})
}
func TestKVCompareAndSetJSON(t *testing.T) {
t.Run("incompatible server version", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.10.0")
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", nil, map[string]interface{}{
"val-b": 20,
})
assert.Equal(t, false, ok)
assert.Error(t, err)
assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.12.0, current version: 5.10.0", err.Error())
})
t.Run("old value JSON marshal error", func(t *testing.T) {
api := &plugintest.API{}
api.AssertNotCalled(t, "KVCompareAndSet")
api.On("GetServerVersion").Return("5.12.0")
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", func() {}, map[string]interface{}{})
api.AssertExpectations(t)
assert.Equal(t, false, ok)
assert.Error(t, err)
})
t.Run("new value JSON marshal error", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.12.0")
api.AssertNotCalled(t, "KVCompareAndSet")
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", map[string]interface{}{}, func() {})
api.AssertExpectations(t)
assert.False(t, ok)
assert.Error(t, err)
})
t.Run("KVCompareAndSet error", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.12.0")
api.On("KVCompareAndSet", "test-key", []byte(`{"val-a":10}`), []byte(`{"val-b":20}`)).Return(false, &model.AppError{})
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", map[string]interface{}{
"val-a": 10,
}, map[string]interface{}{
"val-b": 20,
})
api.AssertExpectations(t)
assert.False(t, ok)
assert.Error(t, err)
})
t.Run("old value nil", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.12.0")
api.On("KVCompareAndSet", "test-key", []byte(nil), []byte(`{"val-b":20}`)).Return(true, nil)
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", nil, map[string]interface{}{
"val-b": 20,
})
api.AssertExpectations(t)
assert.True(t, ok)
assert.NoError(t, err)
})
t.Run("old value non-nil", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.12.0")
api.On("KVCompareAndSet", "test-key", []byte(`{"val-a":10}`), []byte(`{"val-b":20}`)).Return(true, nil)
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", map[string]interface{}{
"val-a": 10,
}, map[string]interface{}{
"val-b": 20,
})
api.AssertExpectations(t)
assert.True(t, ok)
assert.NoError(t, err)
})
t.Run("new value nil", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.12.0")
api.On("KVCompareAndSet", "test-key", []byte(`{"val-a":10}`), []byte(nil)).Return(true, nil)
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndSetJSON("test-key", map[string]interface{}{
"val-a": 10,
}, nil)
api.AssertExpectations(t)
assert.True(t, ok)
assert.NoError(t, err)
})
}
func TestKVCompareAndDeleteJSON(t *testing.T) {
t.Run("incompatible server version", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.10.0")
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndDeleteJSON("test-key", map[string]interface{}{
"val-a": 10,
})
assert.Equal(t, false, ok)
assert.Error(t, err)
assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.16.0, current version: 5.10.0", err.Error())
})
t.Run("old value JSON marshal error", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.16.0")
api.AssertNotCalled(t, "KVCompareAndDelete")
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndDeleteJSON("test-key", func() {})
api.AssertExpectations(t)
assert.Equal(t, false, ok)
assert.Error(t, err)
})
t.Run("KVCompareAndDelete error", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.16.0")
api.On("KVCompareAndDelete", "test-key", []byte(`{"val-a":10}`)).Return(false, &model.AppError{})
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndDeleteJSON("test-key", map[string]interface{}{
"val-a": 10,
})
api.AssertExpectations(t)
assert.False(t, ok)
assert.Error(t, err)
})
t.Run("old value nil", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.16.0")
api.On("KVCompareAndDelete", "test-key", []byte(nil)).Return(true, nil)
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndDeleteJSON("test-key", nil)
api.AssertExpectations(t)
assert.True(t, ok)
assert.NoError(t, err)
})
t.Run("old value non-nil", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.16.0")
api.On("KVCompareAndDelete", "test-key", []byte(`{"val-a":10}`)).Return(true, nil)
p := &plugin.HelpersImpl{API: api}
ok, err := p.KVCompareAndDeleteJSON("test-key", map[string]interface{}{
"val-a": 10,
})
api.AssertExpectations(t)
assert.True(t, ok)
assert.NoError(t, err)
})
}
func TestKVSetWithExpiryJSON(t *testing.T) {
t.Run("incompatible server version", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.4.0")
p := &plugin.HelpersImpl{API: api}
err := p.KVSetWithExpiryJSON("test-key", map[string]interface{}{
"val-a": float64(10),
}, 100)
api.AssertExpectations(t)
assert.Error(t, err)
assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.6.0, current version: 5.4.0", err.Error())
})
t.Run("JSON marshal error", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.AssertNotCalled(t, "KVSetWithExpiry")
p := &plugin.HelpersImpl{API: api}
err := p.KVSetWithExpiryJSON("test-key", func() {}, 100)
api.AssertExpectations(t)
assert.Error(t, err)
})
t.Run("KVSetWithExpiry error", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVSetWithExpiry", "test-key", []byte(`{"val-a":10}`), int64(100)).Return(&model.AppError{})
p := &plugin.HelpersImpl{API: api}
err := p.KVSetWithExpiryJSON("test-key", map[string]interface{}{
"val-a": float64(10),
}, 100)
api.AssertExpectations(t)
assert.Error(t, err)
})
t.Run("wellformed JSON", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVSetWithExpiry", "test-key", []byte(`{"val-a":10}`), int64(100)).Return(nil)
p := &plugin.HelpersImpl{API: api}
err := p.KVSetWithExpiryJSON("test-key", map[string]interface{}{
"val-a": float64(10),
}, 100)
api.AssertExpectations(t)
assert.NoError(t, err)
})
}
func TestKVListWithOptions(t *testing.T) {
t.Run("incompatible server version", func(t *testing.T) {
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.1.0")
p := &plugin.HelpersImpl{API: api}
keys, err := p.KVListWithOptions()
api.AssertExpectations(t)
assert.Nil(t, keys)
assert.Error(t, err)
assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.6.0, current version: 5.1.0", err.Error())
})
t.Run("KVList error", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return([]string{}, &model.AppError{})
p.API = api
keys, err := p.KVListWithOptions()
api.AssertExpectations(t)
assert.Empty(t, keys)
assert.Error(t, err)
})
t.Run("No keys", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(nil, nil)
p.API = api
keys, err := p.KVListWithOptions()
api.AssertExpectations(t)
assert.Empty(t, keys)
assert.NoError(t, err)
})
t.Run("Basic Success, one page", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return([]string{"key1", "key2"}, nil)
p.API = api
keys, err := p.KVListWithOptions()
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, []string{"key1", "key2"})
assert.NoError(t, err)
})
t.Run("Basic Success, two page", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
keys, err := p.KVListWithOptions()
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, getKeys(101))
assert.NoError(t, err)
})
t.Run("error on second page", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, &model.AppError{})
p.API = api
keys, err := p.KVListWithOptions()
api.AssertExpectations(t)
assert.Empty(t, keys)
assert.Error(t, err)
})
t.Run("success, two page, filter prefix, one", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
keys, err := p.KVListWithOptions(plugin.WithPrefix("key99"))
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, []string{"key99"})
assert.NoError(t, err)
})
t.Run("success, two page, filter prefix, all", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
keys, err := p.KVListWithOptions(plugin.WithPrefix("notkey"))
api.AssertExpectations(t)
assert.Empty(t, keys)
assert.NoError(t, err)
})
t.Run("success, two page, filter prefix, none", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
keys, err := p.KVListWithOptions(plugin.WithPrefix("key"))
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, getKeys(101))
assert.NoError(t, err)
})
t.Run("success, two page, checker func, one", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
check := func(key string) (bool, error) {
if key == "key1" {
return true, nil
}
return false, nil
}
keys, err := p.KVListWithOptions(plugin.WithChecker(check))
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, []string{"key1"})
assert.NoError(t, err)
})
t.Run("success, two page, checker func, all", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
check := func(key string) (bool, error) {
return false, nil
}
keys, err := p.KVListWithOptions(plugin.WithChecker(check))
api.AssertExpectations(t)
assert.Empty(t, keys)
assert.NoError(t, err)
})
t.Run("success, two page, checker func, none", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return(getKeys(100), nil)
api.On("KVList", 1, 100).Return([]string{"key100"}, nil)
p.API = api
check := func(key string) (bool, error) {
return true, nil
}
keys, err := p.KVListWithOptions(plugin.WithChecker(check))
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, getKeys(101))
assert.NoError(t, err)
})
t.Run("error, checker func", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return([]string{"key1"}, nil)
p.API = api
check := func(key string) (bool, error) {
return true, &model.AppError{}
}
keys, err := p.KVListWithOptions(plugin.WithChecker(check))
api.AssertExpectations(t)
assert.Empty(t, keys)
assert.Error(t, err)
})
t.Run("success, filter and checker func, partial on both", func(t *testing.T) {
p := &plugin.HelpersImpl{}
api := &plugintest.API{}
api.On("GetServerVersion").Return("5.6.0")
api.On("KVList", 0, 100).Return([]string{"key1", "key2", "notkey3", "key4", "key5"}, nil)
p.API = api
check := func(key string) (bool, error) {
if key == "key1" || key == "key5" {
return false, nil
}
return true, nil
}
keys, err := p.KVListWithOptions(plugin.WithPrefix("key"), plugin.WithChecker(check))
api.AssertExpectations(t)
assert.ElementsMatch(t, keys, []string{"key2", "key4"})
assert.NoError(t, err)
})
}
func getKeys(count int) []string {
ret := make([]string, count)
for i := 0; i < count; i++ {
ret[i] = "key" + strconv.Itoa(i)
}
return ret
}

Просмотреть файл

@@ -1,81 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"net/http"
"net/url"
"path"
"time"
"github.com/blang/semver"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
)
// 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
}
// GetPluginAssetURL implements GetPluginAssetURL.
func (p *HelpersImpl) GetPluginAssetURL(pluginID, asset string) (string, error) {
if pluginID == "" {
return "", errors.New("empty pluginID provided")
}
if asset == "" {
return "", errors.New("empty asset name provided")
}
siteURL := *p.API.GetConfig().ServiceSettings.SiteURL
if siteURL == "" {
return "", errors.New("no SiteURL configured by the server")
}
u, err := url.Parse(siteURL + path.Join("/", pluginID, asset))
if err != nil {
return "", err
}
return u.String(), nil
}

Просмотреть файл

@@ -1,177 +0,0 @@
// 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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
)
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())
})
}
func TestGetPluginAssetURL(t *testing.T) {
siteURL := "https://mattermost.example.com"
api := &plugintest.API{}
api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: &siteURL}})
p := &plugin.HelpersImpl{API: api}
t.Run("Valid asset directory was provided", func(t *testing.T) {
pluginID := "mattermost-1234"
dir := "assets"
wantedURL := "https://mattermost.example.com/mattermost-1234/assets"
gotURL, err := p.GetPluginAssetURL(pluginID, dir)
assert.Equalf(t, wantedURL, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%v", pluginID, dir, gotURL, wantedURL)
assert.NoError(t, err)
})
t.Run("Valid asset directory path was provided", func(t *testing.T) {
pluginID := "mattermost-1234"
dirPath := "/mattermost/assets"
wantedURL := "https://mattermost.example.com/mattermost-1234/mattermost/assets"
gotURL, err := p.GetPluginAssetURL(pluginID, dirPath)
assert.Equalf(t, wantedURL, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dirPath, gotURL, wantedURL)
assert.NoError(t, err)
})
t.Run("Valid pluginID was provided", func(t *testing.T) {
pluginID := "mattermost-1234"
dir := "assets"
wantedURL := "https://mattermost.example.com/mattermost-1234/assets"
gotURL, err := p.GetPluginAssetURL(pluginID, dir)
assert.Equalf(t, wantedURL, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dir, gotURL, wantedURL)
assert.NoError(t, err)
})
t.Run("Invalid asset directory name was provided", func(t *testing.T) {
pluginID := "mattermost-1234"
dir := ""
want := ""
gotURL, err := p.GetPluginAssetURL(pluginID, dir)
assert.Emptyf(t, gotURL, "GetPluginAssetURL(%q, %q) got=%s; want=%q", pluginID, dir, gotURL, want)
assert.Error(t, err)
})
t.Run("Invalid pluginID was provided", func(t *testing.T) {
pluginID := ""
dir := "assets"
want := ""
gotURL, err := p.GetPluginAssetURL(pluginID, dir)
assert.Emptyf(t, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dir, gotURL, want)
assert.Error(t, err)
})
siteURL = ""
api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: &siteURL}})
t.Run("Empty SiteURL was configured", func(t *testing.T) {
pluginID := "mattermost-1234"
dir := "assets"
want := ""
gotURL, err := p.GetPluginAssetURL(pluginID, dir)
assert.Emptyf(t, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dir, gotURL, want)
assert.Error(t, err)
})
}

Просмотреть файл

@@ -45,12 +45,8 @@ func Example() {
api.On("GetUser", user.Id).Return(user, nil)
defer api.AssertExpectations(t)
helpers := &plugintest.Helpers{}
defer helpers.AssertExpectations(t)
p := &HelloUserPlugin{}
p.SetAPI(api)
p.SetHelpers(helpers)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)

Просмотреть файл

@@ -1,257 +0,0 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make plugin-mocks`.
package plugintest
import (
model "github.com/mattermost/mattermost-server/v6/model"
plugin "github.com/mattermost/mattermost-server/v6/plugin"
mock "github.com/stretchr/testify/mock"
)
// Helpers is an autogenerated mock type for the Helpers type
type Helpers struct {
mock.Mock
}
// CheckRequiredServerConfiguration provides a mock function with given fields: req
func (_m *Helpers) CheckRequiredServerConfiguration(req *model.Config) (bool, error) {
ret := _m.Called(req)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.Config) bool); ok {
r0 = rf(req)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Config) error); ok {
r1 = rf(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// EnsureBot provides a mock function with given fields: bot, options
func (_m *Helpers) EnsureBot(bot *model.Bot, options ...plugin.EnsureBotOption) (string, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, bot)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 string
if rf, ok := ret.Get(0).(func(*model.Bot, ...plugin.EnsureBotOption) string); ok {
r0 = rf(bot, options...)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Bot, ...plugin.EnsureBotOption) error); ok {
r1 = rf(bot, options...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPluginAssetURL provides a mock function with given fields: pluginID, asset
func (_m *Helpers) GetPluginAssetURL(pluginID string, asset string) (string, error) {
ret := _m.Called(pluginID, asset)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(pluginID, asset)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(pluginID, asset)
} else {
r1 = ret.Error(1)
}
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)
var r0 bool
if rf, ok := ret.Get(0).(func(string, interface{}) bool); ok {
r0 = rf(key, oldValue)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, interface{}) error); ok {
r1 = rf(key, oldValue)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KVCompareAndSetJSON provides a mock function with given fields: key, oldValue, newValue
func (_m *Helpers) KVCompareAndSetJSON(key string, oldValue interface{}, newValue interface{}) (bool, error) {
ret := _m.Called(key, oldValue, newValue)
var r0 bool
if rf, ok := ret.Get(0).(func(string, interface{}, interface{}) bool); ok {
r0 = rf(key, oldValue, newValue)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, interface{}, interface{}) error); ok {
r1 = rf(key, oldValue, newValue)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KVGetJSON provides a mock function with given fields: key, value
func (_m *Helpers) KVGetJSON(key string, value interface{}) (bool, error) {
ret := _m.Called(key, value)
var r0 bool
if rf, ok := ret.Get(0).(func(string, interface{}) bool); ok {
r0 = rf(key, value)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, interface{}) error); ok {
r1 = rf(key, value)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KVListWithOptions provides a mock function with given fields: options
func (_m *Helpers) KVListWithOptions(options ...plugin.KVListOption) ([]string, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 []string
if rf, ok := ret.Get(0).(func(...plugin.KVListOption) []string); ok {
r0 = rf(options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(...plugin.KVListOption) error); ok {
r1 = rf(options...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KVSetJSON provides a mock function with given fields: key, value
func (_m *Helpers) KVSetJSON(key string, value interface{}) error {
ret := _m.Called(key, value)
var r0 error
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
r0 = rf(key, value)
} else {
r0 = ret.Error(0)
}
return r0
}
// KVSetWithExpiryJSON provides a mock function with given fields: key, value, expireInSeconds
func (_m *Helpers) KVSetWithExpiryJSON(key string, value interface{}, expireInSeconds int64) error {
ret := _m.Called(key, value, expireInSeconds)
var r0 error
if rf, ok := ret.Get(0).(func(string, interface{}, int64) error); ok {
r0 = rf(key, value, expireInSeconds)
} else {
r0 = ret.Error(0)
}
return r0
}
// ShouldProcessMessage provides a mock function with given fields: post, options
func (_m *Helpers) ShouldProcessMessage(post *model.Post, options ...plugin.ShouldProcessMessageOption) (bool, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, post)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.Post, ...plugin.ShouldProcessMessageOption) bool); ok {
r0 = rf(post, options...)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, ...plugin.ShouldProcessMessageOption) error); ok {
r1 = rf(post, options...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}