diff --git a/server/channels/app/export_test.go b/server/channels/app/export_test.go index 5637be7dab8..241d166a838 100644 --- a/server/channels/app/export_test.go +++ b/server/channels/app/export_test.go @@ -1586,3 +1586,152 @@ func TestExportSchemes(t *testing.T) { require.Equal(t, customTeamGuestRole.BuiltIn, importedTeamGuestRole.BuiltIn) }) } + +// TestExportDeactivatedUserDMs specifically tests the MM-43598 bug +// by validating that direct messages from deactivated users are exported correctly +func TestExportDeactivatedUserDMs(t *testing.T) { + th1 := Setup(t).InitBasic() + defer th1.TearDown() + + // Create a DM Channel + user2 := th1.BasicUser2 + dmChannel := th1.CreateDmChannel(user2) + + // 1. First basic user (active) sends a message to user2 (who will later be deactivated) + initialMessage := "initial_message_from_basic_user" + initialPost := &model.Post{ + ChannelId: dmChannel.Id, + Message: initialMessage, + UserId: th1.BasicUser.Id, + } + initialPostCreated, appErr := th1.App.CreatePost(th1.Context, initialPost, dmChannel, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + // 2. Have user2 reply with TWO types of replies: + + // 2a. User2 replies in a thread (to the initial message) + threadedReplyMessage := "threaded_reply_from_user2" + threadedReply := &model.Post{ + ChannelId: dmChannel.Id, + Message: threadedReplyMessage, + UserId: user2.Id, + RootId: initialPostCreated.Id, // This makes it a threaded reply + } + _, appErr = th1.App.CreatePost(th1.Context, threadedReply, dmChannel, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + // 2b. User2 sends a standalone reply (NOT in a thread) + nonThreadedReplyMessage := "non_threaded_reply_from_user2" + nonThreadedReply := &model.Post{ + ChannelId: dmChannel.Id, + Message: nonThreadedReplyMessage, + UserId: user2.Id, + // No RootId, making it a standalone message, not a thread reply + } + _, appErr = th1.App.CreatePost(th1.Context, nonThreadedReply, dmChannel, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + // 3. Now deactivate user2 + _, err := th1.App.UpdateActive(th1.Context, user2, false) + require.Nil(t, err) + + // 4. Export data + var b bytes.Buffer + appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) + require.Nil(t, appErr) + + // 5. Make a copy of the buffer for export validation + var exportDataCopy bytes.Buffer + _, nErr := exportDataCopy.Write(b.Bytes()) + require.NoError(t, nErr) + + // 6. Validate export data directly to ensure both types of replies are present + scanner := bufio.NewScanner(&exportDataCopy) + foundThreadedReply := false + foundNonThreadedReply := false + + for scanner.Scan() { + var line imports.LineImportData + err := json.Unmarshal(scanner.Bytes(), &line) + if err != nil { + continue + } + + // Check for direct posts + if line.Type == "direct_post" && line.DirectPost != nil { + // Check for the non-threaded reply (the standalone message) + if line.DirectPost.Message != nil && *line.DirectPost.Message == nonThreadedReplyMessage { + foundNonThreadedReply = true + // Verify username is correctly preserved in export + require.Equal(t, user2.Username, *line.DirectPost.User, + "Deactivated user's username should be preserved in export for non-threaded reply") + } + + // Check for the thread starter and its replies + if line.DirectPost.Message != nil && *line.DirectPost.Message == initialMessage { + // Check if the threaded reply is in the replies array + if line.DirectPost.Replies != nil { + for _, reply := range *line.DirectPost.Replies { + if reply.Message != nil && *reply.Message == threadedReplyMessage { + foundThreadedReply = true + require.Equal(t, user2.Username, *reply.User, + "Deactivated user's username should be preserved in export for threaded reply") + } + } + } + } + } + } + + // This is key for testing MM-43598 + require.True(t, foundNonThreadedReply, + "Non-threaded reply from deactivated user must be present in export data") + require.True(t, foundThreadedReply, + "Threaded reply from deactivated user must be present in export data") + + // 7. Import data into a new instance + th2 := Setup(t) + defer th2.TearDown() + + i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 5) + require.Nil(t, appErr) + require.Equal(t, 0, i) + + // 8. Verify the DM channel was imported + channels, nErr := th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) + require.NoError(t, nErr) + require.Equal(t, 1, len(channels), "Direct channel should be imported") + + // 9. Verify all posts were imported + posts, nErr := th2.App.Srv().Store().Post().GetPosts(model.GetPostsOptions{ + ChannelId: channels[0].Id, + PerPage: 1000, + }, false, nil) + require.NoError(t, nErr) + + // We should have exactly 3 posts + require.Equal(t, 3, len(posts.Posts), "Should have imported exactly 3 posts") + + // 10. Specifically check that both types of replies are present + foundThreadedReplyInImport := false + foundNonThreadedReplyInImport := false + + for _, post := range posts.Posts { + if post.Message == threadedReplyMessage { + foundThreadedReplyInImport = true + // Verify this is a reply in a thread + require.NotEmpty(t, post.RootId, "Threaded reply should have a RootId") + } + if post.Message == nonThreadedReplyMessage { + foundNonThreadedReplyInImport = true + // Verify this is NOT a reply in a thread + require.Empty(t, post.RootId, "Non-threaded reply should not have a RootId") + } + } + + // This directly tests the issue in MM-43598 + require.True(t, foundNonThreadedReplyInImport, + "Non-threaded reply from deactivated user should be imported") + require.True(t, foundThreadedReplyInImport, + "Threaded reply from deactivated user should be imported") +}