MM-52600: [Shared Channels] Shared channels do not sync channel membership (#30976)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0082e3e94d
Коммит
fa1c77d9b0
@@ -1711,6 +1711,13 @@ func (a *App) addUserToChannel(c request.CTX, user *model.User, channel *model.C
|
||||
a.Srv().Platform().InvalidateChannelCacheForUser(user.Id)
|
||||
a.invalidateCacheForChannelMembers(channel.Id)
|
||||
|
||||
// Synchronize membership change for shared channels
|
||||
if channel.IsShared() {
|
||||
if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil {
|
||||
scs.HandleMembershipChange(channel.Id, user.Id, true, user.GetRemoteID())
|
||||
}
|
||||
}
|
||||
|
||||
return newMember, nil
|
||||
}
|
||||
|
||||
@@ -2236,7 +2243,12 @@ func (s *Server) getChannelMemberLastViewedAt(c request.CTX, channelID string, u
|
||||
}
|
||||
|
||||
func (a *App) GetChannelMembersPage(c request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
|
||||
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
|
||||
opts := model.ChannelMembersGetOptions{
|
||||
ChannelID: channelID,
|
||||
Offset: page * perPage,
|
||||
Limit: perPage,
|
||||
}
|
||||
channelMembers, err := a.Srv().Store().Channel().GetMembers(opts)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
@@ -2740,6 +2752,14 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove
|
||||
userMsg.Add("remover_id", removerUserId)
|
||||
a.Publish(userMsg)
|
||||
|
||||
// Synchronize membership change for shared channels
|
||||
if channel.IsShared() {
|
||||
// isAdd=false, empty remoteId means locally initiated
|
||||
if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil {
|
||||
scs.HandleMembershipChange(channel.Id, userIDToRemove, false, "")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3639,7 +3659,12 @@ func (a *App) forEachChannelMember(c request.CTX, channelID string, f func(model
|
||||
page := 0
|
||||
|
||||
for {
|
||||
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
|
||||
opts := model.ChannelMembersGetOptions{
|
||||
ChannelID: channelID,
|
||||
Offset: page * perPage,
|
||||
Limit: perPage,
|
||||
}
|
||||
channelMembers, err := a.Srv().Store().Channel().GetMembers(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2513,8 +2513,16 @@ func TestClearChannelMembersCache(t *testing.T) {
|
||||
ChannelId: "1",
|
||||
})
|
||||
}
|
||||
mockChannelStore.On("GetMembers", "channelID", 0, 100).Return(cms, nil)
|
||||
mockChannelStore.On("GetMembers", "channelID", 100, 100).Return(model.ChannelMembers{
|
||||
mockChannelStore.On("GetMembers", model.ChannelMembersGetOptions{
|
||||
ChannelID: "channelID",
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
}).Return(cms, nil)
|
||||
mockChannelStore.On("GetMembers", model.ChannelMembersGetOptions{
|
||||
ChannelID: "channelID",
|
||||
Offset: 100,
|
||||
Limit: 100,
|
||||
}).Return(model.ChannelMembers{
|
||||
model.ChannelMember{
|
||||
ChannelId: "1",
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ type SharedChannelServiceIFace interface {
|
||||
CheckChannelNotShared(channelID string) error
|
||||
CheckChannelIsShared(channelID string) error
|
||||
CheckCanInviteToSharedChannel(channelId string) error
|
||||
HandleMembershipChange(channelID, userID string, isAdd bool, remoteID string)
|
||||
}
|
||||
|
||||
type MockOptionSharedChannelService func(service *mockSharedChannelService)
|
||||
@@ -77,3 +78,7 @@ func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel,
|
||||
func (mrcs *mockSharedChannelService) NumInvitations() int {
|
||||
return mrcs.numInvitations
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) HandleMembershipChange(channelID, userID string, isAdd bool, remoteID string) {
|
||||
// This is a mock implementation - it doesn't need to do anything
|
||||
}
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -26,6 +26,7 @@ type SharedChannelServiceIFace interface {
|
||||
CheckChannelNotShared(channelID string) error
|
||||
CheckChannelIsShared(channelID string) error
|
||||
CheckCanInviteToSharedChannel(channelId string) error
|
||||
HandleMembershipChange(channelID, userID string, isAdd bool, remoteID string)
|
||||
}
|
||||
|
||||
func NewMockSharedChannelService(service SharedChannelServiceIFace) *mockSharedChannelService {
|
||||
@@ -91,3 +92,9 @@ func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel,
|
||||
func (mrcs *mockSharedChannelService) NumInvitations() int {
|
||||
return mrcs.numInvitations
|
||||
}
|
||||
|
||||
func (mrcs *mockSharedChannelService) HandleMembershipChange(channelID, userID string, isAdd bool, remoteID string) {
|
||||
if mrcs.SharedChannelServiceIFace != nil {
|
||||
mrcs.SharedChannelServiceIFace.HandleMembershipChange(channelID, userID, isAdd, remoteID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,27 @@ func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
// Handle membership sync using unified field
|
||||
if len(syncMsg.MembershipChanges) > 0 {
|
||||
batch := make([]string, 0)
|
||||
for _, change := range syncMsg.MembershipChanges {
|
||||
if change.IsAdd {
|
||||
syncResp.UsersSyncd = append(syncResp.UsersSyncd, change.UserId)
|
||||
batch = append(batch, change.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
// Call appropriate callback
|
||||
if len(batch) > 0 {
|
||||
if h.OnBatchSync != nil {
|
||||
h.OnBatchSync(batch, currentCall)
|
||||
}
|
||||
if len(batch) == 1 && h.OnIndividualSync != nil {
|
||||
h.OnIndividualSync(batch[0], currentCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = response.SetPayload(syncResp)
|
||||
}
|
||||
}
|
||||
@@ -135,28 +156,36 @@ func (h *SelfReferentialSyncHandler) GetSyncMessageCount() int32 {
|
||||
return atomic.LoadInt32(h.syncMessageCount)
|
||||
}
|
||||
|
||||
// GetUsersFromSyncMsg extracts user IDs from a sync message
|
||||
func GetUsersFromSyncMsg(msg model.SyncMsg) []string {
|
||||
var userIds []string
|
||||
|
||||
// Extract from users field
|
||||
for userId := range msg.Users {
|
||||
userIds = append(userIds, userId)
|
||||
}
|
||||
|
||||
return userIds
|
||||
}
|
||||
|
||||
// EnsureCleanState ensures a clean test state by removing all shared channels, remote clusters,
|
||||
// and extra team/channel members. This helps prevent state pollution between tests.
|
||||
func EnsureCleanState(t *testing.T, th *TestHelper, ss store.Store) {
|
||||
t.Helper()
|
||||
|
||||
// First, wait for any pending async tasks to complete, then shutdown services
|
||||
scsInterface := th.App.Srv().GetSharedChannelSyncService()
|
||||
if scsInterface != nil && scsInterface.Active() {
|
||||
// Cast to concrete type to access testing methods
|
||||
if service, ok := scsInterface.(*sharedchannel.Service); ok {
|
||||
// Wait for any pending tasks from previous tests to complete
|
||||
require.Eventually(t, func() bool {
|
||||
return !service.HasPendingTasksForTesting()
|
||||
}, 10*time.Second, 100*time.Millisecond, "All pending sync tasks should complete before cleanup")
|
||||
}
|
||||
|
||||
// Shutdown the shared channel service to stop any async operations
|
||||
_ = scsInterface.Shutdown()
|
||||
|
||||
// Wait for shutdown to complete with more time
|
||||
require.Eventually(t, func() bool {
|
||||
return !scsInterface.Active()
|
||||
}, 5*time.Second, 100*time.Millisecond, "Shared channel service should be inactive after shutdown")
|
||||
}
|
||||
|
||||
// Clear all shared channels and remotes from previous tests
|
||||
allSharedChannels, _ := ss.SharedChannel().GetAll(0, 1000, model.SharedChannelFilterOpts{})
|
||||
for _, sc := range allSharedChannels {
|
||||
// Delete all remotes for this channel
|
||||
remotes, _ := ss.SharedChannel().GetRemotes(0, 100, model.SharedChannelRemoteFilterOpts{ChannelId: sc.ChannelId})
|
||||
remotes, _ := ss.SharedChannel().GetRemotes(0, 999999, model.SharedChannelRemoteFilterOpts{ChannelId: sc.ChannelId})
|
||||
for _, remote := range remotes {
|
||||
_, _ = ss.SharedChannel().DeleteRemote(remote.Id)
|
||||
}
|
||||
@@ -170,13 +199,32 @@ func EnsureCleanState(t *testing.T, th *TestHelper, ss store.Store) {
|
||||
_, _ = ss.RemoteCluster().Delete(rc.RemoteId)
|
||||
}
|
||||
|
||||
// Clear all SharedChannelUsers sync state - this is critical for test isolation
|
||||
// The SharedChannelUsers table tracks per-user sync timestamps that can interfere between tests
|
||||
_, _ = th.SQLStore.GetMaster().Exec("DELETE FROM SharedChannelUsers WHERE 1=1")
|
||||
|
||||
// Clear all SharedChannelAttachments sync state
|
||||
_, _ = th.SQLStore.GetMaster().Exec("DELETE FROM SharedChannelAttachments WHERE 1=1")
|
||||
|
||||
// Reset sync cursors in any remaining SharedChannelRemotes (before they get deleted)
|
||||
// This ensures cursors don't persist if deletion fails
|
||||
_, _ = th.SQLStore.GetMaster().Exec(`UPDATE SharedChannelRemotes SET
|
||||
LastPostCreateAt = 0,
|
||||
LastPostCreateId = '',
|
||||
LastPostUpdateAt = 0,
|
||||
LastPostId = '',
|
||||
LastMembersSyncAt = 0
|
||||
WHERE 1=1`)
|
||||
|
||||
// Remove all channel members from test channels (except the basic team/channel setup)
|
||||
channels, _ := ss.Channel().GetAll(th.BasicTeam.Id)
|
||||
for _, channel := range channels {
|
||||
// Skip direct message and group channels, and skip the default channels
|
||||
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup &&
|
||||
channel.Id != th.BasicChannel.Id {
|
||||
members, _ := ss.Channel().GetMembers(channel.Id, 0, 10000)
|
||||
members, _ := ss.Channel().GetMembers(model.ChannelMembersGetOptions{
|
||||
ChannelID: channel.Id,
|
||||
})
|
||||
for _, member := range members {
|
||||
_ = ss.Channel().RemoveMember(th.Context, channel.Id, member.UserId)
|
||||
}
|
||||
@@ -228,12 +276,16 @@ func EnsureCleanState(t *testing.T, th *TestHelper, ss store.Store) {
|
||||
cfg.ConnectedWorkspacesSettings.GlobalUserSyncBatchSize = &defaultBatchSize
|
||||
})
|
||||
|
||||
// Ensure services are running and ready
|
||||
scsInterface := th.App.Srv().GetSharedChannelSyncService()
|
||||
if scs, ok := scsInterface.(*sharedchannel.Service); ok {
|
||||
require.Eventually(t, func() bool {
|
||||
return scs.Active()
|
||||
}, 2*time.Second, 100*time.Millisecond, "Shared channel service should be active")
|
||||
// Restart services and ensure they are running and ready
|
||||
if scsInterface != nil {
|
||||
// Restart the shared channel service
|
||||
_ = scsInterface.Start()
|
||||
|
||||
if scs, ok := scsInterface.(*sharedchannel.Service); ok {
|
||||
require.Eventually(t, func() bool {
|
||||
return scs.Active()
|
||||
}, 5*time.Second, 100*time.Millisecond, "Shared channel service should be active after restart")
|
||||
}
|
||||
}
|
||||
|
||||
rcService := th.App.Srv().GetRemoteClusterService()
|
||||
@@ -243,6 +295,6 @@ func EnsureCleanState(t *testing.T, th *TestHelper, ss store.Store) {
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
return rcService.Active()
|
||||
}, 2*time.Second, 100*time.Millisecond, "Remote cluster service should be active")
|
||||
}, 5*time.Second, 100*time.Millisecond, "Remote cluster service should be active")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ func setupSharedChannels(tb testing.TB) *TestHelper {
|
||||
return SetupConfig(tb, func(cfg *model.Config) {
|
||||
*cfg.ConnectedWorkspacesSettings.EnableRemoteClusterService = true
|
||||
*cfg.ConnectedWorkspacesSettings.EnableSharedChannels = true
|
||||
cfg.FeatureFlags.EnableSharedChannelsMemberSync = true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user