Offset-based pagination advanced after each page even though deletions shift remaining posts, so large cleanups silently left messages behind. Also page through all auto-cleanup KV keys and log individual delete failures. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
6 KiB
Go
192 lines
6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockPostService struct {
|
|
postsByChannel map[string][]*model.Post
|
|
deleted []string
|
|
failDeleteIDs map[string]bool
|
|
}
|
|
|
|
func newMockPostService() *mockPostService {
|
|
return &mockPostService{
|
|
postsByChannel: make(map[string][]*model.Post),
|
|
failDeleteIDs: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
func (m *mockPostService) addPost(channelID, postID string, createAt int64) {
|
|
m.postsByChannel[channelID] = append(m.postsByChannel[channelID], &model.Post{
|
|
Id: postID,
|
|
ChannelId: channelID,
|
|
CreateAt: createAt,
|
|
})
|
|
}
|
|
|
|
func (m *mockPostService) GetPostsForChannel(channelID string, page, perPage int) (*model.PostList, error) {
|
|
posts := m.postsByChannel[channelID]
|
|
start := page * perPage
|
|
if start >= len(posts) {
|
|
return &model.PostList{Order: []string{}, Posts: map[string]*model.Post{}}, nil
|
|
}
|
|
|
|
end := min(start+perPage, len(posts))
|
|
|
|
pagePosts := posts[start:end]
|
|
order := make([]string, len(pagePosts))
|
|
postMap := make(map[string]*model.Post, len(pagePosts))
|
|
for i, post := range pagePosts {
|
|
order[i] = post.Id
|
|
postMap[post.Id] = post
|
|
}
|
|
|
|
return &model.PostList{Order: order, Posts: postMap}, nil
|
|
}
|
|
|
|
func (m *mockPostService) DeletePost(postID string) error {
|
|
if m.failDeleteIDs[postID] {
|
|
return fmt.Errorf("delete failed for %s", postID)
|
|
}
|
|
|
|
for channelID, posts := range m.postsByChannel {
|
|
for i, post := range posts {
|
|
if post.Id == postID {
|
|
m.deleted = append(m.deleted, postID)
|
|
m.postsByChannel[channelID] = append(posts[:i], posts[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type mockErrorLogger struct {
|
|
messages []string
|
|
}
|
|
|
|
func (l *mockErrorLogger) Error(message string, _ ...any) {
|
|
l.messages = append(l.messages, message)
|
|
}
|
|
|
|
func TestDeleteChannelPostsDeletesAllWhenNoCutoff(t *testing.T) {
|
|
postSvc := newMockPostService()
|
|
postSvc.addPost("channel-a", "post-1", time.Now().UnixMilli())
|
|
postSvc.addPost("channel-a", "post-2", time.Now().Add(-48*time.Hour).UnixMilli())
|
|
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", 0, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, deleted)
|
|
require.ElementsMatch(t, []string{"post-1", "post-2"}, postSvc.deleted)
|
|
}
|
|
|
|
func TestDeleteChannelPostsRespectsAgeThreshold(t *testing.T) {
|
|
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
|
cutoff := cutoffMillis(30, now)
|
|
|
|
postSvc := newMockPostService()
|
|
postSvc.addPost("channel-a", "old-post", now.Add(-31*24*time.Hour).UnixMilli())
|
|
postSvc.addPost("channel-a", "recent-post", now.Add(-5*24*time.Hour).UnixMilli())
|
|
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", cutoff, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, deleted)
|
|
require.Equal(t, []string{"old-post"}, postSvc.deleted)
|
|
}
|
|
|
|
func TestDeleteChannelPostsDoesNotTouchOtherChannels(t *testing.T) {
|
|
postSvc := newMockPostService()
|
|
postSvc.addPost("channel-a", "a-post", time.Now().Add(-60*24*time.Hour).UnixMilli())
|
|
postSvc.addPost("channel-b", "b-post", time.Now().Add(-60*24*time.Hour).UnixMilli())
|
|
|
|
cutoff := cutoffMillis(30, time.Now())
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", cutoff, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, deleted)
|
|
require.Equal(t, []string{"a-post"}, postSvc.deleted)
|
|
require.Len(t, postSvc.postsByChannel["channel-b"], 1)
|
|
}
|
|
|
|
func TestDeleteChannelPostsDeletesNothingWhenAllRecent(t *testing.T) {
|
|
now := time.Now()
|
|
cutoff := cutoffMillis(30, now)
|
|
|
|
postSvc := newMockPostService()
|
|
postSvc.addPost("channel-a", "recent-1", now.Add(-1*24*time.Hour).UnixMilli())
|
|
postSvc.addPost("channel-a", "recent-2", now.Add(-2*24*time.Hour).UnixMilli())
|
|
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", cutoff, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, deleted)
|
|
require.Empty(t, postSvc.deleted)
|
|
}
|
|
|
|
func TestDeleteChannelPostsEmptyChannel(t *testing.T) {
|
|
postSvc := newMockPostService()
|
|
|
|
deleted, err := deleteChannelPosts(postSvc, "empty-channel", cutoffMillis(30, time.Now()), nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, deleted)
|
|
}
|
|
|
|
func TestDeleteChannelPostsDeletesAcrossMultiplePages(t *testing.T) {
|
|
postSvc := newMockPostService()
|
|
total := postsPerPage + 50
|
|
for i := range total {
|
|
postSvc.addPost("channel-a", fmt.Sprintf("post-%d", i), time.Now().UnixMilli())
|
|
}
|
|
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", 0, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, total, deleted)
|
|
require.Len(t, postSvc.deleted, total)
|
|
require.Empty(t, postSvc.postsByChannel["channel-a"])
|
|
}
|
|
|
|
func TestDeleteChannelPostsAgeThresholdAcrossPagesWithRecentPosts(t *testing.T) {
|
|
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
|
cutoff := cutoffMillis(30, now)
|
|
|
|
postSvc := newMockPostService()
|
|
// Newest-first style: recent posts first, then old ones beyond one page.
|
|
for i := range postsPerPage {
|
|
postSvc.addPost("channel-a", fmt.Sprintf("recent-%d", i), now.Add(-5*24*time.Hour).UnixMilli())
|
|
}
|
|
oldCount := 75
|
|
for i := range oldCount {
|
|
postSvc.addPost("channel-a", fmt.Sprintf("old-%d", i), now.Add(-60*24*time.Hour).UnixMilli())
|
|
}
|
|
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", cutoff, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, oldCount, deleted)
|
|
require.Len(t, postSvc.postsByChannel["channel-a"], postsPerPage)
|
|
}
|
|
|
|
func TestDeleteChannelPostsLogsDeleteFailures(t *testing.T) {
|
|
postSvc := newMockPostService()
|
|
postSvc.addPost("channel-a", "ok-post", time.Now().UnixMilli())
|
|
postSvc.addPost("channel-a", "bad-post", time.Now().UnixMilli())
|
|
postSvc.failDeleteIDs["bad-post"] = true
|
|
|
|
logger := &mockErrorLogger{}
|
|
deleted, err := deleteChannelPosts(postSvc, "channel-a", 0, logger)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, deleted)
|
|
require.Equal(t, []string{"ok-post"}, postSvc.deleted)
|
|
require.NotEmpty(t, logger.messages)
|
|
require.Contains(t, logger.messages[0], "Failed to delete post")
|
|
require.Len(t, postSvc.postsByChannel["channel-a"], 1)
|
|
}
|
|
|
|
func TestCutoffMillis(t *testing.T) {
|
|
now := time.Date(2026, 7, 28, 0, 0, 0, 0, time.UTC)
|
|
expected := time.Date(2026, 6, 28, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
require.Equal(t, expected, cutoffMillis(30, now))
|
|
}
|