[MM-30831] granular data retention wireup (#17417)

* pre-checkout commit

* add API endpoints for retention policies

* allow deleting multiple teams/channels from a policy in a single request

* pre-checkout commit

* add auditing in API functions

* add permission checks

* update the store layers

* update storetest

* add check constraint on PostDuration column

* pre-checkout commit

* add query to delete posts under the scope of a granular retention policy

* add suggestions from sbishel

* allow clients to specify channels/teams when creating a new policy

* remove foreign keys referencing Channels and Teams tables

* add checks for whether teams and channels exist

* pre-checkout commit

* remove data referencing the Posts table

* pre-checkout commit

* write data store tests

* sort results of buildGetPoliciesQuery

* add missing test cases for teams

* pre-checkout commit

* add Client4 methods for data retention policy endpoints

* add uint and uint64 to app/layer_generators

* make granular policies override global policies

* fix lint errors

* pre-checkout commit

* add license to top of files

* add tests for data store layer

* add missing test cases for store layer

* run make i18n-extract

* add query to delete ChannelMemberHistory

* work in progress

* add test for old reply to old post

* fix lint error

* use COALESCE on each Posts column

* begin implementing orphaned rows worker

* split PR

* pre-checkout commit

* use RetentionPolicyWithTeamAndChannelCounts

* update app and api layers

* run make i18n-extract

* add RetentionPolicy to retrylayer_test.go

* Revert "split PR"

This reverts commit b316f03dd307a30deae931944ca7e4a1cc904605.

* fix errors caused by revert

* add suggestions from sbishel

* fix copy-paste error

* fix lint errors

* pre-checkout commit

* add function to delete orphaned rows

* use -1 for infinite retention

* remove check constraint

* copy i18n entries from master

* re-run tests with newer enterprise branch

* add team data to channel list

* add search for channels and teams in a policy

* add store tests for channel and team search

* add suggestions from mkraft

* run make einterfaces-mocks

* fix lint errors

* add suggestions from mkraft

* move removeOrphanedRows method to wireup branch

* Revert "move removeOrphanedRows method to wireup branch"

This reverts commit 94605c9b4a5378ffa44a3dec4d3f8e3306b9d33e.

* use DeleteOrphanedRows where possible

* run make i18n-extract

* use COMPLIANCE permissions

* run make migrations-bindadta

* clean up teams before test

* fix tests for TestRetentionPolicyStore

* add API endpoints for mobile

* fix lint error

* fix some of the lint errors

* move user/data_retention endpoints to data_retention.go

* Revert "fix some of the lint errors"

This reverts commit b5b2dc27566c427187db942c5c0afe319e7679c4.

* add exclude_policy_constrained parameter for /channels and /teams

* fix lint errors

* add policy_id field to GET endpoints for channels and teams

* use PolicyWithTeamID in RetentionPolicy layer

* fix lint errors

* run make i18n-extract

* update mock call in telemetry_test.go

* return status:OK in JSON instead of 204

* pre-checkout commit

* add policy_id field on channels/teams

* fix lint errors

* use sq.Eq instead of '?'

* use new subsection permissions

* update channels and teams endpoints to use new subsection permissions

* add extra search opts for channels in a policy

* fix lint errors

* allow negative post duration in patch

* remove DELETE FROM query in retention policy tests

* use *int64 for PostDuration

* re-run CI tests

* use 3-step deletion strategy for each table

* fix lint errors

* run make store-layers

* re-run CI tests

* add test with channel, team and global policies

* use common function for SQL queries

* add pagination test

* use struct for args to common SQL function

* fix lint errors

* run make i18n-extract

* check if Channels.TeamId is "" or nil

* use three OR clauses

* write separate genericRetentionPoliciesDeletion function

* add config setting for BatchSize

* add telemetry for BatchSize

* use feature flag

* add old i18n messages back in

* re-run CI tests

* update call signature in storetest

* MM-30831: Adds constant for retention default batch size.

* MM-30831: Removes comment re: optimization.

* MM-30831: Converts days to milliseconds.

* MM-30831: Reverts change to test.

* Revert "MM-30831: Reverts change to test."

This reverts commit 6d14275a1ceae682bb9e17ec69b39252b44e0c0c.

* Revert "MM-30831: Converts days to milliseconds."

This reverts commit a0cb6ec09d854a05194c1daee1c5333f260231c3.

* MM-30831: Fixes tests.

* MM-30381: Fix for change to method sig.

Co-authored-by: Max Erenberg <max.erenberg@mattermost.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Martin Kraft <martin@upspin.org>
Этот коммит содержится в:
Max Erenberg
2021-06-23 07:55:12 -04:00
коммит произвёл GitHub
родитель 71ef1c5386
Коммит 58d5d51f7a
30 изменённых файлов: 1824 добавлений и 66 удалений

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

@@ -21,6 +21,7 @@ func TestChannelMemberHistoryStore(t *testing.T, ss store.Store) {
t.Run("TestGetUsersInChannelAtChannelMemberHistory", func(t *testing.T) { testGetUsersInChannelAtChannelMemberHistory(t, ss) })
t.Run("TestGetUsersInChannelAtChannelMembers", func(t *testing.T) { testGetUsersInChannelAtChannelMembers(t, ss) })
t.Run("TestPermanentDeleteBatch", func(t *testing.T) { testPermanentDeleteBatch(t, ss) })
t.Run("TestPermanentDeleteBatchForRetentionPolicies", func(t *testing.T) { testPermanentDeleteBatchForRetentionPolicies(t, ss) })
}
func testLogJoinEvent(t *testing.T, ss store.Store) {
@@ -199,7 +200,8 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
var tableDataTruncated = false
for !tableDataTruncated {
var count int64
count, err = ss.ChannelMemberHistory().PermanentDeleteBatch(model.GetMillis(), 1000)
count, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, model.GetMillis(), 1000, model.RetentionPolicyCursor{})
require.NoError(t, err, "Failed to truncate ChannelMemberHistory contents")
tableDataTruncated = count == int64(0)
}
@@ -333,7 +335,8 @@ func testPermanentDeleteBatch(t *testing.T, ss store.Store) {
assert.Len(t, channelMembers, 2)
// the permanent delete should delete at least one record
rowsDeleted, err := ss.ChannelMemberHistory().PermanentDeleteBatch(leaveTime, math.MaxInt64)
rowsDeleted, _, err := ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, leaveTime+1, math.MaxInt64, model.RetentionPolicyCursor{})
require.NoError(t, err)
assert.NotEqual(t, int64(0), rowsDeleted)
@@ -343,3 +346,46 @@ func testPermanentDeleteBatch(t *testing.T, ss store.Store) {
assert.Len(t, channelMembers, 1)
assert.Equal(t, user2.Id, channelMembers[0].UserId)
}
func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TEAM_OPEN,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.CHANNEL_OPEN,
}, -1)
require.NoError(t, err)
userID := model.NewId()
joinTime := int64(1000)
leaveTime := int64(1500)
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, leaveTime)
require.NoError(t, err)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := leaveTime + *channelPolicy.PostDuration*24*60*60*1000 + 1
_, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id)
require.NoError(t, err)
require.Empty(t, result, "history should have been deleted by channel policy")
}

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

@@ -1757,7 +1757,8 @@ func testChannelMembersToAdd(t *testing.T, ss store.Store) {
require.Empty(t, channelMembers)
// Purging ChannelMemberHistory re-returns the result
_, nErr = ss.ChannelMemberHistory().PermanentDeleteBatch(model.GetMillis()+1, 100)
_, _, nErr = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, model.GetMillis()+1, 100, model.RetentionPolicyCursor{})
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)

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

@@ -14,6 +14,27 @@ type ChannelMemberHistoryStore struct {
mock.Mock
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersInChannelDuring provides a mock function with given fields: startTime, endTime, channelID
func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
ret := _m.Called(startTime, endTime, channelID)
@@ -85,3 +106,31 @@ func (_m *ChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit i
return r0, r1
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}

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

@@ -102,6 +102,27 @@ func (_m *PostStore) Delete(postID string, time int64, deleteByID string) error
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PostStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, id, skipFetchThreads, collapsedThreads, collapsedThreadsExtended, userID
func (_m *PostStore) Get(ctx context.Context, id string, skipFetchThreads bool, collapsedThreads bool, collapsedThreadsExtended bool, userID string) (*model.PostList, error) {
ret := _m.Called(ctx, id, skipFetchThreads, collapsedThreads, collapsedThreadsExtended, userID)
@@ -714,6 +735,34 @@ func (_m *PostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, er
return r0, r1
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *PostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteByChannel provides a mock function with given fields: channelID
func (_m *PostStore) PermanentDeleteByChannel(channelID string) error {
ret := _m.Called(channelID)

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

@@ -77,6 +77,27 @@ func (_m *PreferenceStore) DeleteCategoryAndName(category string, name string) e
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: userID, category, name
func (_m *PreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
ret := _m.Called(userID, category, name)

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

@@ -74,6 +74,27 @@ func (_m *ReactionStore) DeleteAllWithEmojiName(emojiName string) error {
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID, allowFromCache
func (_m *ReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
ret := _m.Called(postID, allowFromCache)

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

@@ -56,6 +56,27 @@ func (_m *RetentionPolicyStore) Delete(id string) error {
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *RetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *RetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(id)

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

@@ -66,6 +66,27 @@ func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) err
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *ThreadStore) Get(id string) (*model.Thread, error) {
ret := _m.Called(id)
@@ -292,6 +313,62 @@ func (_m *ThreadStore) MarkAsRead(userID string, threadID string, timestamp int6
return r0
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteBatchThreadMembershipsForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Save provides a mock function with given fields: thread
func (_m *ThreadStore) Save(thread *model.Thread) (*model.Thread, error) {
ret := _m.Called(thread)

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

@@ -2586,16 +2586,31 @@ func testPostStoreGetPostsBatchForIndexing(t *testing.T, ss store.Store) {
}
func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TEAM_OPEN,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.CHANNEL_OPEN,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = model.NewId()
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = "zz" + model.NewId() + "AAAAAAAAAAA"
o1.CreateAt = 1000
o1, err := ss.Post().Save(o1)
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = model.NewId()
o2.ChannelId = channel.Id
o2.UserId = model.NewId()
o2.Message = "zz" + model.NewId() + "AAAAAAAAAAA"
o2.CreateAt = 1000
@@ -2603,14 +2618,14 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = model.NewId()
o3.ChannelId = channel.Id
o3.UserId = model.NewId()
o3.Message = "zz" + model.NewId() + "AAAAAAAAAAA"
o3.CreateAt = 100000
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
_, err = ss.Post().PermanentDeleteBatch(2000, 1000)
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Post().Get(context.Background(), o1.Id, false, false, false, "")
@@ -2620,7 +2635,160 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
require.Error(t, err, "Should have not found post 2 after purge")
_, err = ss.Post().Get(context.Background(), o3.Id, false, false, false, "")
require.NoError(t, err, "Should have not found post 3 after purge")
require.NoError(t, err, "Should have found post 3 after purge")
t.Run("with pagination", func(t *testing.T) {
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err)
}
cursor := model.RetentionPolicyCursor{}
deleted, cursor, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err)
require.Equal(t, int64(2), deleted)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err)
require.Equal(t, int64(1), deleted)
})
t.Run("with data retention policies", func(t *testing.T) {
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err2)
post := &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
}
post, err2 = ss.Post().Save(post)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, false, false, false, "")
require.NoError(t, err2, "global policy should have been ignored due to granular policy")
nowMillis := post.CreateAt + *channelPolicy.PostDuration*24*60*60*1000 + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, false, false, false, "")
require.Error(t, err2, "post should have been deleted by channel policy")
// Create a team policy which is stricter than the channel policy
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
post.Id = ""
post, err2 = ss.Post().Save(post)
require.NoError(t, err2)
nowMillis = post.CreateAt + *teamPolicy.PostDuration*24*60*60*1000 + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, false, false, false, "")
require.NoError(t, err2, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err2 = ss.RetentionPolicy().RemoveChannels(channelPolicy.ID, []string{channel.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, false, false, false, "")
require.Error(t, err2, "post should have been deleted by team policy")
err2 = ss.RetentionPolicy().RemoveTeams(teamPolicy.ID, []string{team.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err2)
})
t.Run("with channel, team and global policies", func(t *testing.T) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = "zz" + model.NewId() + "b"
c1.Type = model.CHANNEL_OPEN
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = "zz" + model.NewId() + "b"
c2.Type = model.CHANNEL_OPEN
c2, _ = ss.Channel().Save(c2, -1)
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(30),
},
ChannelIDs: []string{c1.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(channelPolicy.ID)
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(30),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(teamPolicy.ID)
// This one should be deleted by the channel policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: c1.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the team policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the global policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: c2.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
nowMillis := int64(1 + 30*24*60*60*1000 + 1)
deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
require.Equal(t, int64(3), deleted)
})
}
func testPostStoreGetOldest(t *testing.T, ss store.Store) {

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

@@ -22,7 +22,7 @@ func TestPreferenceStore(t *testing.T, ss store.Store) {
t.Run("PreferenceDelete", func(t *testing.T) { testPreferenceDelete(t, ss) })
t.Run("PreferenceDeleteCategory", func(t *testing.T) { testPreferenceDeleteCategory(t, ss) })
t.Run("PreferenceDeleteCategoryAndName", func(t *testing.T) { testPreferenceDeleteCategoryAndName(t, ss) })
t.Run("PreferenceCleanupFlagsBatch", func(t *testing.T) { testPreferenceCleanupFlagsBatch(t, ss) })
t.Run("PreferenceDeleteOrphanedRows", func(t *testing.T) { testPreferenceDeleteOrphanedRows(t, ss) })
}
func testPreferenceSave(t *testing.T, ss store.Store) {
@@ -330,44 +330,66 @@ func testPreferenceDeleteCategoryAndName(t *testing.T, ss store.Store) {
assert.Empty(t, preferences, "should've returned no preference")
}
func testPreferenceCleanupFlagsBatch(t *testing.T, ss store.Store) {
func testPreferenceDeleteOrphanedRows(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TEAM_OPEN,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.CHANNEL_OPEN,
}, -1)
require.NoError(t, err)
category := model.PREFERENCE_CATEGORY_FLAGGED_POST
userId := model.NewId()
o1 := &model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = userId
o1.Message = "zz" + model.NewId() + "AAAAAAAAAAA"
o1.CreateAt = 1000
o1, err := ss.Post().Save(o1)
olderPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
CreateAt: 1000,
})
require.NoError(t, err)
newerPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
CreateAt: 3000,
})
require.NoError(t, err)
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: o1.Id,
Name: olderPost.Id,
Value: "true",
}
preference2 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Name: newerPost.Id,
Value: "true",
}
nErr := ss.Preference().Save(&model.Preferences{preference1, preference2})
require.NoError(t, nErr)
_, nErr = ss.Preference().CleanupFlagsBatch(-1)
require.Error(t, nErr)
_, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
assert.NoError(t, nErr)
_, nErr = ss.Preference().CleanupFlagsBatch(10000)
_, nErr = ss.Preference().DeleteOrphanedRows(limit)
assert.NoError(t, nErr)
_, nErr = ss.Preference().Get(userId, category, preference1.Name)
assert.NoError(t, nErr)
assert.Error(t, nErr, "older preference should have been deleted")
_, nErr = ss.Preference().Get(userId, category, preference2.Name)
assert.Error(t, nErr)
assert.NoError(t, nErr, "newer preference should not have been deleted")
}

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

@@ -522,61 +522,72 @@ func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore
}
func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
post, err1 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TEAM_OPEN,
})
require.NoError(t, err1)
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.CHANNEL_OPEN,
}, -1)
require.NoError(t, err)
olderPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
CreateAt: 1000,
})
require.NoError(t, err)
newerPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
CreateAt: 3000,
})
require.NoError(t, err)
// Reactions will be deleted based on the timestamp of their post. So the time at
// which a reaction was created doesn't matter.
reactions := []*model.Reaction{
{
UserId: model.NewId(),
PostId: post.Id,
PostId: olderPost.Id,
EmojiName: "sad",
CreateAt: 1000,
},
{
UserId: model.NewId(),
PostId: post.Id,
PostId: olderPost.Id,
EmojiName: "sad",
CreateAt: 1500,
},
{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: "sad",
CreateAt: 2000,
},
{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: "sad",
CreateAt: 2000,
PostId: newerPost.Id,
EmojiName: "smile",
},
}
// Need to hang on to a reaction to delete later in order to clear the cache, as "allowFromCache" isn't honoured any more.
var lastReaction *model.Reaction
for _, reaction := range reactions {
var nErr error
lastReaction, nErr = ss.Reaction().Save(reaction)
require.NoError(t, nErr)
_, err = ss.Reaction().Save(reaction)
require.NoError(t, err)
}
returned, err := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, err)
require.Len(t, returned, 4, "expected 4 reactions")
_, err = ss.Reaction().PermanentDeleteBatch(1800, 1000)
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
// This is to force a clear of the cache.
_, err = ss.Reaction().Delete(lastReaction)
_, err = ss.Reaction().DeleteOrphanedRows(limit)
require.NoError(t, err)
returned, err = ss.Reaction().GetForPost(post.Id, false)
returned, err := ss.Reaction().GetForPost(olderPost.Id, false)
require.NoError(t, err)
require.Len(t, returned, 1, "expected 1 reaction. Got: %v", len(returned))
require.Len(t, returned, 0, "reactions for older post should have been deleted")
returned, err = ss.Reaction().GetForPost(newerPost.Id, false)
require.NoError(t, err)
require.Len(t, returned, 1, "reactions for newer post should not have been deleted")
}
func testReactionBulkGetForPosts(t *testing.T, ss store.Store) {

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

@@ -25,6 +25,7 @@ func TestRetentionPolicyStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetTeams", func(t *testing.T) { testRetentionPolicyStoreGetTeams(t, ss, s) })
t.Run("AddTeams", func(t *testing.T) { testRetentionPolicyStoreAddTeams(t, ss, s) })
t.Run("RemoveTeams", func(t *testing.T) { testRetentionPolicyStoreRemoveTeams(t, ss, s) })
t.Run("RemoveOrphanedRows", func(t *testing.T) { testRetentionPolicyStoreRemoveOrphanedRows(t, ss, s) })
t.Run("GetPoliciesForUser", func(t *testing.T) { testRetentionPolicyStoreGetPoliciesForUser(t, ss, s) })
}
@@ -656,3 +657,21 @@ func testRetentionPolicyStoreGetPoliciesForUser(t *testing.T, ss store.Store, s
require.Equal(t, int64(len(channelIDs)), count)
})
}
func testRetentionPolicyStoreRemoveOrphanedRows(t *testing.T, ss store.Store, s SqlStore) {
teamID := createTeamsForRetentionPolicy(t, ss, 1)[0]
channelID := createChannelsForRetentionPolicy(t, ss, teamID, 1)[0]
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1",
[]string{teamID}, []string{channelID})
err := ss.Channel().PermanentDelete(channelID)
require.NoError(t, err)
err = ss.Team().PermanentDelete(teamID)
require.NoError(t, err)
_, err = ss.RetentionPolicy().DeleteOrphanedRows(1000)
require.NoError(t, err)
policy.ChannelIDs = make([]string, 0)
policy.TeamIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, policy)
}

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

@@ -17,6 +17,12 @@ import (
func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("ThreadStorePopulation", func(t *testing.T) { testThreadStorePopulation(t, ss) })
t.Run("ThreadStorePermanentDeleteBatchForRetentionPolicies", func(t *testing.T) {
testThreadStorePermanentDeleteBatchForRetentionPolicies(t, ss)
})
t.Run("ThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies", func(t *testing.T) {
testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t, ss)
})
}
func testThreadStorePopulation(t *testing.T, ss store.Store) {
@@ -411,3 +417,185 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) {
require.Equal(t, int64(0), th.UnreadReplies)
})
}
func threadStoreCreateReply(t *testing.T, ss store.Store, channelID, postID string, createAt int64) *model.Post {
reply, err := ss.Post().Save(&model.Post{
ChannelId: channelID,
UserId: model.NewId(),
CreateAt: createAt,
RootId: postID,
ParentId: postID,
})
require.NoError(t, err)
return reply
}
func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TEAM_OPEN,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.CHANNEL_OPEN,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post.Id, 2000)
thread, err := ss.Thread().Get(post.Id)
require.NoError(t, err)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := thread.LastReplyAt + *channelPolicy.PostDuration*24*60*60*1000 + 1
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().Get(post.Id)
require.Error(t, err, "thread should have been deleted by channel policy")
// create a new thread
threadStoreCreateReply(t, ss, channel.Id, post.Id, 2000)
thread, err = ss.Thread().Get(post.Id)
require.NoError(t, err)
// Create a team policy which is stricter than the channel policy
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err)
nowMillis = thread.LastReplyAt + *teamPolicy.PostDuration*24*60*60*1000 + 1
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().Get(post.Id)
require.NoError(t, err, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err)
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().Get(post.Id)
require.Error(t, err, "thread should have been deleted by team policy")
}
func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
userID := model.NewId()
createThreadMembership := func(userID, postID string) *model.ThreadMembership {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
threadMembership, err := ss.Thread().GetMembershipForUser(userID, postID)
require.NoError(t, err)
return threadMembership
}
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TEAM_OPEN,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.CHANNEL_OPEN,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post.Id, 2000)
threadMembership := createThreadMembership(userID, post.Id)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := threadMembership.LastUpdated + *channelPolicy.PostDuration*24*60*60*1000 + 1
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted by channel policy")
// create a new thread membership
threadMembership = createThreadMembership(userID, post.Id)
// Create a team policy which is stricter than the channel policy
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDuration: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err)
nowMillis = threadMembership.LastUpdated + *teamPolicy.PostDuration*24*60*60*1000 + 1
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.NoError(t, err, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err)
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted by team policy")
// create a new thread membership
threadMembership = createThreadMembership(userID, post.Id)
// Delete team policy and thread
err = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err)
err = ss.Thread().Delete(post.Id)
require.NoError(t, err)
deleted, err := ss.Thread().DeleteOrphanedRows(1000)
require.NoError(t, err)
require.NotZero(t, deleted)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted because thread no longer exists")
}