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