From 50fec7c8926412fc7e5ee6c50045c5bbeb5409e0 Mon Sep 17 00:00:00 2001 From: "Sinan Sonmez (Chaush)" <37421564+sinansonmez@users.noreply.github.com> Date: Tue, 7 Feb 2023 15:30:37 +0100 Subject: [PATCH] MM-45494: Add endpoint for message history (#20945) * add api url * write sql in store * update app and client golang driver * update layers and tests * change sql query sort * update layers * fix style * fix style post_store.go * fix comments * add test for app/post_test.go * update layers * fix test * fix style * fix style again :) * add permission check * add additional checks and tests * change from nil to empty * update app-layers * fix style * add index for OriginalId for Posts table * fix postgres query * update migration file names * update app-layers * address PR reviews * write tests for post store * fix style * Update file name * Update file name 2 * Update file name 3 * Update file name 4 * change the error orders * update migration file name --------- Co-authored-by: Mattermod Co-authored-by: Mattermost Build --- api4/post.go | 34 ++++++++ api4/post_test.go | 67 +++++++++++++++ app/app_iface.go | 1 + app/opentracing/opentracing_layer.go | 22 +++++ app/post.go | 16 ++++ app/post_test.go | 45 ++++++++++ db/migrations/migrations.list | 4 + .../000102_posts_originalid_index.down.sql | 14 +++ .../000102_posts_originalid_index.up.sql | 14 +++ .../000102_posts_originalid_index.down.sql | 1 + .../000102_posts_originalid_index.up.sql | 1 + model/client4.go | 21 +++++ store/opentracinglayer/opentracinglayer.go | 18 ++++ store/retrylayer/retrylayer.go | 21 +++++ store/sqlstore/post_store.go | 28 ++++++ store/store.go | 1 + store/storetest/mocks/PostStore.go | 23 +++++ store/storetest/post_store.go | 86 +++++++++++++++++++ store/timerlayer/timerlayer.go | 16 ++++ 19 files changed, 433 insertions(+) create mode 100644 db/migrations/mysql/000102_posts_originalid_index.down.sql create mode 100644 db/migrations/mysql/000102_posts_originalid_index.up.sql create mode 100644 db/migrations/postgres/000102_posts_originalid_index.down.sql create mode 100644 db/migrations/postgres/000102_posts_originalid_index.up.sql diff --git a/api4/post.go b/api4/post.go index 34bcf21c2b..eee642b99e 100644 --- a/api4/post.go +++ b/api4/post.go @@ -22,6 +22,7 @@ func (api *API) InitPost() { api.BaseRoutes.Post.Handle("", api.APISessionRequired(deletePost)).Methods("DELETE") api.BaseRoutes.Posts.Handle("/ids", api.APISessionRequired(getPostsByIds)).Methods("POST") api.BaseRoutes.Posts.Handle("/ephemeral", api.APISessionRequired(createEphemeralPost)).Methods("POST") + api.BaseRoutes.Post.Handle("/edit_history", api.APISessionRequired(getEditHistoryForPost)).Methods("GET") api.BaseRoutes.Post.Handle("/thread", api.APISessionRequired(getPostThread)).Methods("GET") api.BaseRoutes.Post.Handle("/info", api.APISessionRequired(getPostInfo)).Methods("GET") api.BaseRoutes.Post.Handle("/files/info", api.APISessionRequired(getFileInfosForPost)).Methods("GET") @@ -495,6 +496,39 @@ func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequirePostId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditPost) { + c.SetPermissionError(model.PermissionEditPost) + return + } + + originalPost, err := c.App.GetSinglePost(c.Params.PostId, false) + if err != nil { + c.SetPermissionError(model.PermissionEditPost) + return + } + + if c.AppContext.Session().UserId != originalPost.UserId { + c.SetPermissionError(model.PermissionEditPost) + return + } + + postsList, err := c.App.GetEditHistoryForPost(c.Params.PostId) + if err != nil { + c.Err = err + return + } + + if err := json.NewEncoder(w).Encode(postsList); err != nil { + c.Logger.Warn("Error while writing response", mlog.Err(err)) + } +} + func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) { c.RequirePostId() if c.Err != nil { diff --git a/api4/post_test.go b/api4/post_test.go index 7f3ec6c442..a73a486a50 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -3073,6 +3073,73 @@ func TestGetPostsByIds(t *testing.T) { CheckNotFoundStatus(t, response) } +func TestGetEditHistoryForPost(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + client := th.Client + + post := &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "new message", + UserId: th.BasicUser.Id, + } + + rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true) + require.Nil(t, err) + + time.Sleep(1 * time.Millisecond) + + t.Run("unedited post", func(t *testing.T) { + history, resp, err := client.GetEditHistoryForPost(rpost.Id) + require.Error(t, err) + CheckNotFoundStatus(t, resp) + require.Len(t, history, 0) + }) + + // update the post message + patch := &model.PostPatch{ + Message: model.NewString("new message edited"), + } + + // Patch the post + _, response1, err1 := client.PatchPost(rpost.Id, patch) + require.NoError(t, err1) + CheckOKStatus(t, response1) + + // update the post message again + patch = &model.PostPatch{ + Message: model.NewString("new message edited again"), + } + + _, response2, err2 := client.PatchPost(rpost.Id, patch) + require.NoError(t, err2) + CheckOKStatus(t, response2) + + t.Run("update history correctly", func(t *testing.T) { + history, response3, err3 := client.GetEditHistoryForPost(rpost.Id) + require.NoError(t, err3) + CheckOKStatus(t, response3) + + require.Len(t, history, 2) + require.Equal(t, "new message edited", history[0].Message) + require.Equal(t, "new message", history[1].Message) + }) + + t.Run("logged out", func(t *testing.T) { + client.Logout() + _, resp, err := client.GetEditHistoryForPost(rpost.Id) + require.Error(t, err) + CheckUnauthorizedStatus(t, resp) + }) + + t.Run("different user", func(t *testing.T) { + th.LoginBasic2() + _, resp, err := client.GetEditHistoryForPost(rpost.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) +} + func TestCreatePostNotificationsWithCRT(t *testing.T) { th := Setup(t).InitBasic() rpost := th.CreatePost() diff --git a/app/app_iface.go b/app/app_iface.go index 4c6776bea1..d7e3ad9c21 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -637,6 +637,7 @@ type AppIface interface { GetDeletedChannels(c request.CTX, teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) GetDraft(userID, channelID, rootID string) (*model.Draft, *model.AppError) GetDraftsForUser(userID, teamID string) ([]*model.Draft, *model.AppError) + GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 78212da98c..300e205a52 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -5960,6 +5960,28 @@ func (a *OpenTracingAppLayer) GetDraftsForUser(userID string, teamID string) ([] return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEditHistoryForPost") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetEditHistoryForPost(postID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmoji") diff --git a/app/post.go b/app/post.go index 833c98f31a..720bbd43d6 100644 --- a/app/post.go +++ b/app/post.go @@ -1998,6 +1998,22 @@ func (a *App) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppE return posts, firstInaccessiblePostTime, nil } +func (a *App) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) { + posts, err := a.Srv().Store().Post().GetEditHistoryForPost(postID) + + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return nil, model.NewAppError("GetEditHistoryForPost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err) + default: + return nil, model.NewAppError("GetEditHistoryForPost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + } + + return posts, nil +} + func (a *App) GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) { if !a.Config().FeatureFlags.InsightsEnabled { return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented) diff --git a/app/post_test.go b/app/post_test.go index 3e6b090419..463b3c40dc 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -3149,6 +3149,51 @@ func TestGetTopThreadsForUserSince(t *testing.T) { require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0) } +func TestGetEditHistoryForPost(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + post := &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "new message", + UserId: th.BasicUser.Id, + } + + rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true) + require.Nil(t, err) + + // update the post message + patch := &model.PostPatch{ + Message: model.NewString("new message edited"), + } + _, err1 := th.App.PatchPost(th.Context, rpost.Id, patch) + require.Nil(t, err1) + + // update the post message again + patch = &model.PostPatch{ + Message: model.NewString("new message edited again"), + } + + _, err2 := th.App.PatchPost(th.Context, rpost.Id, patch) + require.Nil(t, err2) + + // get the edit history + edits, err := th.App.GetEditHistoryForPost(post.Id) + require.Nil(t, err) + + t.Run("should return the edit history", func(t *testing.T) { + require.Len(t, edits, 2) + require.Equal(t, "new message edited", edits[0].Message) + require.Equal(t, "new message", edits[1].Message) + }) + + t.Run("should return an error if the post is not found", func(t *testing.T) { + edits, err := th.App.GetEditHistoryForPost("invalid-post-id") + require.NotNil(t, err) + require.Empty(t, edits) + }) +} + func TestGetTopDMsForUserSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/db/migrations/migrations.list b/db/migrations/migrations.list index dab9088afb..ef1ba5f3e2 100644 --- a/db/migrations/migrations.list +++ b/db/migrations/migrations.list @@ -202,6 +202,8 @@ db/migrations/mysql/000100_add_draft_priority_column.down.sql db/migrations/mysql/000100_add_draft_priority_column.up.sql db/migrations/mysql/000101_create_true_up_review_history.down.sql db/migrations/mysql/000101_create_true_up_review_history.up.sql +db/migrations/mysql/000102_posts_originalid_index.down.sql +db/migrations/mysql/000102_posts_originalid_index.up.sql db/migrations/postgres/000001_create_teams.down.sql db/migrations/postgres/000001_create_teams.up.sql db/migrations/postgres/000002_create_team_members.down.sql @@ -404,3 +406,5 @@ db/migrations/postgres/000100_add_draft_priority_column.down.sql db/migrations/postgres/000100_add_draft_priority_column.up.sql db/migrations/postgres/000101_create_true_up_review_history.down.sql db/migrations/postgres/000101_create_true_up_review_history.up.sql +db/migrations/postgres/000102_posts_originalid_index.down.sql +db/migrations/postgres/000102_posts_originalid_index.up.sql diff --git a/db/migrations/mysql/000102_posts_originalid_index.down.sql b/db/migrations/mysql/000102_posts_originalid_index.down.sql new file mode 100644 index 0000000000..5fb6b76ca8 --- /dev/null +++ b/db/migrations/mysql/000102_posts_originalid_index.down.sql @@ -0,0 +1,14 @@ +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'Posts' + AND table_schema = DATABASE() + AND index_name = 'idx_posts_original_id' + ) > 0, + 'DROP INDEX idx_posts_original_id on Posts;', + 'SELECT 1;' + )); + + PREPARE removeIndexIfExists FROM @preparedStatement; + EXECUTE removeIndexIfExists; + DEALLOCATE PREPARE removeIndexIfExists; diff --git a/db/migrations/mysql/000102_posts_originalid_index.up.sql b/db/migrations/mysql/000102_posts_originalid_index.up.sql new file mode 100644 index 0000000000..355a501678 --- /dev/null +++ b/db/migrations/mysql/000102_posts_originalid_index.up.sql @@ -0,0 +1,14 @@ +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'Posts' + AND table_schema = DATABASE() + AND index_name = 'idx_posts_original_id' + ) > 0, + 'SELECT 1;', + 'CREATE INDEX idx_posts_original_id on Posts(OriginalId);' + )); + + PREPARE createIndexIfNotExists FROM @preparedStatement; + EXECUTE createIndexIfNotExists; + DEALLOCATE PREPARE createIndexIfNotExists; diff --git a/db/migrations/postgres/000102_posts_originalid_index.down.sql b/db/migrations/postgres/000102_posts_originalid_index.down.sql new file mode 100644 index 0000000000..172795f0d8 --- /dev/null +++ b/db/migrations/postgres/000102_posts_originalid_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS idx_posts_original_id; diff --git a/db/migrations/postgres/000102_posts_originalid_index.up.sql b/db/migrations/postgres/000102_posts_originalid_index.up.sql new file mode 100644 index 0000000000..0d2560fdf1 --- /dev/null +++ b/db/migrations/postgres/000102_posts_originalid_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS idx_posts_original_id ON Posts(originalid); diff --git a/model/client4.go b/model/client4.go index a06a89abb7..1587df0e00 100644 --- a/model/client4.go +++ b/model/client4.go @@ -4017,6 +4017,27 @@ func (c *Client4) GetPostsByIds(postIds []string) ([]*Post, *Response, error) { return list, BuildResponse(r), nil } +// GetEditHistoryForPost gets a list of posts by taking a post ids +func (c *Client4) GetEditHistoryForPost(postId string) ([]*Post, *Response, error) { + js, err := json.Marshal(postId) + if err != nil { + return nil, nil, NewAppError("GetEditHistoryForPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + r, err := c.DoAPIGet(c.postRoute(postId)+"/edit_history", string(js)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Post + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + return nil, nil, NewAppError("GetEditHistoryForPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return list, BuildResponse(r), nil +} + // GetFlaggedPostsForUser returns flagged posts of a user based on user id string. func (c *Client4) GetFlaggedPostsForUser(userId string, page int, perPage int) (*PostList, *Response, error) { query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 289b7f9582..5b6ce59fb0 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -5928,6 +5928,24 @@ func (s *OpenTracingLayerPostStore) GetDirectPostParentsForExportAfter(limit int return result, err } +func (s *OpenTracingLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetEditHistoryForPost") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostStore.GetEditHistoryForPost(postId) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetEtag") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index c474c10718..45e8bd1bbc 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -6724,6 +6724,27 @@ func (s *RetryLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afte } +func (s *RetryLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) { + + tries := 0 + for { + result, err := s.PostStore.GetEditHistoryForPost(postId) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string { return s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads) diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 3cf2310a48..0312058075 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -2344,6 +2344,34 @@ func (s *SqlPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) { return posts, nil } +func (s *SqlPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) { + builder := s.getQueryBuilder(). + Select("*"). + From("Posts"). + Where(sq.Eq{"Posts.OriginalId": postId}). + OrderBy("Posts.EditAt DESC") + + queryString, args, err := builder.ToSql() + if err != nil { + if err == sql.ErrNoRows { + return nil, store.NewErrNotFound("Post", postId) + } + return nil, errors.Wrap(err, "failed to find post history") + } + + posts := []*model.Post{} + err = s.GetReplicaX().Select(&posts, queryString, args...) + if err != nil { + return nil, errors.Wrapf(err, "error getting posts edit history with postId=%s", postId) + } + + if len(posts) == 0 { + return nil, store.NewErrNotFound("failed to find post history", postId) + } + + return posts, nil +} + func (s *SqlPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) { posts := []*model.PostForIndexing{} table := "Posts" diff --git a/store/store.go b/store/store.go index a752cb784a..01a74ae943 100644 --- a/store/store.go +++ b/store/store.go @@ -386,6 +386,7 @@ type PostStore interface { Overwrite(post *model.Post) (*model.Post, error) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) GetPostsByIds(postIds []string) ([]*model.Post, error) + GetEditHistoryForPost(postId string) ([]*model.Post, error) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) DeleteOrphanedRows(limit int) (deleted int64, err error) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 91e39efb45..f1ecb5754f 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -171,6 +171,29 @@ func (_m *PostStore) GetDirectPostParentsForExportAfter(limit int, afterID strin return r0, r1 } +// GetEditHistoryForPost provides a mock function with given fields: postId +func (_m *PostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) { + ret := _m.Called(postId) + + var r0 []*model.Post + if rf, ok := ret.Get(0).(func(string) []*model.Post); ok { + r0 = rf(postId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Post) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(postId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetEtag provides a mock function with given fields: channelID, allowFromCache, collapsedThreads func (_m *PostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string { ret := _m.Called(channelID, allowFromCache, collapsedThreads) diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 10f84c132e..8486f89adf 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -64,6 +64,7 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, ss, s) }) t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, ss) }) t.Run("GetTopDMsForUserSince", func(t *testing.T) { testGetTopDMsForUserSince(t, ss, s) }) + t.Run("GetEditHistoryForPost", func(t *testing.T) { testGetEditHistoryForPost(t, ss) }) } func testPostStoreSave(t *testing.T, ss store.Store) { @@ -4965,3 +4966,88 @@ func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) { require.Len(t, topDMs.Items, 2) }) } + +func testGetEditHistoryForPost(t *testing.T, ss store.Store) { + t.Run("should return edit history for post", func(t *testing.T) { + // create a post + post := &model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + Message: "test", + } + originalPost, err := ss.Post().Save(post) + require.NoError(t, err) + // create an edit + updatedPost := originalPost.Clone() + updatedPost.Message = "test edited" + savedUpdatedPost, err := ss.Post().Update(updatedPost, originalPost) + require.NoError(t, err) + // get edit history + edits, err := ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id) + require.NoError(t, err) + require.Len(t, edits, 1) + require.Equal(t, originalPost.Id, edits[0].Id) + require.Equal(t, originalPost.UserId, edits[0].UserId) + require.Equal(t, originalPost.Message, edits[0].Message) + }) + + t.Run("should return error for not edited posts", func(t *testing.T) { + // create a post + post := &model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + Message: "test", + } + originalPost, err := ss.Post().Save(post) + require.NoError(t, err) + // get edit history + _, err = ss.Post().GetEditHistoryForPost(originalPost.Id) + require.Error(t, err) + }) + + t.Run("should return error for non-existent post", func(t *testing.T) { + // get edit history + _, err := ss.Post().GetEditHistoryForPost("non-existent") + require.Error(t, err) + }) + + t.Run("should return error for deleted post", func(t *testing.T) { + // create a post + post := &model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + Message: "test", + } + originalPost, err := ss.Post().Save(post) + require.NoError(t, err) + // delete post + err = ss.Post().Delete(post.Id, 100, post.UserId) + require.NoError(t, err) + // get edit history + _, err = ss.Post().GetEditHistoryForPost(originalPost.Id) + require.Error(t, err) + }) + + t.Run("should return error for deleted edit", func(t *testing.T) { + // create a post + post := &model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + Message: "test", + } + originalPost, err := ss.Post().Save(post) + require.NoError(t, err) + // create an edit + updatedPost := originalPost.Clone() + updatedPost.Message = "test edited" + savedUpdatedPost, err := ss.Post().Update(updatedPost, originalPost) + require.NoError(t, err) + // delete edit + err = ss.Post().Delete(savedUpdatedPost.Id, 100, savedUpdatedPost.UserId) + require.NoError(t, err) + // get edit history + _, err = ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id) + require.NoError(t, err) + }) + +} diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index dfd2e145a2..eb9c4d4bbf 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -5371,6 +5371,22 @@ func (s *TimerLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afte return result, err } +func (s *TimerLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) { + start := time.Now() + + result, err := s.PostStore.GetEditHistoryForPost(postId) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetEditHistoryForPost", success, elapsed) + } + return result, err +} + func (s *TimerLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string { start := time.Now()