diff --git a/api4/channel_test.go b/api4/channel_test.go index 893c69bfcc..77ebcf93d1 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -10,6 +10,7 @@ import ( "sort" "strconv" "strings" + "sync" "testing" "time" @@ -2502,15 +2503,79 @@ func TestRemoveChannelMember(t *testing.T) { _, resp = Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser.Id) CheckForbiddenStatus(t, resp) - th.App.AddUserToChannel(th.BasicUser2, th.BasicChannel) - _, resp = Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser2.Id) - CheckNoError(t, resp) + t.Run("success", func(t *testing.T) { + // Setup the system administrator to listen for websocket events from the channels. + th.LinkUserToTeam(th.SystemAdminUser, th.BasicTeam) + _, err := th.App.AddUserToChannel(th.SystemAdminUser, th.BasicChannel) + _, err = th.App.AddUserToChannel(th.SystemAdminUser, th.BasicChannel2) + require.Nil(t, err) + props := map[string]string{} + props[model.DESKTOP_NOTIFY_PROP] = model.CHANNEL_NOTIFY_ALL + _, resp = th.SystemAdminClient.UpdateChannelNotifyProps(th.BasicChannel.Id, th.SystemAdminUser.Id, props) + _, resp = th.SystemAdminClient.UpdateChannelNotifyProps(th.BasicChannel2.Id, th.SystemAdminUser.Id, props) + CheckNoError(t, resp) - _, resp = Client.RemoveUserFromChannel(th.BasicChannel2.Id, th.BasicUser.Id) - CheckNoError(t, resp) + wsClient, err := th.CreateWebSocketSystemAdminClient() + require.Nil(t, err) + wsClient.Listen() + var closeWsClient sync.Once + defer closeWsClient.Do(func() { + wsClient.Close() + }) - _, resp = th.SystemAdminClient.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser.Id) - CheckNoError(t, resp) + wsr := <-wsClient.EventChannel + require.Equal(t, wsr.Event, model.WEBSOCKET_EVENT_HELLO) + + // requirePost listens for websocket events and tries to find the post matching + // the expected post's channel and message. + requirePost := func(expectedPost *model.Post) { + t.Helper() + for { + select { + case event := <-wsClient.EventChannel: + postData, ok := event.Data["post"] + if !ok { + continue + } + + post := model.PostFromJson(strings.NewReader(postData.(string))) + if post.ChannelId == expectedPost.ChannelId && post.Message == expectedPost.Message { + return + } + case <-time.After(5 * time.Second): + t.Fatal("failed to find expected post after 5 seconds") + return + } + } + } + + th.App.AddUserToChannel(th.BasicUser2, th.BasicChannel) + _, resp = Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser2.Id) + CheckNoError(t, resp) + + requirePost(&model.Post{ + Message: fmt.Sprintf("@%s left the channel.", th.BasicUser2.Username), + ChannelId: th.BasicChannel.Id, + }) + + _, resp = Client.RemoveUserFromChannel(th.BasicChannel2.Id, th.BasicUser.Id) + CheckNoError(t, resp) + requirePost(&model.Post{ + Message: fmt.Sprintf("@%s removed from the channel.", th.BasicUser.Username), + ChannelId: th.BasicChannel2.Id, + }) + + _, resp = th.SystemAdminClient.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser.Id) + CheckNoError(t, resp) + requirePost(&model.Post{ + Message: fmt.Sprintf("@%s removed from the channel.", th.BasicUser.Username), + ChannelId: th.BasicChannel.Id, + }) + + closeWsClient.Do(func() { + wsClient.Close() + }) + }) // Leave deleted channel th.LoginBasic() diff --git a/app/channel.go b/app/channel.go index 7021cd4192..aa0e54bbb8 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1525,9 +1525,12 @@ func (a *App) LeaveChannel(channelId string, userId string) *model.AppError { func (a *App) postLeaveChannelMessage(user *model.User, channel *model.Channel) *model.AppError { post := &model.Post{ ChannelId: channel.Id, - Message: fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), - Type: model.POST_LEAVE_CHANNEL, - UserId: user.Id, + // Message here embeds `@username`, not just `username`, to ensure that mentions + // treat this as a username mention even though the user has now left the channel. + // The client renders its own system message, ignoring this value altogether. + Message: fmt.Sprintf(utils.T("api.channel.leave.left"), fmt.Sprintf("@%s", user.Username)), + Type: model.POST_LEAVE_CHANNEL, + UserId: user.Id, Props: model.StringInterface{ "username": user.Username, }, @@ -1595,9 +1598,12 @@ func (a *App) postAddToTeamMessage(user *model.User, addedUser *model.User, chan func (a *App) postRemoveFromChannelMessage(removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError { post := &model.Post{ ChannelId: channel.Id, - Message: fmt.Sprintf(utils.T("api.channel.remove_member.removed"), removedUser.Username), - Type: model.POST_REMOVE_FROM_CHANNEL, - UserId: removerUserId, + // Message here embeds `@username`, not just `username`, to ensure that mentions + // treat this as a username mention even though the user has now left the channel. + // The client renders its own system message, ignoring this value altogether. + Message: fmt.Sprintf(utils.T("api.channel.remove_member.removed"), fmt.Sprintf("@%s", removedUser.Username)), + Type: model.POST_REMOVE_FROM_CHANNEL, + UserId: removerUserId, Props: model.StringInterface{ "removedUserId": removedUser.Id, "removedUsername": removedUser.Username,