mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
PLT-8131 (part2) Add plugin key value store support (#7902)
* Add plugin key value store support * Add localization strings * Updates per feedback
This commit is contained in:
@@ -5,7 +5,6 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -27,228 +26,6 @@ import (
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
)
|
||||
|
||||
type PluginAPI struct {
|
||||
id string
|
||||
app *App
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return api.app.CreateTeam(team)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteTeam(teamId string) *model.AppError {
|
||||
return api.app.SoftDeleteTeam(teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeam(teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return api.app.UpdateTeam(team)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return api.app.CreateUser(user)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteUser(userId string) *model.AppError {
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = api.app.UpdateActive(user, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUser(userId string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUser(userId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByEmail(email)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return api.app.UpdateUser(user, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
return api.app.CreateChannel(channel, false)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteChannel(channelId string) *model.AppError {
|
||||
channel, err := api.app.GetChannel(channelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return api.app.DeleteChannel(channel, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannel(channelId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
|
||||
return api.app.CreateGroupChannel(userIds, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
return api.app.UpdateChannel(channel)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeletePost(postId string) *model.AppError {
|
||||
_, err := api.app.DeletePost(postId)
|
||||
return err
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetPost(postId string) (*model.Post, *model.AppError) {
|
||||
return api.app.GetSinglePost(postId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.UpdatePost(post, false)
|
||||
}
|
||||
|
||||
type BuiltInPluginAPI struct {
|
||||
id string
|
||||
router *mux.Router
|
||||
app *App
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) PluginRouter() *mux.Router {
|
||||
return api.router
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) {
|
||||
if api.app.Ldap == nil {
|
||||
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.AuthData == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return api.app.Ldap.GetUserAttributes(*user.AuthData, attributes)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError) {
|
||||
token := ""
|
||||
isTokenFromQueryString := false
|
||||
|
||||
// Attempt to parse token out of the header
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
|
||||
// Default session token
|
||||
token = authHeader[7:]
|
||||
|
||||
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
|
||||
// OAuth token
|
||||
token = authHeader[6:]
|
||||
}
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if len(token) == 0 {
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
token = cookie.Value
|
||||
|
||||
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if len(token) == 0 {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
isTokenFromQueryString = true
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
session, err := api.app.GetSession(token)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) I18n(id string, r *http.Request) string {
|
||||
if r != nil {
|
||||
f, _ := utils.GetTranslationsAndLocale(nil, r)
|
||||
return f(id)
|
||||
}
|
||||
f, _ := utils.GetTranslationsBySystemLocale()
|
||||
return f(id)
|
||||
}
|
||||
|
||||
func (a *App) initBuiltInPlugins() {
|
||||
plugins := map[string]builtinplugin.Plugin{
|
||||
"jira": &jira.Plugin{},
|
||||
@@ -557,6 +334,10 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
|
||||
return &PluginAPI{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
keyValueStore: &PluginKeyValueStore{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
)
|
||||
@@ -647,3 +428,45 @@ func (a *App) ShutDownPlugins() {
|
||||
a.PluginConfigListenerId = ""
|
||||
a.PluginEnv = nil
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
l4g.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, key)
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
l4g.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, key)
|
||||
|
||||
if result.Err != nil {
|
||||
l4g.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
260
app/plugin_api.go
Normal file
260
app/plugin_api.go
Normal file
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type PluginAPI struct {
|
||||
id string
|
||||
app *App
|
||||
keyValueStore *PluginKeyValueStore
|
||||
}
|
||||
|
||||
type PluginKeyValueStore struct {
|
||||
id string
|
||||
app *App
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return api.app.CreateTeam(team)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteTeam(teamId string) *model.AppError {
|
||||
return api.app.SoftDeleteTeam(teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeam(teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return api.app.UpdateTeam(team)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return api.app.CreateUser(user)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteUser(userId string) *model.AppError {
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = api.app.UpdateActive(user, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUser(userId string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUser(userId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByEmail(email)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return api.app.UpdateUser(user, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
return api.app.CreateChannel(channel, false)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteChannel(channelId string) *model.AppError {
|
||||
channel, err := api.app.GetChannel(channelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return api.app.DeleteChannel(channel, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannel(channelId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
|
||||
return api.app.CreateGroupChannel(userIds, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
return api.app.UpdateChannel(channel)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeletePost(postId string) *model.AppError {
|
||||
_, err := api.app.DeletePost(postId)
|
||||
return err
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetPost(postId string) (*model.Post, *model.AppError) {
|
||||
return api.app.GetSinglePost(postId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.UpdatePost(post, false)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) KeyValueStore() plugin.KeyValueStore {
|
||||
return api.keyValueStore
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
return s.app.SetPluginKey(s.id, key, value)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
return s.app.GetPluginKey(s.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Delete(key string) *model.AppError {
|
||||
return s.app.DeletePluginKey(s.id, key)
|
||||
}
|
||||
|
||||
type BuiltInPluginAPI struct {
|
||||
id string
|
||||
router *mux.Router
|
||||
app *App
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) PluginRouter() *mux.Router {
|
||||
return api.router
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) {
|
||||
if api.app.Ldap == nil {
|
||||
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.AuthData == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return api.app.Ldap.GetUserAttributes(*user.AuthData, attributes)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError) {
|
||||
token := ""
|
||||
isTokenFromQueryString := false
|
||||
|
||||
// Attempt to parse token out of the header
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
|
||||
// Default session token
|
||||
token = authHeader[7:]
|
||||
|
||||
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
|
||||
// OAuth token
|
||||
token = authHeader[6:]
|
||||
}
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if len(token) == 0 {
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
token = cookie.Value
|
||||
|
||||
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if len(token) == 0 {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
isTokenFromQueryString = true
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
session, err := api.app.GetSession(token)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) I18n(id string, r *http.Request) string {
|
||||
if r != nil {
|
||||
f, _ := utils.GetTranslationsAndLocale(nil, r)
|
||||
return f(id)
|
||||
}
|
||||
f, _ := utils.GetTranslationsBySystemLocale()
|
||||
return f(id)
|
||||
}
|
||||
35
app/plugin_test.go
Normal file
35
app/plugin_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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"))
|
||||
}
|
||||
28
i18n/en.json
28
i18n/en.json
@@ -3554,6 +3554,10 @@
|
||||
"id": "app.notification.subject.notification.full",
|
||||
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.key_value.set.app_error",
|
||||
"translation": "Unable to set key value. See detailed error for more information."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.activate.app_error",
|
||||
"translation": "Unable to activate extracted plugin. Plugin may already exist and be activated."
|
||||
@@ -4242,6 +4246,14 @@
|
||||
"id": "mattermost.working_dir",
|
||||
"translation": "Current working directory is %v"
|
||||
},
|
||||
{
|
||||
"id": "model.plugin_key_value.is_valid.plugin_id.app_error",
|
||||
"translation": "Invalid plugin ID"
|
||||
},
|
||||
{
|
||||
"id": "model.plugin_key_value.is_valid.key.app_error",
|
||||
"translation": "Invalid key"
|
||||
},
|
||||
{
|
||||
"id": "model.access.is_valid.access_token.app_error",
|
||||
"translation": "Invalid access token"
|
||||
@@ -6434,6 +6446,22 @@
|
||||
"id": "store.sql_team.update_display_name.app_error",
|
||||
"translation": "We couldn't update the team name"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_plugin_store.delete.app_error",
|
||||
"translation": "Could not delete plugin key value"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_plugin_store.get.app_error",
|
||||
"translation": "Could not get plugin key value"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_plugin_store.save.app_error",
|
||||
"translation": "Could not save or update plugin key value"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_plugin_store.save_unique.app_error",
|
||||
"translation": "Could not save or update plugin key value due to unique constraint violation"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user.analytics_get_inactive_users_count.app_error",
|
||||
"translation": "We could not count the inactive users"
|
||||
|
||||
26
model/plugin_key_value.go
Normal file
26
model/plugin_key_value.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PluginKeyValue struct {
|
||||
PluginId string `json:"plugin_id"`
|
||||
Key string `json:"key" db:"PKey"`
|
||||
Value []byte `json:"value" db:"PValue"`
|
||||
}
|
||||
|
||||
func (kv *PluginKeyValue) IsValid() *AppError {
|
||||
if len(kv.PluginId) == 0 {
|
||||
return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.plugin_id.app_error", nil, "key="+kv.Key, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(kv.Key) == 0 {
|
||||
return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.key.app_error", nil, "key="+kv.Key, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
22
model/plugin_key_value_test.go
Normal file
22
model/plugin_key_value_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPluginKeyIsValid(t *testing.T) {
|
||||
kv := PluginKeyValue{PluginId: "someid", Key: "somekey", Value: []byte("somevalue")}
|
||||
assert.Nil(t, kv.IsValid())
|
||||
|
||||
kv.PluginId = ""
|
||||
assert.NotNil(t, kv.IsValid())
|
||||
|
||||
kv.PluginId = "someid"
|
||||
kv.Key = ""
|
||||
assert.NotNil(t, kv.IsValid())
|
||||
}
|
||||
@@ -79,6 +79,20 @@ type API interface {
|
||||
// GetPost gets a post.
|
||||
GetPost(postId string) (*model.Post, *model.AppError)
|
||||
|
||||
// Update post updates a post.
|
||||
// UpdatePost updates a post.
|
||||
UpdatePost(post *model.Post) (*model.Post, *model.AppError)
|
||||
|
||||
// KeyValueStore returns an object for accessing the persistent key value storage.
|
||||
KeyValueStore() KeyValueStore
|
||||
}
|
||||
|
||||
type KeyValueStore interface {
|
||||
// Set will store a key-value pair, unique per plugin.
|
||||
Set(key string, value []byte) *model.AppError
|
||||
|
||||
// Get will retrieve a value based on the key. Returns nil for non-existent keys.
|
||||
Get(key string) ([]byte, *model.AppError)
|
||||
|
||||
// Delete will remove a key-value pair. Returns nil for non-existent keys.
|
||||
Delete(key string) *model.AppError
|
||||
}
|
||||
|
||||
@@ -12,9 +12,15 @@ import (
|
||||
|
||||
type API struct {
|
||||
mock.Mock
|
||||
Store *KeyValueStore
|
||||
}
|
||||
|
||||
type KeyValueStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var _ plugin.API = (*API)(nil)
|
||||
var _ plugin.KeyValueStore = (*KeyValueStore)(nil)
|
||||
|
||||
func (m *API) LoadPluginConfiguration(dest interface{}) error {
|
||||
return m.Called(dest).Error(0)
|
||||
@@ -235,3 +241,35 @@ func (m *API) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
err, _ := ret.Get(1).(*model.AppError)
|
||||
return postOut, err
|
||||
}
|
||||
|
||||
func (m *API) KeyValueStore() plugin.KeyValueStore {
|
||||
return m.Store
|
||||
}
|
||||
|
||||
func (m *KeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
ret := m.Called(key, value)
|
||||
if f, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok {
|
||||
return f(key, value)
|
||||
}
|
||||
err, _ := ret.Get(0).(*model.AppError)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *KeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
ret := m.Called(key)
|
||||
if f, ok := ret.Get(0).(func(string) ([]byte, *model.AppError)); ok {
|
||||
return f(key)
|
||||
}
|
||||
psv, _ := ret.Get(0).([]byte)
|
||||
err, _ := ret.Get(1).(*model.AppError)
|
||||
return psv, err
|
||||
}
|
||||
|
||||
func (m *KeyValueStore) Delete(key string) *model.AppError {
|
||||
ret := m.Called(key)
|
||||
if f, ok := ret.Get(0).(func(string) *model.AppError); ok {
|
||||
return f(key)
|
||||
}
|
||||
err, _ := ret.Get(0).(*model.AppError)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -259,6 +259,41 @@ func (api *LocalAPI) UpdatePost(args *model.Post, reply *APIPostReply) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type APIKeyValueStoreReply struct {
|
||||
Value []byte
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
type APIKeyValueStoreSetArgs struct {
|
||||
Key string
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (api *LocalAPI) KeyValueStoreSet(args *APIKeyValueStoreSetArgs, reply *APIErrorReply) error {
|
||||
err := api.api.KeyValueStore().Set(args.Key, args.Value)
|
||||
*reply = APIErrorReply{
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) KeyValueStoreGet(args string, reply *APIKeyValueStoreReply) error {
|
||||
v, err := api.api.KeyValueStore().Get(args)
|
||||
*reply = APIKeyValueStoreReply{
|
||||
Value: v,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) KeyValueStoreDelete(args string, reply *APIErrorReply) error {
|
||||
err := api.api.KeyValueStore().Delete(args)
|
||||
*reply = APIErrorReply{
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&LocalAPI{
|
||||
@@ -269,11 +304,17 @@ func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||
}
|
||||
|
||||
type RemoteAPI struct {
|
||||
client *rpc.Client
|
||||
muxer *Muxer
|
||||
client *rpc.Client
|
||||
muxer *Muxer
|
||||
keyValueStore *RemoteKeyValueStore
|
||||
}
|
||||
|
||||
type RemoteKeyValueStore struct {
|
||||
api *RemoteAPI
|
||||
}
|
||||
|
||||
var _ plugin.API = (*RemoteAPI)(nil)
|
||||
var _ plugin.KeyValueStore = (*RemoteKeyValueStore)(nil)
|
||||
|
||||
func (api *RemoteAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
var config []byte
|
||||
@@ -467,13 +508,47 @@ func (api *RemoteAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError
|
||||
return reply.Post, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) KeyValueStore() plugin.KeyValueStore {
|
||||
return api.keyValueStore
|
||||
}
|
||||
|
||||
func (s *RemoteKeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := s.api.client.Call("LocalAPI.KeyValueStoreSet", &APIKeyValueStoreSetArgs{Key: key, Value: value}, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.KeyValueStoreSet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (s *RemoteKeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
var reply APIKeyValueStoreReply
|
||||
if err := s.api.client.Call("LocalAPI.KeyValueStoreGet", key, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.KeyValueStoreGet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Value, reply.Error
|
||||
}
|
||||
|
||||
func (s *RemoteKeyValueStore) Delete(key string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := s.api.client.Call("LocalAPI.KeyValueStoreDelete", key, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.KeyValueStoreDelete", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (h *RemoteAPI) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI {
|
||||
return &RemoteAPI{
|
||||
client: rpc.NewClient(conn),
|
||||
muxer: muxer,
|
||||
remoteKeyValueStore := &RemoteKeyValueStore{}
|
||||
remoteApi := &RemoteAPI{
|
||||
client: rpc.NewClient(conn),
|
||||
muxer: muxer,
|
||||
keyValueStore: remoteKeyValueStore,
|
||||
}
|
||||
|
||||
remoteKeyValueStore.api = remoteApi
|
||||
|
||||
return remoteApi
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ func testAPIRPC(api plugin.API, f func(plugin.API)) {
|
||||
}
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
var api plugintest.API
|
||||
keyValueStore := &plugintest.KeyValueStore{}
|
||||
api := plugintest.API{Store: keyValueStore}
|
||||
defer api.AssertExpectations(t)
|
||||
|
||||
type Config struct {
|
||||
@@ -199,5 +200,20 @@ func TestAPI(t *testing.T) {
|
||||
post, err = remote.UpdatePost(testPost)
|
||||
assert.Equal(t, testPost, post)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.KeyValueStore().(*plugintest.KeyValueStore).On("Set", "thekey", []byte("thevalue")).Return(nil).Once()
|
||||
err = remote.KeyValueStore().Set("thekey", []byte("thevalue"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.KeyValueStore().(*plugintest.KeyValueStore).On("Get", "thekey").Return(func(key string) ([]byte, *model.AppError) {
|
||||
return []byte("thevalue"), nil
|
||||
}).Once()
|
||||
ret, err := remote.KeyValueStore().Get("thekey")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("thevalue"), ret)
|
||||
|
||||
api.KeyValueStore().(*plugintest.KeyValueStore).On("Delete", "thekey").Return(nil).Once()
|
||||
err = remote.KeyValueStore().Delete("thekey")
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ func (s *LayeredStore) UserAccessToken() UserAccessTokenStore {
|
||||
return s.DatabaseLayer.UserAccessToken()
|
||||
}
|
||||
|
||||
func (s *LayeredStore) Plugin() PluginStore {
|
||||
return s.DatabaseLayer.Plugin()
|
||||
}
|
||||
|
||||
func (s *LayeredStore) MarkSystemRanUnitTests() {
|
||||
s.DatabaseLayer.MarkSystemRanUnitTests()
|
||||
}
|
||||
|
||||
92
store/sqlstore/plugin_store.go
Normal file
92
store/sqlstore/plugin_store.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
)
|
||||
|
||||
type SqlPluginStore struct {
|
||||
SqlStore
|
||||
}
|
||||
|
||||
func NewSqlPluginStore(sqlStore SqlStore) store.PluginStore {
|
||||
s := &SqlPluginStore{sqlStore}
|
||||
|
||||
for _, db := range sqlStore.GetAllConns() {
|
||||
table := db.AddTableWithName(model.PluginKeyValue{}, "PluginKeyValueStore").SetKeys(false, "PluginId", "Key")
|
||||
table.ColMap("Value").SetMaxSize(8192)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) CreateIndexesIfNotExists() {
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) SaveOrUpdate(kv *model.PluginKeyValue) store.StoreChannel {
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
if result.Err = kv.IsValid(); result.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ps.DriverName() == model.DATABASE_DRIVER_POSTGRES {
|
||||
// Unfortunately PostgreSQL pre-9.5 does not have an atomic upsert, so we use
|
||||
// separate update and insert queries to accomplish our upsert
|
||||
if rowsAffected, err := ps.GetMaster().Update(kv); err != nil {
|
||||
result.Err = model.NewAppError("SqlPluginStore.SaveOrUpdate", "store.sql_plugin_store.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
} else if rowsAffected == 0 {
|
||||
// No rows were affected by the update, so let's try an insert
|
||||
if err := ps.GetMaster().Insert(kv); err != nil {
|
||||
// If the error is from unique constraints violation, it's the result of a
|
||||
// valid race and we can report success. Otherwise we have a real error and
|
||||
// need to return it
|
||||
if !IsUniqueConstraintError(err, []string{"PRIMARY", "PluginId", "Key", "PKey"}) {
|
||||
result.Err = model.NewAppError("SqlPluginStore.SaveOrUpdate", "store.sql_plugin_store.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ps.DriverName() == model.DATABASE_DRIVER_MYSQL {
|
||||
if _, err := ps.GetMaster().Exec("INSERT INTO PluginKeyValueStore (PluginId, PKey, PValue) VALUES(:PluginId, :Key, :Value) ON DUPLICATE KEY UPDATE PValue = :Value", map[string]interface{}{"PluginId": kv.PluginId, "Key": kv.Key, "Value": kv.Value}); err != nil {
|
||||
result.Err = model.NewAppError("SqlPluginStore.SaveOrUpdate", "store.sql_plugin_store.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result.Data = kv
|
||||
})
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) Get(pluginId, key string) store.StoreChannel {
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
var kv *model.PluginKeyValue
|
||||
|
||||
if err := ps.GetReplica().SelectOne(&kv, "SELECT * FROM PluginKeyValueStore WHERE PluginId = :PluginId AND PKey = :Key", map[string]interface{}{"PluginId": pluginId, "Key": key}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
result.Err = model.NewAppError("SqlPluginStore.Get", "store.sql_plugin_store.get.app_error", nil, fmt.Sprintf("plugin_id=%v, key=%v, err=%v", pluginId, key, err.Error()), http.StatusNotFound)
|
||||
} else {
|
||||
result.Err = model.NewAppError("SqlPluginStore.Get", "store.sql_plugin_store.get.app_error", nil, fmt.Sprintf("plugin_id=%v, key=%v, err=%v", pluginId, key, err.Error()), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
result.Data = kv
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) Delete(pluginId, key string) store.StoreChannel {
|
||||
return store.Do(func(result *store.StoreResult) {
|
||||
if _, err := ps.GetMaster().Exec("DELETE FROM PluginKeyValueStore WHERE PluginId = :PluginId AND PKey = :Key", map[string]interface{}{"PluginId": pluginId, "Key": key}); err != nil {
|
||||
result.Err = model.NewAppError("SqlPluginStore.Delete", "store.sql_plugin_store.delete.app_error", nil, fmt.Sprintf("plugin_id=%v, key=%v, err=%v", pluginId, key, err.Error()), http.StatusInternalServerError)
|
||||
} else {
|
||||
result.Data = true
|
||||
}
|
||||
})
|
||||
}
|
||||
14
store/sqlstore/plugin_store_test.go
Normal file
14
store/sqlstore/plugin_store_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/store/storetest"
|
||||
)
|
||||
|
||||
func TestPluginStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestPluginStore)
|
||||
}
|
||||
@@ -85,5 +85,6 @@ type SqlStore interface {
|
||||
FileInfo() store.FileInfoStore
|
||||
Reaction() store.ReactionStore
|
||||
Job() store.JobStore
|
||||
Plugin() store.PluginStore
|
||||
UserAccessToken() store.UserAccessTokenStore
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ type SqlSupplierOldStores struct {
|
||||
reaction store.ReactionStore
|
||||
job store.JobStore
|
||||
userAccessToken store.UserAccessTokenStore
|
||||
plugin store.PluginStore
|
||||
}
|
||||
|
||||
type SqlSupplier struct {
|
||||
@@ -129,6 +130,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter
|
||||
supplier.oldStores.fileInfo = NewSqlFileInfoStore(supplier, metrics)
|
||||
supplier.oldStores.job = NewSqlJobStore(supplier)
|
||||
supplier.oldStores.userAccessToken = NewSqlUserAccessTokenStore(supplier)
|
||||
supplier.oldStores.plugin = NewSqlPluginStore(supplier)
|
||||
|
||||
initSqlSupplierReactions(supplier)
|
||||
|
||||
@@ -161,6 +163,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter
|
||||
supplier.oldStores.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists()
|
||||
supplier.oldStores.job.(*SqlJobStore).CreateIndexesIfNotExists()
|
||||
supplier.oldStores.userAccessToken.(*SqlUserAccessTokenStore).CreateIndexesIfNotExists()
|
||||
supplier.oldStores.plugin.(*SqlPluginStore).CreateIndexesIfNotExists()
|
||||
|
||||
supplier.oldStores.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
@@ -798,6 +801,10 @@ func (ss *SqlSupplier) UserAccessToken() store.UserAccessTokenStore {
|
||||
return ss.oldStores.userAccessToken
|
||||
}
|
||||
|
||||
func (ss *SqlSupplier) Plugin() store.PluginStore {
|
||||
return ss.oldStores.plugin
|
||||
}
|
||||
|
||||
func (ss *SqlSupplier) DropAllTables() {
|
||||
ss.master.TruncateTables()
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ type Store interface {
|
||||
Reaction() ReactionStore
|
||||
Job() JobStore
|
||||
UserAccessToken() UserAccessTokenStore
|
||||
Plugin() PluginStore
|
||||
MarkSystemRanUnitTests()
|
||||
Close()
|
||||
DropAllTables()
|
||||
@@ -440,3 +441,9 @@ type UserAccessTokenStore interface {
|
||||
UpdateTokenEnable(tokenId string) StoreChannel
|
||||
UpdateTokenDisable(tokenId string) StoreChannel
|
||||
}
|
||||
|
||||
type PluginStore interface {
|
||||
SaveOrUpdate(keyVal *model.PluginKeyValue) StoreChannel
|
||||
Get(pluginId, key string) StoreChannel
|
||||
Delete(pluginId, key string) StoreChannel
|
||||
}
|
||||
|
||||
@@ -221,6 +221,22 @@ func (_m *LayeredStoreDatabaseLayer) OAuth() store.OAuthStore {
|
||||
return r0
|
||||
}
|
||||
|
||||
// Plugin provides a mock function with given fields:
|
||||
func (_m *LayeredStoreDatabaseLayer) Plugin() store.PluginStore {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 store.PluginStore
|
||||
if rf, ok := ret.Get(0).(func() store.PluginStore); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.PluginStore)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Post provides a mock function with given fields:
|
||||
func (_m *LayeredStoreDatabaseLayer) Post() store.PostStore {
|
||||
ret := _m.Called()
|
||||
|
||||
62
store/storetest/mocks/PluginStore.go
Normal file
62
store/storetest/mocks/PluginStore.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Code generated by mockery v1.0.0
|
||||
|
||||
// Regenerate this file using `make store-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import model "github.com/mattermost/mattermost-server/model"
|
||||
import store "github.com/mattermost/mattermost-server/store"
|
||||
|
||||
// PluginStore is an autogenerated mock type for the PluginStore type
|
||||
type PluginStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: pluginId, key
|
||||
func (_m *PluginStore) Delete(pluginId string, key string) store.StoreChannel {
|
||||
ret := _m.Called(pluginId, key)
|
||||
|
||||
var r0 store.StoreChannel
|
||||
if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
|
||||
r0 = rf(pluginId, key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.StoreChannel)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: pluginId, key
|
||||
func (_m *PluginStore) Get(pluginId string, key string) store.StoreChannel {
|
||||
ret := _m.Called(pluginId, key)
|
||||
|
||||
var r0 store.StoreChannel
|
||||
if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
|
||||
r0 = rf(pluginId, key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.StoreChannel)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SaveOrUpdate provides a mock function with given fields: keyVal
|
||||
func (_m *PluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) store.StoreChannel {
|
||||
ret := _m.Called(keyVal)
|
||||
|
||||
var r0 store.StoreChannel
|
||||
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue) store.StoreChannel); ok {
|
||||
r0 = rf(keyVal)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.StoreChannel)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -143,6 +143,20 @@ func (_m *SqlStore) CreateColumnIfNotExists(tableName string, columnName string,
|
||||
return r0
|
||||
}
|
||||
|
||||
// CreateCompositeIndexIfNotExists provides a mock function with given fields: indexName, tableName, columnNames
|
||||
func (_m *SqlStore) CreateCompositeIndexIfNotExists(indexName string, tableName string, columnNames []string) bool {
|
||||
ret := _m.Called(indexName, tableName, columnNames)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string, []string) bool); ok {
|
||||
r0 = rf(indexName, tableName, columnNames)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CreateFullTextIndexIfNotExists provides a mock function with given fields: indexName, tableName, columnName
|
||||
func (_m *SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName string, columnName string) bool {
|
||||
ret := _m.Called(indexName, tableName, columnName)
|
||||
@@ -404,6 +418,22 @@ func (_m *SqlStore) OAuth() store.OAuthStore {
|
||||
return r0
|
||||
}
|
||||
|
||||
// Plugin provides a mock function with given fields:
|
||||
func (_m *SqlStore) Plugin() store.PluginStore {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 store.PluginStore
|
||||
if rf, ok := ret.Get(0).(func() store.PluginStore); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.PluginStore)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Post provides a mock function with given fields:
|
||||
func (_m *SqlStore) Post() store.PostStore {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -203,6 +203,22 @@ func (_m *Store) OAuth() store.OAuthStore {
|
||||
return r0
|
||||
}
|
||||
|
||||
// Plugin provides a mock function with given fields:
|
||||
func (_m *Store) Plugin() store.PluginStore {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 store.PluginStore
|
||||
if rf, ok := ret.Get(0).(func() store.PluginStore); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.PluginStore)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Post provides a mock function with given fields:
|
||||
func (_m *Store) Post() store.PostStore {
|
||||
ret := _m.Called()
|
||||
|
||||
69
store/storetest/plugin_store.go
Normal file
69
store/storetest/plugin_store.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package storetest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPluginStore(t *testing.T, ss store.Store) {
|
||||
t.Run("PluginSaveGet", func(t *testing.T) { testPluginSaveGet(t, ss) })
|
||||
t.Run("PluginDelete", func(t *testing.T) { testPluginDelete(t, ss) })
|
||||
}
|
||||
|
||||
func testPluginSaveGet(t *testing.T, ss store.Store) {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: model.NewId(),
|
||||
Key: model.NewId(),
|
||||
Value: []byte(model.NewId()),
|
||||
}
|
||||
|
||||
if result := <-ss.Plugin().SaveOrUpdate(kv); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
<-ss.Plugin().Delete(kv.PluginId, kv.Key)
|
||||
}()
|
||||
|
||||
if result := <-ss.Plugin().Get(kv.PluginId, kv.Key); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
received := result.Data.(*model.PluginKeyValue)
|
||||
assert.Equal(t, kv.PluginId, received.PluginId)
|
||||
assert.Equal(t, kv.Key, received.Key)
|
||||
assert.Equal(t, kv.Value, received.Value)
|
||||
}
|
||||
|
||||
// Try inserting when already exists
|
||||
kv.Value = []byte(model.NewId())
|
||||
if result := <-ss.Plugin().SaveOrUpdate(kv); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if result := <-ss.Plugin().Get(kv.PluginId, kv.Key); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
received := result.Data.(*model.PluginKeyValue)
|
||||
assert.Equal(t, kv.PluginId, received.PluginId)
|
||||
assert.Equal(t, kv.Key, received.Key)
|
||||
assert.Equal(t, kv.Value, received.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginDelete(t *testing.T, ss store.Store) {
|
||||
kv := store.Must(ss.Plugin().SaveOrUpdate(&model.PluginKeyValue{
|
||||
PluginId: model.NewId(),
|
||||
Key: model.NewId(),
|
||||
Value: []byte(model.NewId()),
|
||||
})).(*model.PluginKeyValue)
|
||||
|
||||
if result := <-ss.Plugin().Delete(kv.PluginId, kv.Key); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ type Store struct {
|
||||
ReactionStore mocks.ReactionStore
|
||||
JobStore mocks.JobStore
|
||||
UserAccessTokenStore mocks.UserAccessTokenStore
|
||||
PluginStore mocks.PluginStore
|
||||
}
|
||||
|
||||
func (s *Store) Team() store.TeamStore { return &s.TeamStore }
|
||||
@@ -65,6 +66,7 @@ func (s *Store) FileInfo() store.FileInfoStore { return &s.FileI
|
||||
func (s *Store) Reaction() store.ReactionStore { return &s.ReactionStore }
|
||||
func (s *Store) Job() store.JobStore { return &s.JobStore }
|
||||
func (s *Store) UserAccessToken() store.UserAccessTokenStore { return &s.UserAccessTokenStore }
|
||||
func (s *Store) Plugin() store.PluginStore { return &s.PluginStore }
|
||||
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
|
||||
func (s *Store) Close() { /* do nothing */ }
|
||||
func (s *Store) DropAllTables() { /* do nothing */ }
|
||||
@@ -96,5 +98,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
|
||||
&s.ReactionStore,
|
||||
&s.JobStore,
|
||||
&s.UserAccessTokenStore,
|
||||
&s.PluginStore,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user