mattermost-plugin-cleanup-c.../server/cleanup_test.go
Felipe M. a1e70da201
All checks were successful
ci / test (push) Successful in 4m53s
ci / lint (push) Successful in 5m19s
ci / build (push) Successful in 4m2s
Fix channel cleanup skipping posts across paginated deletes.
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>
2026-07-30 15:58:41 +02:00

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