[MM-61691] Deleting drafts when permanently deleting a user (#30233)

* deleting drafts on permanently deleting user

* verified count of drafts before deleting

* i18n fix

* use ExecBuilder

* changed error message to specify user

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
This commit is contained in:
Arya Khochare 2025-03-13 19:19:38 +05:30 committed by GitHub
parent e2a7240e34
commit 4fbfd84957
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 159 additions and 0 deletions

View file

@ -1832,6 +1832,10 @@ func (a *App) PermanentDeleteUser(rctx request.CTX, user *model.User) *model.App
return model.NewAppError("PermanentDeleteUser", "app.scheduled_post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Draft().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.drafts.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil {
var invErr *store.ErrInvalidInput
switch {

View file

@ -4209,6 +4209,27 @@ func (s *RetryLayerDraftStore) GetLastCreateAtAndUserIdValuesForEmptyDraftsMigra
}
func (s *RetryLayerDraftStore) PermanentDeleteByUser(userId string) error {
tries := 0
for {
err := s.DraftStore.PermanentDeleteByUser(userId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Upsert(d *model.Draft) (*model.Draft, error) {
tries := 0

View file

@ -181,6 +181,20 @@ func (s *SqlDraftStore) Delete(userID, channelID, rootID string) error {
return nil
}
func (s *SqlDraftStore) PermanentDeleteByUser(userID string) error {
query := s.getQueryBuilder().
Delete("Drafts").
Where(sq.Eq{
"UserId": userID,
})
if _, err := s.GetMaster().ExecBuilder(query); err != nil {
return errors.Wrapf(err, "PermanentDeleteByUser: failed to delete drafts for user: %s", userID)
}
return nil
}
// DeleteDraftsAssociatedWithPost deletes all drafts associated with a post.
func (s *SqlDraftStore) DeleteDraftsAssociatedWithPost(channelID, rootID string) error {
query := s.getQueryBuilder().

View file

@ -1039,6 +1039,7 @@ type DraftStore interface {
GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(createAt int64, userID string) (int64, string, error)
DeleteEmptyDraftsByCreateAtAndUserId(createAt int64, userID string) error
DeleteOrphanDraftsByCreateAtAndUserId(createAt int64, userID string) error
PermanentDeleteByUser(userId string) error
}
type PostAcknowledgementStore interface {

View file

@ -25,6 +25,7 @@ func TestDraftStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore)
t.Run("GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration", func(t *testing.T) { testGetLastCreateAtAndUserIDValuesForEmptyDraftsMigration(t, rctx, ss) })
t.Run("DeleteEmptyDraftsByCreateAtAndUserId", func(t *testing.T) { testDeleteEmptyDraftsByCreateAtAndUserID(t, rctx, ss) })
t.Run("DeleteOrphanDraftsByCreateAtAndUserId", func(t *testing.T) { testDeleteOrphanDraftsByCreateAtAndUserID(t, rctx, ss) })
t.Run("PermanentDeleteByUser", func(t *testing.T) { testPermanentDeleteDraftsByUser(t, rctx, ss) })
}
func testSaveDraft(t *testing.T, rctx request.CTX, ss store.Store) {
@ -218,6 +219,86 @@ func testDeleteDraft(t *testing.T, rctx request.CTX, ss store.Store) {
})
}
func testPermanentDeleteDraftsByUser(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should delete all drafts for a given user", func(t *testing.T) {
userId := model.NewId()
channel1Id := model.NewId()
channel2Id := model.NewId()
member1 := &model.ChannelMember{
ChannelId: channel1Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(rctx, member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(rctx, member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel1Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel2Id,
Message: "draft2",
}
_, err = ss.Draft().Upsert(draft1)
require.NoError(t, err)
_, err = ss.Draft().Upsert(draft2)
require.NoError(t, err)
draftsResp, err := ss.Draft().GetDraftsForUser(userId, "")
assert.NoError(t, err)
assert.Len(t, draftsResp, 2)
// Delete draft for the user
err = ss.Draft().PermanentDeleteByUser(userId)
assert.NoError(t, err)
// Verify that no drafts exist for the user
draftsResp, err = ss.Draft().GetDraftsForUser(userId, "")
assert.NoError(t, err)
assert.Len(t, draftsResp, 0)
})
t.Run("should not fail if no drafts exist for the user", func(t *testing.T) {
userId := model.NewId()
// Attempt to delete drafts for a user with no drafts
err := ss.Draft().PermanentDeleteByUser(userId)
assert.NoError(t, err)
})
t.Run("should handle empty user id", func(t *testing.T) {
err := ss.Draft().PermanentDeleteByUser("")
assert.NoError(t, err)
})
t.Run("should handle non-existing user id", func(t *testing.T) {
nonExistingUserId := model.NewId()
err := ss.Draft().PermanentDeleteByUser(nonExistingUserId)
assert.NoError(t, err)
})
}
func testGetDraft(t *testing.T, rctx request.CTX, ss store.Store) {
user := &model.User{
Id: model.NewId(),

View file

@ -181,6 +181,24 @@ func (_m *DraftStore) GetLastCreateAtAndUserIdValuesForEmptyDraftsMigration(crea
return r0, r1, r2
}
// PermanentDeleteByUser provides a mock function with given fields: userId
func (_m *DraftStore) PermanentDeleteByUser(userId string) error {
ret := _m.Called(userId)
if len(ret) == 0 {
panic("no return value specified for PermanentDeleteByUser")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// Upsert provides a mock function with given fields: d
func (_m *DraftStore) Upsert(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)

View file

@ -3429,6 +3429,22 @@ func (s *TimerLayerDraftStore) GetLastCreateAtAndUserIdValuesForEmptyDraftsMigra
return result, resultVar1, err
}
func (s *TimerLayerDraftStore) PermanentDeleteByUser(userId string) error {
start := time.Now()
err := s.DraftStore.PermanentDeleteByUser(userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerDraftStore) Upsert(d *model.Draft) (*model.Draft, error) {
start := time.Now()

View file

@ -5122,6 +5122,10 @@
"id": "app.draft.save.app_error",
"translation": "Unable to save the Draft."
},
{
"id": "app.drafts.permanent_delete_by_user.app_error",
"translation": "Unable to delete drafts for user."
},
{
"id": "app.email.no_rate_limiter.app_error",
"translation": "Rate limiter is not set up."