Merge branch 'master' into MM-50966-in-product-expansion
Этот коммит содержится в:
@@ -138,7 +138,7 @@ TEMPLATES_DIR=templates
|
||||
|
||||
# Plugins Packages
|
||||
PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.15.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0
|
||||
|
||||
@@ -70,7 +70,10 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) {
|
||||
}
|
||||
*settings.DriverName = s.dbType
|
||||
|
||||
db := sqlstore.SetupConnection("master", connectionString, &settings)
|
||||
db, err := sqlstore.SetupConnection("master", connectionString, &settings, sqlstore.DBPingAttempts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
ENV PATH="/mattermost/bin:${PATH}"
|
||||
ARG PUID=2000
|
||||
ARG PGID=2000
|
||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.9.2/mattermost-7.9.2-linux-amd64.tar.gz?src=docker"
|
||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.10.0/mattermost-7.10.0-linux-amd64.tar.gz?src=docker"
|
||||
|
||||
# # Install needed packages and indirect dependencies
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -323,7 +323,14 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer c.LogAuditRec(auditRec)
|
||||
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
|
||||
|
||||
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
|
||||
// Checks that user is a member of the specified channel, and that they have permission to create a post in it.
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost) {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
// For compatibility reasons, PermissionCreatePost is also checked.
|
||||
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||
return
|
||||
@@ -343,6 +350,13 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
|
||||
// they can't just execute slash commands against arbitrary teams
|
||||
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePost) {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
||||
// For compatibility reasons, PermissionCreatePost is also checked.
|
||||
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
|
||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||
return
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
@@ -1065,3 +1066,80 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestExecuteCommandReadOnly(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
ctx := request.EmptyContext(th.TestLogger)
|
||||
defer th.TearDown()
|
||||
client := th.Client
|
||||
|
||||
enableCommands := *th.App.Config().ServiceSettings.EnableCommands
|
||||
allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
|
||||
defer func() {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
|
||||
})
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||
})
|
||||
|
||||
expectedCommandResponse := &model.CommandResponse{
|
||||
Text: "test post command response",
|
||||
ResponseType: model.CommandResponseTypeInChannel,
|
||||
Type: "custom_test",
|
||||
Props: map[string]any{"someprop": "somevalue"},
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
r.ParseForm()
|
||||
require.Equal(t, th.BasicTeam.Name, r.FormValue("team_domain"))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(expectedCommandResponse); err != nil {
|
||||
th.TestLogger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// create a slash command on that team
|
||||
postCmd := &model.Command{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
URL: ts.URL,
|
||||
Method: model.CommandMethodPost,
|
||||
Trigger: "postcommand",
|
||||
}
|
||||
_, appErr := th.App.CreateCommand(postCmd)
|
||||
require.Nil(t, appErr, "failed to create post command")
|
||||
|
||||
// Confirm that the command works when the channel is not read only
|
||||
_, resp, err := client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
// Enable Enterprise features
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
|
||||
th.App.SetPhase2PermissionsMigrationStatus(true)
|
||||
|
||||
_, appErr = th.App.PatchChannelModerationsForChannel(
|
||||
ctx,
|
||||
th.BasicChannel,
|
||||
[]*model.ChannelModerationPatch{{
|
||||
Name: &model.PermissionCreatePost.Id,
|
||||
Roles: &model.ChannelModeratedRolesPatch{
|
||||
Guests: model.NewBool(false),
|
||||
Members: model.NewBool(false),
|
||||
},
|
||||
}})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Confirm that the command fails when the channel is read only
|
||||
_, resp, err = client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand")
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
|
||||
@@ -892,6 +892,7 @@ func TestCompleteOnboarding(t *testing.T) {
|
||||
|
||||
req := &model.CompleteOnboardingRequest{
|
||||
InstallPlugins: []string{"testplugin2"},
|
||||
Organization: "my-org",
|
||||
}
|
||||
|
||||
t.Run("as a regular user", func(t *testing.T) {
|
||||
|
||||
@@ -3106,6 +3106,10 @@ func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
extendedStr := r.URL.Query().Get("extended")
|
||||
extended, _ := strconv.ParseBool(extendedStr)
|
||||
|
||||
@@ -3136,6 +3140,10 @@ func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
options := model.GetUserThreadsOpts{
|
||||
Since: 0,
|
||||
@@ -3213,6 +3221,10 @@ func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Requ
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
|
||||
if err != nil {
|
||||
@@ -3279,6 +3291,10 @@ func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
|
||||
if err != nil {
|
||||
@@ -3338,6 +3354,10 @@ func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http.
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
|
||||
if err != nil {
|
||||
|
||||
@@ -6360,6 +6360,15 @@ func TestGetThreadsForUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uss.TotalUnreadThreads, int64(2))
|
||||
})
|
||||
|
||||
t.Run("should error when not a team member", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
||||
|
||||
_, resp, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestThreadSocketEvents(t *testing.T) {
|
||||
@@ -6855,52 +6864,64 @@ func TestSingleThreadGet(t *testing.T) {
|
||||
})
|
||||
|
||||
client := th.Client
|
||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
|
||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
||||
|
||||
// create a post by regular user
|
||||
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
|
||||
// reply with another
|
||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
|
||||
t.Run("get single thread", func(t *testing.T) {
|
||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
|
||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
||||
|
||||
// create another thread to check that we are not returning it by mistake
|
||||
rpost2, _ := postAndCheck(t, client, &model.Post{
|
||||
ChannelId: th.BasicChannel2.Id,
|
||||
Message: "testMsg2",
|
||||
Metadata: &model.PostMetadata{
|
||||
Priority: &model.PostPriority{
|
||||
Priority: model.NewString(model.PostPriorityUrgent),
|
||||
// create a post by regular user
|
||||
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
|
||||
// reply with another
|
||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
|
||||
|
||||
// create another thread to check that we are not returning it by mistake
|
||||
rpost2, _ := postAndCheck(t, client, &model.Post{
|
||||
ChannelId: th.BasicChannel2.Id,
|
||||
Message: "testMsg2",
|
||||
Metadata: &model.PostMetadata{
|
||||
Priority: &model.PostPriority{
|
||||
Priority: model.NewString(model.PostPriorityUrgent),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id})
|
||||
})
|
||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id})
|
||||
|
||||
// regular user should have two threads with 3 replies total
|
||||
threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
|
||||
// regular user should have two threads with 3 replies total
|
||||
threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
|
||||
|
||||
tr, _, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tr)
|
||||
require.Equal(t, threads.Threads[0].PostId, tr.PostId)
|
||||
require.Empty(t, tr.Participants[0].Username)
|
||||
tr, _, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tr)
|
||||
require.Equal(t, threads.Threads[0].PostId, tr.PostId)
|
||||
require.Empty(t, tr.Participants[0].Username)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.PostPriority = false
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.PostPriority = false
|
||||
})
|
||||
|
||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, tr.Participants[0].Username)
|
||||
require.Equal(t, false, tr.IsUrgent)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.PostPriority = true
|
||||
cfg.FeatureFlags.PostPriority = true
|
||||
})
|
||||
|
||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, tr.IsUrgent)
|
||||
})
|
||||
|
||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, tr.Participants[0].Username)
|
||||
require.Equal(t, false, tr.IsUrgent)
|
||||
t.Run("should error when not a team member", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.PostPriority = true
|
||||
cfg.FeatureFlags.PostPriority = true
|
||||
_, resp, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), false)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, tr.IsUrgent)
|
||||
}
|
||||
|
||||
func TestMaintainUnreadMentionsInThread(t *testing.T) {
|
||||
@@ -7072,6 +7093,23 @@ func TestReadThreads(t *testing.T) {
|
||||
|
||||
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
|
||||
})
|
||||
|
||||
t.Run("should error when not a team member", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
||||
|
||||
_, resp, err := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.GetMillis())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
_, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.NewId())
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
resp, err = th.Client.UpdateThreadsReadForUser(th.BasicUser.Id, th.BasicTeam.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarkThreadUnreadMentionCount(t *testing.T) {
|
||||
|
||||
@@ -2518,6 +2518,9 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove
|
||||
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
|
||||
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userIDToRemove, channel.Id); err != nil {
|
||||
return model.NewAppError("removeUserFromChannel", model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if isGuest {
|
||||
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
|
||||
|
||||
@@ -609,6 +609,85 @@ func TestLeaveDefaultChannel(t *testing.T) {
|
||||
_, err = th.App.GetChannelMember(th.Context, townSquare.Id, guest.Id)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("Trying to leave the default channel should not delete thread memberships", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
ChannelId: townSquare.Id,
|
||||
Message: "root post",
|
||||
UserId: th.BasicUser.Id,
|
||||
}
|
||||
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
reply := &model.Post{
|
||||
ChannelId: townSquare.Id,
|
||||
Message: "reply post",
|
||||
UserId: th.BasicUser.Id,
|
||||
RootId: rpost.Id,
|
||||
}
|
||||
_, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{})
|
||||
require.Nil(t, err)
|
||||
require.Len(t, threads.Threads, 1)
|
||||
|
||||
err = th.App.LeaveChannel(th.Context, townSquare.Id, th.BasicUser.Id)
|
||||
assert.NotNil(t, err, "It should fail to remove a regular user from the default channel")
|
||||
assert.Equal(t, err.Id, "api.channel.remove.default.app_error")
|
||||
|
||||
threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{})
|
||||
require.Nil(t, err)
|
||||
require.Len(t, threads.Threads, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeaveChannel(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
createThread := func(channel *model.Channel) (rpost *model.Post) {
|
||||
t.Helper()
|
||||
post := &model.Post{
|
||||
ChannelId: channel.Id,
|
||||
Message: "root post",
|
||||
UserId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
reply := &model.Post{
|
||||
ChannelId: channel.Id,
|
||||
Message: "reply post",
|
||||
UserId: th.BasicUser.Id,
|
||||
RootId: rpost.Id,
|
||||
}
|
||||
_, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
return rpost
|
||||
}
|
||||
|
||||
t.Run("thread memberships are deleted", func(t *testing.T) {
|
||||
createThread(th.BasicChannel)
|
||||
channel2 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
|
||||
createThread(channel2)
|
||||
|
||||
threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{})
|
||||
require.Nil(t, err)
|
||||
require.Len(t, threads.Threads, 2)
|
||||
|
||||
err = th.App.LeaveChannel(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NotNil(t, err, "It should remove channel membership")
|
||||
|
||||
threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{})
|
||||
require.Nil(t, err)
|
||||
require.Len(t, threads.Threads, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeaveLastChannel(t *testing.T) {
|
||||
|
||||
@@ -28,6 +28,24 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError {
|
||||
}
|
||||
|
||||
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
|
||||
isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud
|
||||
|
||||
if !isCloud && request.Organization == "" {
|
||||
mlog.Error("No organization name provided for self hosted onboarding")
|
||||
return model.NewAppError("CompleteOnboarding", "api.error_no_organization_name_provided_for_self_hosted_onboarding", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if request.Organization != "" {
|
||||
err := a.Srv().Store().System().SaveOrUpdate(&model.System{
|
||||
Name: model.SystemOrganizationName,
|
||||
Value: request.Organization,
|
||||
})
|
||||
if err != nil {
|
||||
// don't block onboarding because of that.
|
||||
a.Log().Error("failed to save organization name", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return a.markAdminOnboardingComplete(c)
|
||||
|
||||
30
server/channels/app/onboarding_test.go
Обычный файл
30
server/channels/app/onboarding_test.go
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
mm_model "github.com/mattermost/mattermost-server/server/v8/model"
|
||||
)
|
||||
|
||||
func TestOnboardingSavesOrganizationName(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{
|
||||
Organization: "Mattermost In Tests",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func() {
|
||||
th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemOrganizationName)
|
||||
}()
|
||||
|
||||
sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemOrganizationName)
|
||||
require.NoError(t, storeErr)
|
||||
require.Equal(t, "Mattermost In Tests", sys.Value)
|
||||
}
|
||||
@@ -212,6 +212,8 @@ channels/db/migrations/mysql/000105_remove_tokens.down.sql
|
||||
channels/db/migrations/mysql/000105_remove_tokens.up.sql
|
||||
channels/db/migrations/mysql/000106_fileinfo_channelid.down.sql
|
||||
channels/db/migrations/mysql/000106_fileinfo_channelid.up.sql
|
||||
channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql
|
||||
channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||
@@ -424,3 +426,5 @@ channels/db/migrations/postgres/000105_remove_tokens.down.sql
|
||||
channels/db/migrations/postgres/000105_remove_tokens.up.sql
|
||||
channels/db/migrations/postgres/000106_fileinfo_channelid.down.sql
|
||||
channels/db/migrations/postgres/000106_fileinfo_channelid.up.sql
|
||||
channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql
|
||||
channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Skipping it because the forward migrations are destructive
|
||||
@@ -0,0 +1,5 @@
|
||||
DELETE FROM
|
||||
tm USING ThreadMemberships AS tm
|
||||
JOIN Threads ON Threads.PostId = tm.PostId
|
||||
WHERE
|
||||
(tm.UserId, Threads.ChannelId) NOT IN (SELECT UserId, ChannelId FROM ChannelMembers);
|
||||
@@ -0,0 +1 @@
|
||||
-- Skipping it because the forward migrations are destructive
|
||||
@@ -0,0 +1,12 @@
|
||||
DELETE FROM threadmemberships WHERE (postid, userid) IN (
|
||||
SELECT
|
||||
threadmemberships.postid,
|
||||
threadmemberships.userid
|
||||
FROM
|
||||
threadmemberships
|
||||
JOIN threads ON threads.postid = threadmemberships.postid
|
||||
LEFT JOIN channelmembers ON channelmembers.userid = threadmemberships.userid
|
||||
AND threads.channelid = channelmembers.channelid
|
||||
WHERE
|
||||
channelmembers.channelid IS NULL
|
||||
);
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
type MetricsInterface interface {
|
||||
Register()
|
||||
RegisterDBCollector(db *sql.DB, name string)
|
||||
UnregisterDBCollector(db *sql.DB, name string)
|
||||
|
||||
IncrementPostCreate()
|
||||
IncrementWebhookPost()
|
||||
|
||||
@@ -319,6 +319,11 @@ func (_m *MetricsInterface) SetReplicaLagTime(node string, value float64) {
|
||||
_m.Called(node, value)
|
||||
}
|
||||
|
||||
// UnregisterDBCollector provides a mock function with given fields: db, name
|
||||
func (_m *MetricsInterface) UnregisterDBCollector(db *sql.DB, name string) {
|
||||
_m.Called(db, name)
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewMetricsInterface interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
|
||||
@@ -10123,6 +10123,24 @@ func (s *OpenTracingLayerThreadStore) DeleteMembershipForUser(userId string, pos
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteMembershipsForChannel")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
|
||||
|
||||
@@ -11563,6 +11563,27 @@ func (s *RetryLayerThreadStore) DeleteMembershipForUser(userId string, postID st
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -335,7 +335,7 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor
|
||||
Id: newCategoryId,
|
||||
UserId: userId,
|
||||
TeamId: teamId,
|
||||
Sorting: model.SidebarCategorySortDefault,
|
||||
Sorting: newCategory.Sorting,
|
||||
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
|
||||
Type: model.SidebarCategoryCustom,
|
||||
Muted: newCategory.Muted,
|
||||
|
||||
@@ -6,9 +6,12 @@ package sqlstore
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
@@ -66,14 +69,18 @@ type sqlxDBWrapper struct {
|
||||
*sqlx.DB
|
||||
queryTimeout time.Duration
|
||||
trace bool
|
||||
isOnline *atomic.Bool
|
||||
}
|
||||
|
||||
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
|
||||
return &sqlxDBWrapper{
|
||||
w := &sqlxDBWrapper{
|
||||
DB: db,
|
||||
queryTimeout: timeout,
|
||||
trace: trace,
|
||||
isOnline: &atomic.Bool{},
|
||||
}
|
||||
w.isOnline.Store(true)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
||||
@@ -83,19 +90,19 @@ func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
||||
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
|
||||
tx, err := w.DB.Beginx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, w.checkErr(err)
|
||||
}
|
||||
|
||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
|
||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
|
||||
tx, err := w.DB.BeginTxx(context.Background(), opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, w.checkErr(err)
|
||||
}
|
||||
|
||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
|
||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
||||
@@ -109,7 +116,7 @@ func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.GetContext(ctx, dest, query, args...)
|
||||
return w.checkErr(w.DB.GetContext(ctx, dest, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
|
||||
@@ -134,7 +141,7 @@ func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.NamedExecContext(ctx, query, arg)
|
||||
return w.checkErrWithResult(w.DB.NamedExecContext(ctx, query, arg))
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
||||
@@ -161,7 +168,7 @@ func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.ExecContext(context.Background(), query, args...)
|
||||
return w.checkErrWithResult(w.DB.ExecContext(context.Background(), query, args...))
|
||||
}
|
||||
|
||||
// ExecRaw is like Exec but without any rebinding of params. You need to pass
|
||||
@@ -176,7 +183,7 @@ func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.ExecContext(ctx, query, args...)
|
||||
return w.checkErrWithResult(w.DB.ExecContext(ctx, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||
@@ -192,7 +199,7 @@ func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.NamedQueryContext(ctx, query, arg)
|
||||
return w.checkErrWithRows(w.DB.NamedQueryContext(ctx, query, arg))
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
||||
@@ -220,7 +227,7 @@ func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.QueryxContext(ctx, query, args)
|
||||
return w.checkErrWithRows(w.DB.QueryxContext(ctx, query, args))
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
|
||||
@@ -238,7 +245,7 @@ func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, a
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.DB.SelectContext(ctx, dest, query, args...)
|
||||
return w.checkErr(w.DB.SelectContext(ctx, dest, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
|
||||
@@ -254,13 +261,15 @@ type sqlxTxWrapper struct {
|
||||
*sqlx.Tx
|
||||
queryTimeout time.Duration
|
||||
trace bool
|
||||
dbw *sqlxDBWrapper
|
||||
}
|
||||
|
||||
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper {
|
||||
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool, dbw *sqlxDBWrapper) *sqlxTxWrapper {
|
||||
return &sqlxTxWrapper{
|
||||
Tx: tx,
|
||||
queryTimeout: timeout,
|
||||
trace: trace,
|
||||
dbw: dbw,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +284,7 @@ func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.Tx.GetContext(ctx, dest, query, args...)
|
||||
return w.dbw.checkErr(w.Tx.GetContext(ctx, dest, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
||||
@@ -284,13 +293,13 @@ func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.Get(dest, query, args...)
|
||||
return w.dbw.checkErr(w.Get(dest, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
||||
query = w.Tx.Rebind(query)
|
||||
|
||||
return w.ExecRaw(query, args...)
|
||||
return w.dbw.checkErrWithResult(w.ExecRaw(query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
|
||||
@@ -302,7 +311,7 @@ func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.Tx.ExecContext(context.Background(), query, args...)
|
||||
return w.dbw.checkErrWithResult(w.Tx.ExecContext(context.Background(), query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
|
||||
@@ -326,7 +335,7 @@ func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.Tx.ExecContext(ctx, query, args...)
|
||||
return w.dbw.checkErrWithResult(w.Tx.ExecContext(ctx, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
||||
@@ -342,7 +351,7 @@ func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.Tx.NamedExecContext(ctx, query, arg)
|
||||
return w.dbw.checkErrWithResult(w.Tx.NamedExecContext(ctx, query, arg))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||
@@ -386,7 +395,7 @@ func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return res.rows, res.err
|
||||
return res.rows, w.dbw.checkErr(res.err)
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
||||
@@ -414,7 +423,7 @@ func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.Tx.QueryxContext(ctx, query, args)
|
||||
return w.dbw.checkErrWithRows(w.Tx.QueryxContext(ctx, query, args))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
||||
@@ -428,7 +437,7 @@ func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
return w.Tx.SelectContext(ctx, dest, query, args...)
|
||||
return w.dbw.checkErr(w.Tx.SelectContext(ctx, dest, query, args...))
|
||||
}
|
||||
|
||||
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
|
||||
@@ -459,3 +468,23 @@ func printArgs(query string, dur time.Duration, args ...any) {
|
||||
}
|
||||
mlog.Debug(query, fields...)
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) checkErrWithResult(res sql.Result, err error) (sql.Result, error) {
|
||||
return res, w.checkErr(err)
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) checkErrWithRows(res *sqlx.Rows, err error) (*sqlx.Rows, error) {
|
||||
return res, w.checkErr(err)
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) checkErr(err error) error {
|
||||
var netError *net.OpError
|
||||
if errors.As(err, &netError) && (!netError.Temporary() && !netError.Timeout()) {
|
||||
w.isOnline.Store(false)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *sqlxDBWrapper) Online() bool {
|
||||
return w.isOnline.Load()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package sqlstore
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,12 +29,14 @@ func TestSqlX(t *testing.T) {
|
||||
}
|
||||
*settings.QueryTimeout = 1
|
||||
store := &SqlStore{
|
||||
rrCounter: 0,
|
||||
srCounter: 0,
|
||||
settings: settings,
|
||||
rrCounter: 0,
|
||||
srCounter: 0,
|
||||
settings: settings,
|
||||
quitMonitor: make(chan struct{}),
|
||||
wgMonitor: &sync.WaitGroup{},
|
||||
}
|
||||
|
||||
store.initConnection()
|
||||
require.NoError(t, store.initConnection())
|
||||
|
||||
defer store.Close()
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const (
|
||||
MySQLForeignKeyViolationErrorCode = 1452
|
||||
PGDuplicateObjectErrorCode = "42710"
|
||||
MySQLDuplicateObjectErrorCode = 1022
|
||||
DBPingAttempts = 18
|
||||
DBPingAttempts = 5
|
||||
DBPingTimeoutSecs = 10
|
||||
// This is a numerical version string by postgres. The format is
|
||||
// 2 characters for major, minor, and patch version prior to 10.
|
||||
@@ -123,9 +123,9 @@ type SqlStore struct {
|
||||
|
||||
masterX *sqlxDBWrapper
|
||||
|
||||
ReplicaXs []*sqlxDBWrapper
|
||||
ReplicaXs []*atomic.Pointer[sqlxDBWrapper]
|
||||
|
||||
searchReplicaXs []*sqlxDBWrapper
|
||||
searchReplicaXs []*atomic.Pointer[sqlxDBWrapper]
|
||||
|
||||
replicaLagHandles []*dbsql.DB
|
||||
stores SqlStoreStores
|
||||
@@ -138,17 +138,28 @@ type SqlStore struct {
|
||||
|
||||
isBinaryParam bool
|
||||
pgDefaultTextSearchConfig string
|
||||
|
||||
quitMonitor chan struct{}
|
||||
wgMonitor *sync.WaitGroup
|
||||
}
|
||||
|
||||
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
|
||||
store := &SqlStore{
|
||||
rrCounter: 0,
|
||||
srCounter: 0,
|
||||
settings: &settings,
|
||||
metrics: metrics,
|
||||
rrCounter: 0,
|
||||
srCounter: 0,
|
||||
settings: &settings,
|
||||
metrics: metrics,
|
||||
quitMonitor: make(chan struct{}),
|
||||
wgMonitor: &sync.WaitGroup{},
|
||||
}
|
||||
|
||||
store.initConnection()
|
||||
err := store.initConnection()
|
||||
if err != nil {
|
||||
mlog.Fatal("Error setting up connections", mlog.Err(err))
|
||||
}
|
||||
|
||||
store.wgMonitor.Add(1)
|
||||
go store.monitorReplicas()
|
||||
|
||||
ver, err := store.GetDbVersion(true)
|
||||
if err != nil {
|
||||
@@ -230,29 +241,28 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
|
||||
|
||||
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
|
||||
// It also applies any database configuration settings that are required.
|
||||
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB {
|
||||
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings, attempts int) (*dbsql.DB, error) {
|
||||
db, err := dbsql.Open(*settings.DriverName, dataSource)
|
||||
if err != nil {
|
||||
mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err))
|
||||
return nil, errors.Wrap(err, "failed to open SQL connection")
|
||||
}
|
||||
|
||||
for i := 0; i < DBPingAttempts; i++ {
|
||||
for i := 0; i < attempts; i++ {
|
||||
// At this point, we have passed sql.Open, so we deliberately ignore any errors.
|
||||
sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource)
|
||||
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
|
||||
defer cancel()
|
||||
err = db.PingContext(ctx)
|
||||
if err == nil {
|
||||
break
|
||||
} else {
|
||||
if i == DBPingAttempts-1 {
|
||||
mlog.Fatal("Failed to ping DB, server will exit.", mlog.Err(err))
|
||||
} else {
|
||||
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
|
||||
time.Sleep(DBPingTimeoutSecs * time.Second)
|
||||
if err != nil {
|
||||
if i == attempts-1 {
|
||||
return nil, err
|
||||
}
|
||||
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
|
||||
time.Sleep(DBPingTimeoutSecs * time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(connType, replicaLagPrefix) {
|
||||
@@ -272,7 +282,7 @@ func SetupConnection(connType string, dataSource string, settings *model.SqlSett
|
||||
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
||||
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
|
||||
|
||||
return db
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (ss *SqlStore) SetContext(context context.Context) {
|
||||
@@ -285,7 +295,7 @@ func (ss *SqlStore) Context() context.Context {
|
||||
|
||||
func noOpMapper(s string) string { return s }
|
||||
|
||||
func (ss *SqlStore) initConnection() {
|
||||
func (ss *SqlStore) initConnection() error {
|
||||
dataSource := *ss.settings.DataSource
|
||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
|
||||
@@ -294,11 +304,14 @@ func (ss *SqlStore) initConnection() {
|
||||
var err error
|
||||
dataSource, err = ResetReadTimeout(dataSource)
|
||||
if err != nil {
|
||||
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource))
|
||||
return errors.Wrap(err, "failed to reset read timeout from datasource")
|
||||
}
|
||||
}
|
||||
|
||||
handle := SetupConnection("master", dataSource, ss.settings)
|
||||
handle, err := SetupConnection("master", dataSource, ss.settings, DBPingAttempts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||
*ss.settings.Trace)
|
||||
@@ -310,34 +323,32 @@ func (ss *SqlStore) initConnection() {
|
||||
}
|
||||
|
||||
if len(ss.settings.DataSourceReplicas) > 0 {
|
||||
ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas))
|
||||
ss.ReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceReplicas))
|
||||
for i, replica := range ss.settings.DataSourceReplicas {
|
||||
handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings)
|
||||
ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||
*ss.settings.Trace)
|
||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||
ss.ReplicaXs[i].MapperFunc(noOpMapper)
|
||||
}
|
||||
if ss.metrics != nil {
|
||||
ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i))
|
||||
ss.ReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{}
|
||||
handle, err = SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings, DBPingAttempts)
|
||||
if err != nil {
|
||||
// Initializing to be offline
|
||||
ss.ReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}})
|
||||
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("replica-%v", i)), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
ss.setDB(ss.ReplicaXs[i], handle, "replica-"+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
if len(ss.settings.DataSourceSearchReplicas) > 0 {
|
||||
ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas))
|
||||
ss.searchReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceSearchReplicas))
|
||||
for i, replica := range ss.settings.DataSourceSearchReplicas {
|
||||
handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings)
|
||||
ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||
*ss.settings.Trace)
|
||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||
ss.searchReplicaXs[i].MapperFunc(noOpMapper)
|
||||
}
|
||||
if ss.metrics != nil {
|
||||
ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i))
|
||||
ss.searchReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{}
|
||||
handle, err = SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings, DBPingAttempts)
|
||||
if err != nil {
|
||||
// Initializing to be offline
|
||||
ss.searchReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}})
|
||||
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("search-replica-%v", i)), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
ss.setDB(ss.searchReplicaXs[i], handle, "searchreplica-"+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,9 +358,14 @@ func (ss *SqlStore) initConnection() {
|
||||
if src.DataSource == nil {
|
||||
continue
|
||||
}
|
||||
ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings)
|
||||
ss.replicaLagHandles[i], err = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings, DBPingAttempts)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed to setup replica lag handle. Skipping..", mlog.String("db", fmt.Sprintf(replicaLagPrefix+"-%d", i)), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *SqlStore) DriverName() string {
|
||||
@@ -455,8 +471,15 @@ func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper {
|
||||
return ss.GetReplicaX()
|
||||
}
|
||||
|
||||
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
|
||||
return ss.searchReplicaXs[rrNum]
|
||||
for i := 0; i < len(ss.searchReplicaXs); i++ {
|
||||
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
|
||||
if ss.searchReplicaXs[rrNum].Load().Online() {
|
||||
return ss.searchReplicaXs[rrNum].Load()
|
||||
}
|
||||
}
|
||||
|
||||
// If all search replicas are down, then go with replica.
|
||||
return ss.GetReplicaX()
|
||||
}
|
||||
|
||||
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
||||
@@ -464,23 +487,64 @@ func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
||||
return ss.GetMasterX()
|
||||
}
|
||||
|
||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||
return ss.ReplicaXs[rrNum]
|
||||
}
|
||||
|
||||
func (ss *SqlStore) GetInternalReplicaDBs() []*sql.DB {
|
||||
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
|
||||
return []*sql.DB{
|
||||
ss.GetMasterX().DB.DB,
|
||||
for i := 0; i < len(ss.ReplicaXs); i++ {
|
||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||
if ss.ReplicaXs[rrNum].Load().Online() {
|
||||
return ss.ReplicaXs[rrNum].Load()
|
||||
}
|
||||
}
|
||||
|
||||
dbs := make([]*sql.DB, len(ss.ReplicaXs))
|
||||
for i, rx := range ss.ReplicaXs {
|
||||
dbs[i] = rx.DB.DB
|
||||
}
|
||||
// If all replicas are down, then go with master.
|
||||
return ss.GetMasterX()
|
||||
}
|
||||
|
||||
return dbs
|
||||
func (ss *SqlStore) monitorReplicas() {
|
||||
t := time.NewTicker(time.Duration(*ss.settings.ReplicaMonitorIntervalSeconds) * time.Second)
|
||||
defer func() {
|
||||
t.Stop()
|
||||
ss.wgMonitor.Done()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-ss.quitMonitor:
|
||||
return
|
||||
case <-t.C:
|
||||
setupReplica := func(r *atomic.Pointer[sqlxDBWrapper], dsn, name string) {
|
||||
if r.Load().Online() {
|
||||
return
|
||||
}
|
||||
|
||||
handle, err := SetupConnection(name, dsn, ss.settings, 1)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", name), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
if ss.metrics != nil && r.Load() != nil && r.Load().DB != nil {
|
||||
ss.metrics.UnregisterDBCollector(r.Load().DB.DB, name)
|
||||
}
|
||||
ss.setDB(r, handle, name)
|
||||
}
|
||||
for i, replica := range ss.ReplicaXs {
|
||||
setupReplica(replica, ss.settings.DataSourceReplicas[i], "replica-"+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
for i, replica := range ss.searchReplicaXs {
|
||||
setupReplica(replica, ss.settings.DataSourceSearchReplicas[i], "search-replica-"+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *SqlStore) setDB(replica *atomic.Pointer[sqlxDBWrapper], handle *dbsql.DB, name string) {
|
||||
replica.Store(newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||
*ss.settings.Trace))
|
||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||
replica.Load().MapperFunc(noOpMapper)
|
||||
}
|
||||
if ss.metrics != nil {
|
||||
ss.metrics.RegisterDBCollector(replica.Load().DB.DB, name)
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
||||
@@ -489,7 +553,7 @@ func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
||||
}
|
||||
|
||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||
return ss.ReplicaXs[rrNum].DB.DB
|
||||
return ss.ReplicaXs[rrNum].Load().DB.DB
|
||||
}
|
||||
|
||||
func (ss *SqlStore) TotalMasterDbConnections() int {
|
||||
@@ -541,7 +605,10 @@ func (ss *SqlStore) TotalReadDbConnections() int {
|
||||
|
||||
count := 0
|
||||
for _, db := range ss.ReplicaXs {
|
||||
count = count + db.Stats().OpenConnections
|
||||
if !db.Load().Online() {
|
||||
continue
|
||||
}
|
||||
count = count + db.Load().Stats().OpenConnections
|
||||
}
|
||||
|
||||
return count
|
||||
@@ -554,7 +621,10 @@ func (ss *SqlStore) TotalSearchDbConnections() int {
|
||||
|
||||
count := 0
|
||||
for _, db := range ss.searchReplicaXs {
|
||||
count = count + db.Stats().OpenConnections
|
||||
if !db.Load().Online() {
|
||||
continue
|
||||
}
|
||||
count = count + db.Load().Stats().OpenConnections
|
||||
}
|
||||
|
||||
return count
|
||||
@@ -782,9 +852,14 @@ func IsUniqueConstraintError(err error, indexName []string) bool {
|
||||
}
|
||||
|
||||
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
|
||||
all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1)
|
||||
copy(all, ss.ReplicaXs)
|
||||
all[len(ss.ReplicaXs)] = ss.masterX
|
||||
all := make([]*sqlxDBWrapper, 0, len(ss.ReplicaXs)+1)
|
||||
for i := range ss.ReplicaXs {
|
||||
if !ss.ReplicaXs[i].Load().Online() {
|
||||
continue
|
||||
}
|
||||
all = append(all, ss.ReplicaXs[i].Load())
|
||||
}
|
||||
all = append(all, ss.masterX)
|
||||
return all
|
||||
}
|
||||
|
||||
@@ -807,11 +882,24 @@ func (ss *SqlStore) RecycleDBConnections(d time.Duration) {
|
||||
|
||||
func (ss *SqlStore) Close() {
|
||||
ss.masterX.Close()
|
||||
// Closing monitor and waiting for it to be done.
|
||||
// This needs to be done before closing the replica handles.
|
||||
close(ss.quitMonitor)
|
||||
ss.wgMonitor.Wait()
|
||||
|
||||
for _, replica := range ss.ReplicaXs {
|
||||
replica.Close()
|
||||
if replica.Load().Online() {
|
||||
replica.Load().Close()
|
||||
}
|
||||
}
|
||||
|
||||
for _, replica := range ss.searchReplicaXs {
|
||||
if replica.Load().Online() {
|
||||
replica.Load().Close()
|
||||
}
|
||||
}
|
||||
|
||||
for _, replica := range ss.replicaLagHandles {
|
||||
replica.Close()
|
||||
}
|
||||
}
|
||||
@@ -1132,7 +1220,10 @@ func (ss *SqlStore) migrate(direction migrationDirection) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db := SetupConnection("master", dataSource, ss.settings)
|
||||
db, err2 := SetupConnection("master", dataSource, ss.settings, DBPingAttempts)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
driver, err = ms.WithInstance(db)
|
||||
defer db.Close()
|
||||
case model.DatabaseDriverPostgres:
|
||||
|
||||
@@ -761,13 +761,15 @@ func TestReplicaLagQuery(t *testing.T) {
|
||||
mockMetrics.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master")
|
||||
|
||||
store := &SqlStore{
|
||||
rrCounter: 0,
|
||||
srCounter: 0,
|
||||
settings: settings,
|
||||
metrics: mockMetrics,
|
||||
rrCounter: 0,
|
||||
srCounter: 0,
|
||||
settings: settings,
|
||||
metrics: mockMetrics,
|
||||
quitMonitor: make(chan struct{}),
|
||||
wgMonitor: &sync.WaitGroup{},
|
||||
}
|
||||
|
||||
store.initConnection()
|
||||
require.NoError(t, store.initConnection())
|
||||
store.stores.post = newSqlPostStore(store, mockMetrics)
|
||||
err = store.migrate(migrationsDirectionUp)
|
||||
require.NoError(t, err)
|
||||
@@ -839,9 +841,11 @@ func TestMySQLReadTimeout(t *testing.T) {
|
||||
settings.DataSource = &dataSource
|
||||
|
||||
store := &SqlStore{
|
||||
settings: settings,
|
||||
settings: settings,
|
||||
quitMonitor: make(chan struct{}),
|
||||
wgMonitor: &sync.WaitGroup{},
|
||||
}
|
||||
store.initConnection()
|
||||
require.NoError(t, store.initConnection())
|
||||
defer store.Close()
|
||||
|
||||
_, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`)
|
||||
|
||||
@@ -688,6 +688,28 @@ func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (*
|
||||
return s.updateMembership(s.GetMasterX(), membership)
|
||||
}
|
||||
|
||||
func (s *SqlThreadStore) DeleteMembershipsForChannel(userID, channelID string) error {
|
||||
subQuery := s.getSubQueryBuilder().
|
||||
Select("1").
|
||||
From("Threads").
|
||||
Where(sq.And{
|
||||
sq.Expr("Threads.PostId = ThreadMemberships.PostId"),
|
||||
sq.Eq{"Threads.ChannelId": channelID},
|
||||
})
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Delete("ThreadMemberships").
|
||||
Where(sq.Eq{"UserId": userID}).
|
||||
Where(sq.Expr("EXISTS (?)", subQuery))
|
||||
|
||||
_, err := s.GetMasterX().ExecBuilder(query)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to remove thread memberships with userid=%s channelid=%s", userID, channelID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Update("ThreadMemberships").
|
||||
@@ -712,7 +734,14 @@ func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model.
|
||||
memberships := []*model.ThreadMembership{}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("ThreadMemberships.*").
|
||||
Select(
|
||||
"ThreadMemberships.PostId",
|
||||
"ThreadMemberships.UserId",
|
||||
"ThreadMemberships.Following",
|
||||
"ThreadMemberships.LastUpdated",
|
||||
"ThreadMemberships.LastViewed",
|
||||
"ThreadMemberships.UnreadMentions",
|
||||
).
|
||||
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
|
||||
From("ThreadMemberships").
|
||||
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
|
||||
@@ -732,7 +761,14 @@ func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.Thr
|
||||
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
|
||||
var membership model.ThreadMembership
|
||||
query := s.getQueryBuilder().
|
||||
Select("*").
|
||||
Select(
|
||||
"PostId",
|
||||
"UserId",
|
||||
"Following",
|
||||
"LastUpdated",
|
||||
"LastViewed",
|
||||
"UnreadMentions",
|
||||
).
|
||||
From("ThreadMemberships").
|
||||
Where(sq.And{
|
||||
sq.Eq{"PostId": postId},
|
||||
|
||||
@@ -72,10 +72,7 @@ type Store interface {
|
||||
// GetInternalMasterDB allows access to the raw master DB
|
||||
// handle for the multi-product architecture.
|
||||
GetInternalMasterDB() *sql.DB
|
||||
// GetInternalReplicaDBs allows access to the raw replica DB
|
||||
// handles for the multi-product architecture.
|
||||
GetInternalReplicaDB() *sql.DB
|
||||
GetInternalReplicaDBs() []*sql.DB
|
||||
TotalMasterDbConnections() int
|
||||
TotalReadDbConnections() int
|
||||
TotalSearchDbConnections() int
|
||||
@@ -347,6 +344,7 @@ type ThreadStore interface {
|
||||
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
|
||||
DeleteOrphanedRows(limit int) (deleted int64, err error)
|
||||
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
|
||||
DeleteMembershipsForChannel(userID, channelID string) error
|
||||
|
||||
// Insights - threads
|
||||
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
||||
|
||||
@@ -672,6 +672,38 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{}, res2.Channels)
|
||||
})
|
||||
|
||||
t.Run("should store the correct sorting value", func(t *testing.T) {
|
||||
userId := model.NewId()
|
||||
|
||||
team := setupTeam(t, ss, userId)
|
||||
|
||||
opts := &store.SidebarCategorySearchOpts{
|
||||
TeamID: team.Id,
|
||||
ExcludeTeam: false,
|
||||
}
|
||||
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
|
||||
require.NoError(t, nErr)
|
||||
require.NotEmpty(t, res)
|
||||
// Create the category
|
||||
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
|
||||
SidebarCategory: model.SidebarCategory{
|
||||
DisplayName: model.NewId(),
|
||||
Sorting: model.SidebarCategorySortManual,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm that sorting value is correct
|
||||
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Categories, 4)
|
||||
// first category will be favorites and second will be newly created
|
||||
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
|
||||
assert.Equal(t, created.Id, res.Categories[1].Id)
|
||||
assert.Equal(t, model.SidebarCategorySortManual, res.Categories[1].Sorting)
|
||||
assert.Equal(t, model.SidebarCategorySortManual, created.Sorting)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
|
||||
|
||||
@@ -346,22 +346,6 @@ func (_m *Store) GetInternalReplicaDB() *sql.DB {
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetInternalReplicaDBs provides a mock function with given fields:
|
||||
func (_m *Store) GetInternalReplicaDBs() []*sql.DB {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*sql.DB
|
||||
if rf, ok := ret.Get(0).(func() []*sql.DB); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*sql.DB)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Group provides a mock function with given fields:
|
||||
func (_m *Store) Group() store.GroupStore {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -29,6 +29,20 @@ func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) err
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteMembershipsForChannel provides a mock function with given fields: userID, channelID
|
||||
func (_m *ThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||
ret := _m.Called(userID, channelID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(userID, channelID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteOrphanedRows provides a mock function with given fields: limit
|
||||
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||
ret := _m.Called(limit)
|
||||
|
||||
@@ -261,6 +261,7 @@ func MakeSqlSettings(driver string, withReplica bool) *model.SqlSettings {
|
||||
}
|
||||
|
||||
log("Created temporary " + driver + " database " + dbName)
|
||||
settings.ReplicaMonitorIntervalSeconds = model.NewInt(5)
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
|
||||
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
|
||||
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
|
||||
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
|
||||
t.Run("DeleteMembershipsForChannel", func(t *testing.T) { testDeleteMembershipsForChannel(t, ss) })
|
||||
}
|
||||
|
||||
func testThreadStorePopulation(t *testing.T, ss store.Store) {
|
||||
@@ -1914,3 +1915,121 @@ func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) {
|
||||
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteMembershipsForChannel(t *testing.T, ss store.Store) {
|
||||
createThreadMembership := func(userID, postID string) (*model.ThreadMembership, func()) {
|
||||
t.Helper()
|
||||
opts := store.ThreadMembershipOpts{
|
||||
Following: true,
|
||||
IncrementMentions: false,
|
||||
UpdateFollowing: true,
|
||||
UpdateViewedTimestamp: false,
|
||||
UpdateParticipants: false,
|
||||
}
|
||||
mem, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
return mem, func() {
|
||||
err := ss.Thread().DeleteMembershipForUser(userID, postID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
postingUserID := model.NewId()
|
||||
userAID := model.NewId()
|
||||
userBID := model.NewId()
|
||||
|
||||
team, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
channel1, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel1" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
channel2, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team.Id,
|
||||
DisplayName: "DisplayName2",
|
||||
Name: "channel2" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
rootPost1, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel1.Id,
|
||||
UserId: postingUserID,
|
||||
Message: model.NewRandomString(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ss.Post().Save(&model.Post{
|
||||
ChannelId: channel1.Id,
|
||||
UserId: postingUserID,
|
||||
Message: model.NewRandomString(10),
|
||||
RootId: rootPost1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rootPost2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel2.Id,
|
||||
UserId: postingUserID,
|
||||
Message: model.NewRandomString(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Post().Save(&model.Post{
|
||||
ChannelId: channel2.Id,
|
||||
UserId: postingUserID,
|
||||
Message: model.NewRandomString(10),
|
||||
RootId: rootPost2.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should return memberships for user", func(t *testing.T) {
|
||||
memA1, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
||||
defer cleanupA1()
|
||||
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
||||
defer cleanupA2()
|
||||
|
||||
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, membershipsA, 2)
|
||||
require.ElementsMatch(t, []*model.ThreadMembership{memA1, memA2}, membershipsA)
|
||||
})
|
||||
|
||||
t.Run("should delete memberships for user for channel", func(t *testing.T) {
|
||||
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
||||
defer cleanupA1()
|
||||
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
||||
defer cleanupA2()
|
||||
|
||||
ss.Thread().DeleteMembershipsForChannel(userAID, channel1.Id)
|
||||
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, membershipsA, 1)
|
||||
require.ElementsMatch(t, []*model.ThreadMembership{memA2}, membershipsA)
|
||||
})
|
||||
|
||||
t.Run("deleting memberships for channel for userA should not affect userB", func(t *testing.T) {
|
||||
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
||||
defer cleanupA1()
|
||||
_, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
||||
defer cleanupA2()
|
||||
memB1, cleanupB2 := createThreadMembership(userBID, rootPost1.Id)
|
||||
defer cleanupB2()
|
||||
|
||||
membershipsB, err := ss.Thread().GetMembershipsForUser(userBID, team.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, membershipsB, 1)
|
||||
require.ElementsMatch(t, []*model.ThreadMembership{memB1}, membershipsB)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9112,6 +9112,22 @@ func (s *TimerLayerThreadStore) DeleteMembershipForUser(userId string, postID st
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||
start := time.Now()
|
||||
|
||||
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteMembershipsForChannel", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||
start := time.Now()
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ func (h *MainHelper) SetReplicationLagForTesting(seconds int) error {
|
||||
|
||||
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
|
||||
for _, replica := range h.SQLStore.ReplicaXs {
|
||||
_, err := replica.Exec(query, args...)
|
||||
_, err := replica.Load().Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
||||
|
||||
props["CWSURL"] = *c.CloudSettings.CWSURL
|
||||
|
||||
props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus)
|
||||
|
||||
// Set default values for all options that require a license.
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
||||
props["LdapNicknameAttributeSet"] = "false"
|
||||
|
||||
@@ -1777,6 +1777,10 @@
|
||||
"id": "api.error_get_first_admin_visit_marketplace_status",
|
||||
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
||||
},
|
||||
{
|
||||
"id": "api.error_no_organization_name_provided_for_self_hosted_onboarding",
|
||||
"translation": "Error no organization name provided for self hosted onboarding."
|
||||
},
|
||||
{
|
||||
"id": "api.error_set_first_admin_complete_setup",
|
||||
"translation": "Error trying to save first admin complete setup in the store."
|
||||
|
||||
@@ -974,6 +974,7 @@ type ExperimentalSettings struct {
|
||||
EnableRemoteClusterService *bool `access:"experimental_features"`
|
||||
EnableAppBar *bool `access:"experimental_features"`
|
||||
PatchPluginsReactDOM *bool `access:"experimental_features"`
|
||||
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
||||
}
|
||||
|
||||
func (s *ExperimentalSettings) SetDefaults() {
|
||||
@@ -1012,6 +1013,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
||||
if s.PatchPluginsReactDOM == nil {
|
||||
s.PatchPluginsReactDOM = NewBool(false)
|
||||
}
|
||||
|
||||
if s.DisableRefetchingOnBrowserFocus == nil {
|
||||
s.DisableRefetchingOnBrowserFocus = NewBool(false)
|
||||
}
|
||||
}
|
||||
|
||||
type AnalyticsSettings struct {
|
||||
@@ -1163,6 +1168,7 @@ type SqlSettings struct {
|
||||
DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||
MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||
ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||
ReplicaMonitorIntervalSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||
}
|
||||
|
||||
func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
||||
@@ -1227,6 +1233,10 @@ func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
||||
if s.ReplicaLagSettings == nil {
|
||||
s.ReplicaLagSettings = []*ReplicaLagSettings{}
|
||||
}
|
||||
|
||||
if s.ReplicaMonitorIntervalSeconds == nil {
|
||||
s.ReplicaMonitorIntervalSeconds = NewInt(5)
|
||||
}
|
||||
}
|
||||
|
||||
type LogSettings struct {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
// CompleteOnboardingRequest describes parameters of the requested plugin.
|
||||
type CompleteOnboardingRequest struct {
|
||||
Organization string `json:"organization"` // Organization is the name of the organization
|
||||
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ type Permission struct {
|
||||
|
||||
var PermissionInviteUser *Permission
|
||||
var PermissionAddUserToTeam *Permission
|
||||
|
||||
// Deprecated: PermissionCreatePost should be used to determine if a slash command can be executed.
|
||||
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||
var PermissionUseSlashCommands *Permission
|
||||
var PermissionManageSlashCommands *Permission
|
||||
var PermissionManageOthersSlashCommands *Permission
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey"
|
||||
SystemPostActionCookieSecretKey = "PostActionCookieSecret"
|
||||
SystemInstallationDateKey = "InstallationDate"
|
||||
SystemOrganizationName = "OrganizationName"
|
||||
SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp"
|
||||
SystemClusterEncryptionKey = "ClusterEncryptionKey"
|
||||
SystemUpgradedFromTeId = "UpgradedFromTE"
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// It should be maintained in chronological order with most current
|
||||
// release at the front of the list.
|
||||
var versions = []string{
|
||||
"7.11.0",
|
||||
"7.10.0",
|
||||
"7.9.0",
|
||||
"7.8.0",
|
||||
|
||||
@@ -521,6 +521,7 @@ func (ts *TelemetryService) trackConfig() {
|
||||
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
||||
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
|
||||
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
|
||||
"replica_monitor_interval_seconds": *cfg.SqlSettings.ReplicaMonitorIntervalSeconds,
|
||||
})
|
||||
|
||||
ts.SendTelemetry(TrackConfigLog, map[string]any{
|
||||
@@ -749,15 +750,16 @@ func (ts *TelemetryService) trackConfig() {
|
||||
})
|
||||
|
||||
ts.SendTelemetry(TrackConfigExperimental, map[string]any{
|
||||
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
|
||||
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
|
||||
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
|
||||
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
|
||||
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
|
||||
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
|
||||
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
|
||||
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
|
||||
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
|
||||
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
|
||||
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
|
||||
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
|
||||
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
|
||||
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
|
||||
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
|
||||
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
|
||||
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
|
||||
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
|
||||
"disable_refetching_on_browser_focus": *cfg.ExperimentalSettings.DisableRefetchingOnBrowserFocus,
|
||||
})
|
||||
|
||||
ts.SendTelemetry(TrackConfigAnalytics, map[string]any{
|
||||
|
||||
Ссылка в новой задаче
Block a user