[MM-58745] export/import: enable exporting and importing bots canonically (#28214)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3049191e2f
Коммит
f41d2d7774
@@ -561,6 +561,42 @@ func (s *OpenTracingLayerBotStore) GetAll(options *model.BotGetOptions) ([]*mode
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerBotStore) GetAllAfter(limit int, afterId string) ([]*model.Bot, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.GetAllAfter")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.BotStore.GetAllAfter(limit, afterId)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerBotStore) GetByUsername(username string) (*model.Bot, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.GetByUsername")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.BotStore.GetByUsername(username)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerBotStore) PermanentDelete(userID string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.PermanentDelete")
|
||||
|
||||
@@ -596,6 +596,48 @@ func (s *RetryLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot,
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerBotStore) GetAllAfter(limit int, afterId string) ([]*model.Bot, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.BotStore.GetAllAfter(limit, afterId)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerBotStore) GetByUsername(username string) (*model.Bot, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.BotStore.GetByUsername(username)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerBotStore) PermanentDelete(userID string) error {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
sq "github.com/mattermost/squirrel"
|
||||
)
|
||||
|
||||
// bot is a subset of the model.Bot type, omitting the model.User fields.
|
||||
@@ -44,14 +45,25 @@ func botFromModel(b *model.Bot) *bot {
|
||||
type SqlBotStore struct {
|
||||
*SqlStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
|
||||
// botsQuery is a starting point for all queries that return one or more Bots.
|
||||
botsQuery sq.SelectBuilder
|
||||
}
|
||||
|
||||
// newSqlBotStore creates an instance of SqlBotStore, registering the table schema in question.
|
||||
func newSqlBotStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.BotStore {
|
||||
return &SqlBotStore{
|
||||
bs := &SqlBotStore{
|
||||
SqlStore: sqlStore,
|
||||
metrics: metrics,
|
||||
}
|
||||
|
||||
// note: we are providing field names explicitly here to maintain order of columns (needed when using raw queries)
|
||||
bs.botsQuery = bs.getQueryBuilder().
|
||||
Select("b.UserId", "u.Username", "u.FirstName AS DisplayName", "b.Description", "b.OwnerId", "COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate", "b.CreateAt", "b.UpdateAt", "b.DeleteAt").
|
||||
From("Bots b").
|
||||
Join("Users u ON ( u.Id = b.UserId )")
|
||||
|
||||
return bs
|
||||
}
|
||||
|
||||
// Get fetches the given bot in the database.
|
||||
@@ -219,3 +231,40 @@ func (us SqlBotStore) PermanentDelete(botUserId string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us SqlBotStore) GetAllAfter(limit int, afterId string) ([]*model.Bot, error) {
|
||||
query := us.botsQuery.Where("b.UserId > ?", afterId).OrderBy("b.UserId ASC").Limit(uint64(limit))
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get_all_after_tosql")
|
||||
}
|
||||
|
||||
bots := []*model.Bot{}
|
||||
if err := us.GetReplicaX().Select(&bots, queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find Bots")
|
||||
}
|
||||
|
||||
return bots, nil
|
||||
}
|
||||
|
||||
// Get fetches the given bot in the database.
|
||||
func (us SqlBotStore) GetByUsername(username string) (*model.Bot, error) {
|
||||
query := us.botsQuery.Where("u.Username = lower(?)", username)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get_by_username_tosql")
|
||||
}
|
||||
|
||||
bot := model.Bot{}
|
||||
if err := us.GetReplicaX().Get(&bot, queryString, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errors.Wrap(store.NewErrNotFound("Bot", fmt.Sprintf("username=%s", username)), "failed to find Bot")
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to find Bot with username=%s", username)
|
||||
}
|
||||
|
||||
return &bot, nil
|
||||
}
|
||||
|
||||
@@ -497,7 +497,9 @@ type UserStore interface {
|
||||
|
||||
type BotStore interface {
|
||||
Get(userID string, includeDeleted bool) (*model.Bot, error)
|
||||
GetByUsername(username string) (*model.Bot, error)
|
||||
GetAll(options *model.BotGetOptions) ([]*model.Bot, error)
|
||||
GetAllAfter(limit int, afterId string) ([]*model.Bot, error)
|
||||
Save(bot *model.Bot) (*model.Bot, error)
|
||||
Update(bot *model.Bot) (*model.Bot, error)
|
||||
PermanentDelete(userID string) error
|
||||
|
||||
@@ -5,8 +5,10 @@ package storetest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -27,7 +29,9 @@ func makeBotWithUser(t *testing.T, rctx request.CTX, ss store.Store, bot *model.
|
||||
|
||||
func TestBotStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
||||
t.Run("Get", func(t *testing.T) { testBotStoreGet(t, rctx, ss, s) })
|
||||
t.Run("GetByUsername", func(t *testing.T) { testBotStoreGetByUsername(t, rctx, ss) })
|
||||
t.Run("GetAll", func(t *testing.T) { testBotStoreGetAll(t, rctx, ss, s) })
|
||||
t.Run("GetAllAfter", func(t *testing.T) { testBotStoreGetAllAfter(t, rctx, ss) })
|
||||
t.Run("Save", func(t *testing.T) { testBotStoreSave(t, rctx, ss) })
|
||||
t.Run("Update", func(t *testing.T) { testBotStoreUpdate(t, rctx, ss) })
|
||||
t.Run("PermanentDelete", func(t *testing.T) { testBotStorePermanentDelete(t, rctx, ss) })
|
||||
@@ -208,8 +212,8 @@ func testBotStoreGetAll(t *testing.T, rctx request.CTX, ss store.Store, s SqlSto
|
||||
Description: "Orphaned bot 5",
|
||||
OwnerId: deletedUser.Id,
|
||||
})
|
||||
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b4.UserId)) }()
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, b4.UserId)) }()
|
||||
defer func() { require.NoError(t, ss.Bot().PermanentDelete(ob5.UserId)) }()
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, ob5.UserId)) }()
|
||||
|
||||
t.Run("get newly created bot stoo", func(t *testing.T) {
|
||||
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10})
|
||||
@@ -471,3 +475,119 @@ func testBotStorePermanentDelete(t *testing.T, rctx request.CTX, ss store.Store)
|
||||
require.True(t, errors.As(err, &nfErr))
|
||||
})
|
||||
}
|
||||
|
||||
func testBotStoreGetAllAfter(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
bot1 := &model.Bot{
|
||||
Username: "bot_1",
|
||||
Description: "description",
|
||||
OwnerId: model.NewId(),
|
||||
}
|
||||
|
||||
user1, err := ss.User().Save(rctx, model.UserFromBot(bot1))
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, user1.Id)) }()
|
||||
bot1.UserId = user1.Id
|
||||
|
||||
returnedNewBot1, nErr := ss.Bot().Save(bot1)
|
||||
require.NoError(t, nErr)
|
||||
defer func() { require.NoError(t, ss.Bot().PermanentDelete(bot1.UserId)) }()
|
||||
|
||||
bot2 := &model.Bot{
|
||||
Username: "bot_2",
|
||||
Description: "description",
|
||||
OwnerId: model.NewId(),
|
||||
}
|
||||
|
||||
user2, err := ss.User().Save(rctx, model.UserFromBot(bot2))
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, user2.Id)) }()
|
||||
bot2.UserId = user2.Id
|
||||
|
||||
returnedNewBot2, nErr := ss.Bot().Save(bot2)
|
||||
require.NoError(t, nErr)
|
||||
defer func() { require.NoError(t, ss.Bot().PermanentDelete(bot2.UserId)) }()
|
||||
|
||||
expected := []*model.Bot{returnedNewBot1, returnedNewBot2}
|
||||
if strings.Compare(returnedNewBot2.UserId, returnedNewBot1.UserId) < 0 {
|
||||
expected = []*model.Bot{returnedNewBot2, returnedNewBot1}
|
||||
}
|
||||
|
||||
t.Run("get after lowest possible id", func(t *testing.T) {
|
||||
actual, err := ss.Bot().GetAllAfter(10000, strings.Repeat("0", 26))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
t.Run("get after first user", func(t *testing.T) {
|
||||
actual, err := ss.Bot().GetAllAfter(10000, expected[0].UserId)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []*model.Bot{expected[1]}, actual)
|
||||
})
|
||||
|
||||
t.Run("get after second user", func(t *testing.T) {
|
||||
actual, err := ss.Bot().GetAllAfter(10000, expected[1].UserId)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []*model.Bot{}, actual)
|
||||
})
|
||||
}
|
||||
|
||||
func testBotStoreGetByUsername(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
bot1 := &model.Bot{
|
||||
Username: "bot_1",
|
||||
Description: "description",
|
||||
OwnerId: model.NewId(),
|
||||
}
|
||||
|
||||
user1, err := ss.User().Save(rctx, model.UserFromBot(bot1))
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, user1.Id)) }()
|
||||
bot1.UserId = user1.Id
|
||||
|
||||
returnedNewBot1, nErr := ss.Bot().Save(bot1)
|
||||
require.NoError(t, nErr)
|
||||
defer func() { require.NoError(t, ss.Bot().PermanentDelete(bot1.UserId)) }()
|
||||
|
||||
bot2 := &model.Bot{
|
||||
Username: "bot_2",
|
||||
Description: "description",
|
||||
OwnerId: model.NewId(),
|
||||
}
|
||||
|
||||
user2, err := ss.User().Save(rctx, model.UserFromBot(bot2))
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, user2.Id)) }()
|
||||
bot2.UserId = user2.Id
|
||||
|
||||
returnedNewBot2, nErr := ss.Bot().Save(bot2)
|
||||
require.NoError(t, nErr)
|
||||
defer func() { require.NoError(t, ss.Bot().PermanentDelete(bot2.UserId)) }()
|
||||
|
||||
t.Run("get bot1 by username", func(t *testing.T) {
|
||||
result, err := ss.Bot().GetByUsername(returnedNewBot1.Username)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, returnedNewBot1, result)
|
||||
})
|
||||
|
||||
t.Run("get bot2 by username", func(t *testing.T) {
|
||||
result, err := ss.Bot().GetByUsername(returnedNewBot2.Username)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, returnedNewBot2, result)
|
||||
})
|
||||
|
||||
t.Run("get by empty username", func(t *testing.T) {
|
||||
_, err := ss.Bot().GetByUsername("")
|
||||
require.Error(t, err)
|
||||
var nfErr *store.ErrNotFound
|
||||
require.True(t, errors.As(err, &nfErr))
|
||||
})
|
||||
|
||||
t.Run("get by unknown", func(t *testing.T) {
|
||||
_, err := ss.Bot().GetByUsername("unknown")
|
||||
require.Error(t, err)
|
||||
var nfErr *store.ErrNotFound
|
||||
require.True(t, errors.As(err, &nfErr))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,6 +74,66 @@ func (_m *BotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetAllAfter provides a mock function with given fields: limit, afterId
|
||||
func (_m *BotStore) GetAllAfter(limit int, afterId string) ([]*model.Bot, error) {
|
||||
ret := _m.Called(limit, afterId)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAllAfter")
|
||||
}
|
||||
|
||||
var r0 []*model.Bot
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(int, string) ([]*model.Bot, error)); ok {
|
||||
return rf(limit, afterId)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(int, string) []*model.Bot); ok {
|
||||
r0 = rf(limit, afterId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.Bot)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(int, string) error); ok {
|
||||
r1 = rf(limit, afterId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetByUsername provides a mock function with given fields: username
|
||||
func (_m *BotStore) GetByUsername(username string) (*model.Bot, error) {
|
||||
ret := _m.Called(username)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetByUsername")
|
||||
}
|
||||
|
||||
var r0 *model.Bot
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*model.Bot, error)); ok {
|
||||
return rf(username)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *model.Bot); ok {
|
||||
r0 = rf(username)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Bot)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(username)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PermanentDelete provides a mock function with given fields: userID
|
||||
func (_m *BotStore) PermanentDelete(userID string) error {
|
||||
ret := _m.Called(userID)
|
||||
|
||||
@@ -551,6 +551,38 @@ func (s *TimerLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot,
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerBotStore) GetAllAfter(limit int, afterId string) ([]*model.Bot, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.BotStore.GetAllAfter(limit, afterId)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.GetAllAfter", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerBotStore) GetByUsername(username string) (*model.Bot, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.BotStore.GetByUsername(username)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.GetByUsername", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerBotStore) PermanentDelete(userID string) error {
|
||||
start := time.Now()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user