MM-45009: Delete ThreadMemberships from "left" channels (#22559)

* MM-50550: Filter out threads from "left" channels v2

Currently leaving a channel doesn't affect the thread memberships of
that user/channel combination.
This PR aims to filter out all threads from those channels for the user.

Adds a DeleteAt column in the ThreadMemberships table, and filter out
all thread memberships that are "deleted".
Each time a user leaves a channel all thread memberships are going to be
marked as deleted, and when a user joins a channel again all those
existing thread memberships will be re-instantiated.

Adds a migration to mark all existing thread memberships as deleted
depending on whether there exists a channel membership for that
channel/user.

* Added migration files into list

* Fixes tests

* Fixes case where DeleteAt would be null

* Guard thread API endpoints with appropriate perms

* Deletes ThreadMembership rows upon leaving channel

* Minor style changes

* Use NoTranslation error

* Refactors tests

* Adds API tests to assert permissions on Team

* Adds tests, and fixes migrations

* Fixes test description

* Fix test

* Removes check on DM/GMs

* Change the MySQL query in the migration

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Kyriakos Z
2023-04-19 15:20:34 +03:00
коммит произвёл GitHub
родитель c34a50a6c7
Коммит a24111f9bd
16 изменённых файлов: 426 добавлений и 38 удалений

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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)
})
}