mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Content flagging actions implementation tests (#35035)
* Added tests for TestGetFlaggingConfiguration * added tests for RemoveFlaggedPost * added tests for KeepFlaggedPost * Added tests for scrubPost and keep flagged post APp layer functions * Added tests for DeleteAllForPost * added tests for DeleteAllPostRemindersForPost * Added tests for DataSpillageFooter * Added tests for KeepRemoveFlaggedMessageConfirmationModal * Added tests for KeepRemoveFlaggedMessageConfirmationModal * Fixed lint errors * lint fix * Fixed query param * fixed tests * Used middleware to check for content flagging basic checks * Added TestRequireContentFlaggingEnabled * Updated tests * refactoring to reduce code duplication * review fixes --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
033867a344
commit
b96f7c1a8d
9 changed files with 1855 additions and 338 deletions
|
|
@ -20,18 +20,19 @@ func (api *API) InitContentFlagging() {
|
|||
return
|
||||
}
|
||||
|
||||
api.BaseRoutes.ContentFlagging.Handle("/flag/config", api.APISessionRequired(getFlaggingConfiguration)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/status", api.APISessionRequired(getTeamPostFlaggingFeatureStatus)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/flag", api.APISessionRequired(flagPost)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/fields", api.APISessionRequired(getContentFlaggingFields)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/field_values", api.APISessionRequired(getPostPropertyValues)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(getFlaggedPost)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/remove", api.APISessionRequired(removeFlaggedPost)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/keep", api.APISessionRequired(keepFlaggedPost)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/flag/config", api.APISessionRequired(contentFlaggingRequired(getFlaggingConfiguration))).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/status", api.APISessionRequired(contentFlaggingRequired(getTeamPostFlaggingFeatureStatus))).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/flag", api.APISessionRequired(contentFlaggingRequired(flagPost))).Methods(http.MethodPost)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/fields", api.APISessionRequired(contentFlaggingRequired(getContentFlaggingFields))).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/field_values", api.APISessionRequired(contentFlaggingRequired(getPostPropertyValues))).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(contentFlaggingRequired(getFlaggedPost))).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/remove", api.APISessionRequired(contentFlaggingRequired(removeFlaggedPost))).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/keep", api.APISessionRequired(contentFlaggingRequired(keepFlaggedPost))).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/reviewers/search", api.APISessionRequired(contentFlaggingRequired(searchReviewers))).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/assign/{content_reviewer_id:[A-Za-z0-9]+}", api.APISessionRequired(contentFlaggingRequired(assignFlaggedPostReviewer))).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.ContentFlagging.Handle("/config", api.APISessionRequired(saveContentFlaggingSettings)).Methods(http.MethodPut)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/config", api.APISessionRequired(getContentFlaggingSettings)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/reviewers/search", api.APISessionRequired(searchReviewers)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/post/{post_id:[A-Za-z0-9]+}/assign/{content_reviewer_id:[A-Za-z0-9]+}", api.APISessionRequired(assignFlaggedPostReviewer)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func requireContentFlaggingAvailable(c *Context) {
|
||||
|
|
@ -54,6 +55,17 @@ func requireContentFlaggingEnabled(c *Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func contentFlaggingRequired(h handlerFunc) handlerFunc {
|
||||
return func(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h(c, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func requireTeamContentReviewer(c *Context, userId, teamId string) {
|
||||
isReviewer, appErr := c.App.IsUserTeamContentReviewer(userId, teamId)
|
||||
if appErr != nil {
|
||||
|
|
@ -81,7 +93,6 @@ func requireFlaggedPost(c *Context, postId string) {
|
|||
}
|
||||
|
||||
func getFlaggingConfiguration(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -112,7 +123,6 @@ func getFlaggingConfiguration(c *Context, w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
func getTeamPostFlaggingFeatureStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -145,7 +155,6 @@ func getTeamPostFlaggingFeatureStatus(c *Context, w http.ResponseWriter, r *http
|
|||
}
|
||||
|
||||
func flagPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -226,7 +235,6 @@ func getFlaggingConfig(contentFlaggingSettings model.ContentFlaggingSettings, as
|
|||
}
|
||||
|
||||
func getContentFlaggingFields(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -250,7 +258,6 @@ func getContentFlaggingFields(c *Context, w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
func getPostPropertyValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -294,7 +301,6 @@ func getPostPropertyValues(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func getFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -406,7 +412,6 @@ func keepFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func keepRemoveFlaggedPostChecks(c *Context, r *http.Request) (*model.FlagContentActionRequest, string, *model.Post) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
|
@ -525,7 +530,6 @@ func getContentFlaggingSettings(c *Context, w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
|
||||
func searchReviewers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -557,7 +561,6 @@ func searchReviewers(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func assignFlaggedPostReviewer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -36,6 +36,23 @@ func setBaseConfig(th *TestHelper) *model.AppError {
|
|||
return nil
|
||||
}
|
||||
|
||||
func searchPropertyValue(t *testing.T, th *TestHelper, postId, fieldName string) []*model.PropertyValue {
|
||||
t.Helper()
|
||||
groupId, appErr := th.App.ContentFlaggingGroupId()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
values, err := th.Server.propertyAccessService.SearchPropertyValues("", groupId, model.PropertyValueSearchOpts{
|
||||
TargetIDs: []string{postId},
|
||||
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
|
||||
FieldID: mappedFields[fieldName].ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return values
|
||||
}
|
||||
|
||||
func setupFlaggedPost(t *testing.T, th *TestHelper) *model.Post {
|
||||
post := th.CreatePost(t, th.BasicChannel)
|
||||
|
||||
|
|
@ -2465,7 +2482,7 @@ func TestPermanentDeleteFlaggedPost(t *testing.T) {
|
|||
require.Eventually(t, func() bool {
|
||||
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.Nil(t, appErr)
|
||||
return true
|
||||
return appErr == nil
|
||||
}, 5*time.Second, 200*time.Millisecond)
|
||||
|
||||
// Verify post was deleted and status updated
|
||||
|
|
@ -2506,7 +2523,7 @@ func TestPermanentDeleteFlaggedPost(t *testing.T) {
|
|||
require.Eventually(t, func() bool {
|
||||
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, editedPost)
|
||||
require.Nil(t, appErr)
|
||||
return true
|
||||
return appErr == nil
|
||||
}, 5*time.Second, 200*time.Millisecond)
|
||||
|
||||
// Verify status was updated
|
||||
|
|
@ -2545,7 +2562,7 @@ func TestPermanentDeleteFlaggedPost(t *testing.T) {
|
|||
deletedPost, appErr = th.App.GetSinglePost(th.Context, post.Id, true)
|
||||
require.Nil(t, appErr)
|
||||
require.True(t, deletedPost.DeleteAt > 0)
|
||||
return true
|
||||
return appErr == nil
|
||||
}, 5*time.Second, 200*time.Millisecond)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
|
|
@ -2567,3 +2584,508 @@ func TestPermanentDeleteFlaggedPost(t *testing.T) {
|
|||
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeepFlaggedPost(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
require.Nil(t, setBaseConfig(th))
|
||||
|
||||
t.Run("should successfully keep pending flagged post", func(t *testing.T) {
|
||||
post := setupFlaggedPost(t, th)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "This post is acceptable after review",
|
||||
}
|
||||
|
||||
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify post still exists and is not deleted
|
||||
updatedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, int64(0), updatedPost.DeleteAt)
|
||||
|
||||
// Verify status was updated to retained
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
|
||||
|
||||
// Verify actor properties were created
|
||||
// Check actor user property
|
||||
actorValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorUserID)
|
||||
require.Len(t, actorValues, 1)
|
||||
require.Equal(t, `"`+th.SystemAdminUser.Id+`"`, string(actorValues[0].Value))
|
||||
|
||||
// Check actor comment property
|
||||
commentValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorComment)
|
||||
require.Len(t, commentValues, 1)
|
||||
require.Equal(t, `"This post is acceptable after review"`, string(commentValues[0].Value))
|
||||
|
||||
// Check action time property
|
||||
timeValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActionTime)
|
||||
require.Len(t, timeValues, 1)
|
||||
|
||||
var actionTime int64
|
||||
err := json.Unmarshal(timeValues[0].Value, &actionTime)
|
||||
require.NoError(t, err)
|
||||
require.True(t, actionTime > 0)
|
||||
})
|
||||
|
||||
t.Run("should successfully keep and restore hidden flagged post", func(t *testing.T) {
|
||||
baseConfig := getBaseConfig(th)
|
||||
baseConfig.AdditionalSettings.HideFlaggedContent = model.NewPointer(true)
|
||||
appErr := th.App.SaveContentFlaggingConfig(baseConfig)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post := th.CreatePost(t, th.BasicChannel)
|
||||
|
||||
flagData := model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
// Flag the post (this will hide/delete it due to HideFlaggedContent setting)
|
||||
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
var hiddenPost *model.Post
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
// Get the updated post (should be deleted/hidden)
|
||||
hiddenPost, appErr = th.App.GetSinglePost(th.Context, post.Id, true)
|
||||
require.Nil(t, appErr)
|
||||
return hiddenPost.DeleteAt > 0
|
||||
}, 5*time.Second, 200*time.Millisecond)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Restoring this post after review",
|
||||
}
|
||||
|
||||
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, hiddenPost)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify post was restored (DeleteAt should be 0)
|
||||
restoredPost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, int64(0), restoredPost.DeleteAt)
|
||||
|
||||
// Verify status was updated to retained
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
|
||||
})
|
||||
|
||||
t.Run("should successfully keep assigned flagged post", func(t *testing.T) {
|
||||
// Reset config to not hide flagged content
|
||||
require.Nil(t, setBaseConfig(th))
|
||||
|
||||
post := setupFlaggedPost(t, th)
|
||||
|
||||
// Assign the post to a reviewer first
|
||||
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Assigned post is acceptable",
|
||||
}
|
||||
|
||||
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify status was updated to retained
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
|
||||
})
|
||||
|
||||
t.Run("should fail when trying to keep already removed post", func(t *testing.T) {
|
||||
post := setupFlaggedPost(t, th)
|
||||
|
||||
// Set status to removed
|
||||
groupId, appErr := th.App.ContentFlaggingGroupId()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
|
||||
_, err := th.App.Srv().propertyAccessService.UpdatePropertyValue("", groupId, statusValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Trying to keep already removed post",
|
||||
}
|
||||
|
||||
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, "api.content_flagging.error.post_not_in_progress", appErr.Id)
|
||||
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should fail when trying to keep already retained post", func(t *testing.T) {
|
||||
post := setupFlaggedPost(t, th)
|
||||
|
||||
// Set status to retained
|
||||
groupId, appErr := th.App.ContentFlaggingGroupId()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
|
||||
_, err := th.App.Srv().propertyAccessService.UpdatePropertyValue("", groupId, statusValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Trying to keep already retained post",
|
||||
}
|
||||
|
||||
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, "api.content_flagging.error.post_not_in_progress", appErr.Id)
|
||||
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should fail when trying to keep non-flagged post", func(t *testing.T) {
|
||||
post := th.CreatePost(t, th.BasicChannel)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Trying to keep non-flagged post",
|
||||
}
|
||||
|
||||
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should handle empty comment", func(t *testing.T) {
|
||||
post := setupFlaggedPost(t, th)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "",
|
||||
}
|
||||
|
||||
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify empty comment was stored
|
||||
commentValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorComment)
|
||||
require.Len(t, commentValues, 1)
|
||||
require.Equal(t, `""`, string(commentValues[0].Value))
|
||||
})
|
||||
|
||||
t.Run("should handle special characters in comment", func(t *testing.T) {
|
||||
post := setupFlaggedPost(t, th)
|
||||
|
||||
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: specialComment,
|
||||
}
|
||||
|
||||
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify special characters were properly escaped and stored
|
||||
commentValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorComment)
|
||||
require.Len(t, commentValues, 1)
|
||||
|
||||
var storedComment string
|
||||
err := json.Unmarshal(commentValues[0].Value, &storedComment)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, specialComment, storedComment)
|
||||
})
|
||||
|
||||
t.Run("should preserve file attachments when keeping flagged post", func(t *testing.T) {
|
||||
// Create a post with file attachments
|
||||
post := th.CreatePost(t, th.BasicChannel)
|
||||
|
||||
// Create some file infos and associate them with the post
|
||||
fileInfo1 := &model.FileInfo{
|
||||
Id: model.NewId(),
|
||||
PostId: post.Id,
|
||||
CreatorId: post.UserId,
|
||||
Path: "test/file1.txt",
|
||||
Name: "file1.txt",
|
||||
Size: 100,
|
||||
}
|
||||
fileInfo2 := &model.FileInfo{
|
||||
Id: model.NewId(),
|
||||
PostId: post.Id,
|
||||
CreatorId: post.UserId,
|
||||
Path: "test/file2.txt",
|
||||
Name: "file2.txt",
|
||||
Size: 200,
|
||||
}
|
||||
|
||||
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo1)
|
||||
require.NoError(t, err)
|
||||
_, err = th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update post to include file IDs
|
||||
post.FileIds = []string{fileInfo1.Id, fileInfo2.Id}
|
||||
_, _, appErr := th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Flag the post
|
||||
flagData := model.FlagContentRequest{
|
||||
Reason: "spam",
|
||||
Comment: "This is spam content",
|
||||
}
|
||||
|
||||
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Post with files is acceptable",
|
||||
}
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
|
||||
require.Nil(t, appErr)
|
||||
return appErr == nil
|
||||
}, 5*time.Second, 200*time.Millisecond)
|
||||
|
||||
// Verify post was retained
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
|
||||
|
||||
// Verify file infos are still present (not deleted)
|
||||
files, err := th.App.Srv().Store().FileInfo().GetByIds([]string{fileInfo1.Id, fileInfo2.Id}, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, files, 2, "File attachments should be preserved when keeping flagged post")
|
||||
})
|
||||
|
||||
t.Run("should preserve edit history when keeping flagged post", func(t *testing.T) {
|
||||
post := th.CreatePost(t, th.BasicChannel)
|
||||
|
||||
// Create edit history for the post
|
||||
editedPost := post.Clone()
|
||||
editedPost.Message = "Edited message"
|
||||
editedPost.EditAt = model.GetMillis()
|
||||
|
||||
_, _, appErr := th.App.UpdatePost(th.Context, editedPost, &model.UpdatePostOptions{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify edit history exists before flagging
|
||||
editHistoryBefore, appErr := th.App.GetEditHistoryForPost(post.Id)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEmpty(t, editHistoryBefore)
|
||||
editHistoryPostId := editHistoryBefore[0].Id
|
||||
|
||||
// Flag the post
|
||||
flagData := model.FlagContentRequest{
|
||||
Reason: "inappropriate",
|
||||
Comment: "This post is inappropriate",
|
||||
}
|
||||
|
||||
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
actionRequest := &model.FlagContentActionRequest{
|
||||
Comment: "Post with edit history is acceptable",
|
||||
}
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, editedPost)
|
||||
require.Nil(t, appErr)
|
||||
return appErr == nil
|
||||
}, 5*time.Second, 200*time.Millisecond)
|
||||
|
||||
// Verify status was updated to retained
|
||||
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(editedPost.Id, ContentFlaggingPropertyNameStatus)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
var stringValue string
|
||||
err := json.Unmarshal(statusValue.Value, &stringValue)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, model.ContentFlaggingStatusRetained, stringValue)
|
||||
|
||||
// Verify edit history is still present (not deleted)
|
||||
editHistoryAfter, appErr := th.App.GetEditHistoryForPost(post.Id)
|
||||
require.Nil(t, appErr, "Edit history should be preserved when keeping flagged post")
|
||||
require.NotEmpty(t, editHistoryAfter)
|
||||
require.Equal(t, editHistoryPostId, editHistoryAfter[0].Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestScrubPost(t *testing.T) {
|
||||
expectedMessage := "*Content deleted as part of Content Flagging review process*"
|
||||
|
||||
t.Run("should scrub all post content fields", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "This is the original message with sensitive content",
|
||||
MessageSource: "This is the original message source",
|
||||
Hashtags: "#hashtag1 #hashtag2",
|
||||
FileIds: []string{"file1", "file2", "file3"},
|
||||
Metadata: &model.PostMetadata{
|
||||
Embeds: []*model.PostEmbed{
|
||||
{Type: "link", URL: "https://example.com"},
|
||||
},
|
||||
Files: []*model.FileInfo{
|
||||
{Id: "file1", Name: "test.png"},
|
||||
},
|
||||
},
|
||||
}
|
||||
post.SetProps(map[string]any{
|
||||
"custom_prop": "custom_value",
|
||||
"another_prop": 123,
|
||||
"sensitive_data": "should be removed",
|
||||
})
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.Equal(t, expectedMessage, post.MessageSource)
|
||||
require.Equal(t, "", post.Hashtags)
|
||||
require.Nil(t, post.Metadata)
|
||||
require.Empty(t, post.FileIds)
|
||||
require.NotNil(t, post.GetProps())
|
||||
require.Empty(t, post.GetProps())
|
||||
})
|
||||
|
||||
t.Run("should handle post with empty fields", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "",
|
||||
MessageSource: "",
|
||||
Hashtags: "",
|
||||
FileIds: []string{},
|
||||
Metadata: nil,
|
||||
}
|
||||
post.SetProps(make(map[string]any))
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.Equal(t, expectedMessage, post.MessageSource)
|
||||
require.Equal(t, "", post.Hashtags)
|
||||
require.Nil(t, post.Metadata)
|
||||
require.Empty(t, post.FileIds)
|
||||
require.NotNil(t, post.GetProps())
|
||||
require.Empty(t, post.GetProps())
|
||||
})
|
||||
|
||||
t.Run("should handle post with nil FileIds", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "Test message",
|
||||
FileIds: nil,
|
||||
}
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.NotNil(t, post.FileIds)
|
||||
require.Empty(t, post.FileIds)
|
||||
})
|
||||
|
||||
t.Run("should handle post with nil Props", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "Test message",
|
||||
}
|
||||
// Props is nil by default
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.NotNil(t, post.GetProps())
|
||||
require.Empty(t, post.GetProps())
|
||||
})
|
||||
|
||||
t.Run("should preserve non-content fields", func(t *testing.T) {
|
||||
postId := model.NewId()
|
||||
userId := model.NewId()
|
||||
channelId := model.NewId()
|
||||
rootId := model.NewId()
|
||||
createAt := model.GetMillis()
|
||||
updateAt := model.GetMillis()
|
||||
editAt := model.GetMillis()
|
||||
|
||||
post := &model.Post{
|
||||
Id: postId,
|
||||
CreateAt: createAt,
|
||||
UpdateAt: updateAt,
|
||||
EditAt: editAt,
|
||||
DeleteAt: 0,
|
||||
UserId: userId,
|
||||
ChannelId: channelId,
|
||||
RootId: rootId,
|
||||
OriginalId: "",
|
||||
Message: "Original message to be scrubbed",
|
||||
MessageSource: "Original source",
|
||||
Type: model.PostTypeDefault,
|
||||
Hashtags: "#test",
|
||||
FileIds: []string{"file1"},
|
||||
}
|
||||
post.SetProps(map[string]any{"key": "value"})
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
// Verify content fields are scrubbed
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.Equal(t, expectedMessage, post.MessageSource)
|
||||
require.Equal(t, "", post.Hashtags)
|
||||
require.Nil(t, post.Metadata)
|
||||
require.Empty(t, post.FileIds)
|
||||
require.Empty(t, post.GetProps())
|
||||
|
||||
// Verify non-content fields are preserved
|
||||
require.Equal(t, postId, post.Id)
|
||||
require.Equal(t, createAt, post.CreateAt)
|
||||
require.Equal(t, updateAt, post.UpdateAt)
|
||||
require.Equal(t, editAt, post.EditAt)
|
||||
require.Equal(t, userId, post.UserId)
|
||||
require.Equal(t, channelId, post.ChannelId)
|
||||
require.Equal(t, rootId, post.RootId)
|
||||
require.Equal(t, model.PostTypeDefault, post.Type)
|
||||
})
|
||||
|
||||
t.Run("should handle post with special characters in message", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "Message with <script>alert('xss')</script> and @mentions #hashtags ~channels",
|
||||
MessageSource: "Source with unicode: 你好世界 🎉 émojis",
|
||||
Hashtags: "#特殊 #émoji",
|
||||
}
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.Equal(t, expectedMessage, post.MessageSource)
|
||||
require.Equal(t, "", post.Hashtags)
|
||||
})
|
||||
|
||||
t.Run("should handle post with complex Metadata", func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "Test message",
|
||||
Metadata: &model.PostMetadata{
|
||||
Embeds: []*model.PostEmbed{
|
||||
{Type: "link", URL: "https://example1.com"},
|
||||
{Type: "link", URL: "https://example2.com"},
|
||||
},
|
||||
Emojis: []*model.Emoji{
|
||||
{Id: "emoji1", Name: "custom_emoji"},
|
||||
},
|
||||
Files: []*model.FileInfo{
|
||||
{Id: "file1", Name: "document.pdf", Size: 1024},
|
||||
{Id: "file2", Name: "image.png", Size: 2048},
|
||||
},
|
||||
Reactions: []*model.Reaction{
|
||||
{UserId: "user1", PostId: "post1", EmojiName: "thumbsup"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
scrubPost(post)
|
||||
|
||||
require.Equal(t, expectedMessage, post.Message)
|
||||
require.Nil(t, post.Metadata)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ func TestPostAcknowledgementsStore(t *testing.T, rctx request.CTX, ss store.Stor
|
|||
t.Run("GetForPosts", func(t *testing.T) { testPostAcknowledgementsStoreGetForPosts(t, rctx, ss) })
|
||||
t.Run("BatchSave", func(t *testing.T) { testPostAcknowledgementsStoreBatchSave(t, rctx, ss) })
|
||||
t.Run("BatchDelete", func(t *testing.T) { testPostAcknowledgementsStoreBatchDelete(t, rctx, ss) })
|
||||
t.Run("DeleteAllForPost", func(t *testing.T) { testPostAcknowledgementsStoreDeleteAllForPost(t, rctx, ss) })
|
||||
}
|
||||
|
||||
func testPostAcknowledgementsStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
|
|
@ -419,3 +420,163 @@ func testPostAcknowledgementsStoreBatchDelete(t *testing.T, rctx request.CTX, ss
|
|||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testPostAcknowledgementsStoreDeleteAllForPost(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
userID1 := model.NewId()
|
||||
userID2 := model.NewId()
|
||||
userID3 := model.NewId()
|
||||
|
||||
t.Run("should permanently delete all acknowledgements for a post", func(t *testing.T) {
|
||||
p1 := model.Post{}
|
||||
p1.ChannelId = model.NewId()
|
||||
p1.UserId = model.NewId()
|
||||
p1.Message = NewTestID()
|
||||
p1.Metadata = &model.PostMetadata{
|
||||
Priority: &model.PostPriority{
|
||||
Priority: model.NewPointer("important"),
|
||||
RequestedAck: model.NewPointer(true),
|
||||
PersistentNotifications: model.NewPointer(false),
|
||||
},
|
||||
}
|
||||
post, err := ss.Post().Save(rctx, &p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create multiple acknowledgements
|
||||
ack1 := &model.PostAcknowledgement{PostId: post.Id, UserId: userID1, AcknowledgedAt: 0, ChannelId: post.ChannelId}
|
||||
_, err = ss.PostAcknowledgement().SaveWithModel(ack1)
|
||||
require.NoError(t, err)
|
||||
ack2 := &model.PostAcknowledgement{PostId: post.Id, UserId: userID2, AcknowledgedAt: 0, ChannelId: post.ChannelId}
|
||||
_, err = ss.PostAcknowledgement().SaveWithModel(ack2)
|
||||
require.NoError(t, err)
|
||||
ack3 := &model.PostAcknowledgement{PostId: post.Id, UserId: userID3, AcknowledgedAt: 0, ChannelId: post.ChannelId}
|
||||
_, err = ss.PostAcknowledgement().SaveWithModel(ack3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify acknowledgements were created
|
||||
acks, err := ss.PostAcknowledgement().GetForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, acks, 3)
|
||||
|
||||
// Delete all acknowledgements for the post
|
||||
err = ss.PostAcknowledgement().DeleteAllForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all acknowledgements were permanently deleted
|
||||
acks, err = ss.PostAcknowledgement().GetForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, acks)
|
||||
|
||||
// Verify they are truly deleted and not just soft-deleted (AcknowledgedAt = 0)
|
||||
// by checking GetForPostSince with inclDeleted = true
|
||||
acksWithDeleted, err := ss.PostAcknowledgement().GetForPostSince(post.Id, 0, "", true)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, acksWithDeleted, "Acknowledgements should be permanently deleted, not soft-deleted")
|
||||
})
|
||||
|
||||
t.Run("should only delete acknowledgements for the specified post", func(t *testing.T) {
|
||||
// Create two posts
|
||||
p1 := model.Post{}
|
||||
p1.ChannelId = model.NewId()
|
||||
p1.UserId = model.NewId()
|
||||
p1.Message = NewTestID()
|
||||
post1, err := ss.Post().Save(rctx, &p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
p2 := model.Post{}
|
||||
p2.ChannelId = model.NewId()
|
||||
p2.UserId = model.NewId()
|
||||
p2.Message = NewTestID()
|
||||
post2, err := ss.Post().Save(rctx, &p2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create acknowledgements for both posts
|
||||
ack1 := &model.PostAcknowledgement{PostId: post1.Id, UserId: userID1, AcknowledgedAt: 0, ChannelId: post1.ChannelId}
|
||||
_, err = ss.PostAcknowledgement().SaveWithModel(ack1)
|
||||
require.NoError(t, err)
|
||||
ack2 := &model.PostAcknowledgement{PostId: post1.Id, UserId: userID2, AcknowledgedAt: 0, ChannelId: post1.ChannelId}
|
||||
_, err = ss.PostAcknowledgement().SaveWithModel(ack2)
|
||||
require.NoError(t, err)
|
||||
ack3 := &model.PostAcknowledgement{PostId: post2.Id, UserId: userID1, AcknowledgedAt: 0, ChannelId: post2.ChannelId}
|
||||
ack3, err = ss.PostAcknowledgement().SaveWithModel(ack3)
|
||||
require.NoError(t, err)
|
||||
ack4 := &model.PostAcknowledgement{PostId: post2.Id, UserId: userID2, AcknowledgedAt: 0, ChannelId: post2.ChannelId}
|
||||
ack4, err = ss.PostAcknowledgement().SaveWithModel(ack4)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete all acknowledgements for post1 only
|
||||
err = ss.PostAcknowledgement().DeleteAllForPost(post1.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify post1 acknowledgements are deleted
|
||||
acks1, err := ss.PostAcknowledgement().GetForPost(post1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, acks1)
|
||||
|
||||
// Verify post2 acknowledgements are still present
|
||||
acks2, err := ss.PostAcknowledgement().GetForPost(post2.Id)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, acks2, 2)
|
||||
require.ElementsMatch(t, acks2, []*model.PostAcknowledgement{ack3, ack4})
|
||||
})
|
||||
|
||||
t.Run("should not error when deleting from post with no acknowledgements", func(t *testing.T) {
|
||||
p1 := model.Post{}
|
||||
p1.ChannelId = model.NewId()
|
||||
p1.UserId = model.NewId()
|
||||
p1.Message = NewTestID()
|
||||
post, err := ss.Post().Save(rctx, &p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify no acknowledgements exist
|
||||
acks, err := ss.PostAcknowledgement().GetForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, acks)
|
||||
|
||||
// Delete should not error
|
||||
err = ss.PostAcknowledgement().DeleteAllForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should not error when deleting from non-existent post", func(t *testing.T) {
|
||||
nonExistentPostId := model.NewId()
|
||||
|
||||
// Delete should not error even for non-existent post
|
||||
err := ss.PostAcknowledgement().DeleteAllForPost(nonExistentPostId)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should delete both active and soft-deleted acknowledgements", func(t *testing.T) {
|
||||
p1 := model.Post{}
|
||||
p1.ChannelId = model.NewId()
|
||||
p1.UserId = model.NewId()
|
||||
p1.Message = NewTestID()
|
||||
post, err := ss.Post().Save(rctx, &p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create active acknowledgement
|
||||
ack1 := &model.PostAcknowledgement{PostId: post.Id, UserId: userID1, AcknowledgedAt: 0, ChannelId: post.ChannelId}
|
||||
_, err = ss.PostAcknowledgement().SaveWithModel(ack1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create another acknowledgement and soft-delete it
|
||||
ack2 := &model.PostAcknowledgement{PostId: post.Id, UserId: userID2, AcknowledgedAt: 0, ChannelId: post.ChannelId}
|
||||
ack2, err = ss.PostAcknowledgement().SaveWithModel(ack2)
|
||||
require.NoError(t, err)
|
||||
err = ss.PostAcknowledgement().Delete(ack2) // Soft delete sets AcknowledgedAt to 0
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify we have active acknowledgements
|
||||
activeAcks, err := ss.PostAcknowledgement().GetForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, activeAcks, 1)
|
||||
|
||||
// Delete all acknowledgements for the post
|
||||
err = ss.PostAcknowledgement().DeleteAllForPost(post.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all acknowledgements (including soft-deleted) are permanently deleted
|
||||
acksWithDeleted, err := ss.PostAcknowledgement().GetForPostSince(post.Id, 0, "", true)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, acksWithDeleted, "All acknowledgements including soft-deleted should be permanently deleted")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ func TestPostStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|||
t.Run("SetPostReminder", func(t *testing.T) { testSetPostReminder(t, rctx, ss, s) })
|
||||
t.Run("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, rctx, ss, s) })
|
||||
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, rctx, ss, s) })
|
||||
t.Run("DeleteAllPostRemindersForPost", func(t *testing.T) { testDeleteAllPostRemindersForPost(t, rctx, ss, s) })
|
||||
t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, rctx, ss) })
|
||||
t.Run("GetEditHistoryForPost", func(t *testing.T) { testGetEditHistoryForPost(t, rctx, ss) })
|
||||
t.Run("RestoreContentFlaggedPost", func(t *testing.T) { testRestoreContentFlaggedPost(t, rctx, ss) })
|
||||
|
|
@ -6105,3 +6106,192 @@ func testRestoreContentFlaggedPost(t *testing.T, rctx request.CTX, ss store.Stor
|
|||
require.Equal(t, int64(1), thread.ReplyCount)
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteAllPostRemindersForPost(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
||||
t.Run("should permanently delete all reminders for a post", func(t *testing.T) {
|
||||
userID1 := NewTestID()
|
||||
userID2 := NewTestID()
|
||||
userID3 := NewTestID()
|
||||
|
||||
p1 := &model.Post{
|
||||
UserId: userID1,
|
||||
ChannelId: NewTestID(),
|
||||
Message: "test post",
|
||||
Type: model.PostTypeDefault,
|
||||
}
|
||||
p1, err := ss.Post().Save(rctx, p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create multiple reminders for the same post from different users
|
||||
reminder1 := &model.PostReminder{
|
||||
TargetTime: 1000,
|
||||
PostId: p1.Id,
|
||||
UserId: userID1,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder1))
|
||||
|
||||
reminder2 := &model.PostReminder{
|
||||
TargetTime: 2000,
|
||||
PostId: p1.Id,
|
||||
UserId: userID2,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder2))
|
||||
|
||||
reminder3 := &model.PostReminder{
|
||||
TargetTime: 3000,
|
||||
PostId: p1.Id,
|
||||
UserId: userID3,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder3))
|
||||
|
||||
// Verify reminders were created
|
||||
var count int
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, count)
|
||||
|
||||
// Delete all reminders for the post
|
||||
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all reminders were deleted
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
})
|
||||
|
||||
t.Run("should only delete reminders for the specified post", func(t *testing.T) {
|
||||
userID1 := NewTestID()
|
||||
userID2 := NewTestID()
|
||||
|
||||
// Create two posts
|
||||
p1 := &model.Post{
|
||||
UserId: userID1,
|
||||
ChannelId: NewTestID(),
|
||||
Message: "test post 1",
|
||||
Type: model.PostTypeDefault,
|
||||
}
|
||||
p1, err := ss.Post().Save(rctx, p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
p2 := &model.Post{
|
||||
UserId: userID1,
|
||||
ChannelId: NewTestID(),
|
||||
Message: "test post 2",
|
||||
Type: model.PostTypeDefault,
|
||||
}
|
||||
p2, err = ss.Post().Save(rctx, p2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create reminders for both posts
|
||||
reminder1 := &model.PostReminder{
|
||||
TargetTime: 1000,
|
||||
PostId: p1.Id,
|
||||
UserId: userID1,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder1))
|
||||
|
||||
reminder2 := &model.PostReminder{
|
||||
TargetTime: 2000,
|
||||
PostId: p1.Id,
|
||||
UserId: userID2,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder2))
|
||||
|
||||
reminder3 := &model.PostReminder{
|
||||
TargetTime: 3000,
|
||||
PostId: p2.Id,
|
||||
UserId: userID1,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder3))
|
||||
|
||||
reminder4 := &model.PostReminder{
|
||||
TargetTime: 4000,
|
||||
PostId: p2.Id,
|
||||
UserId: userID2,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder4))
|
||||
|
||||
// Delete all reminders for post1 only
|
||||
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify post1 reminders are deleted
|
||||
var count int
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
|
||||
// Verify post2 reminders are still present
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p2.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, count)
|
||||
})
|
||||
|
||||
t.Run("should not error when deleting from post with no reminders", func(t *testing.T) {
|
||||
userID := NewTestID()
|
||||
|
||||
p1 := &model.Post{
|
||||
UserId: userID,
|
||||
ChannelId: NewTestID(),
|
||||
Message: "test post",
|
||||
Type: model.PostTypeDefault,
|
||||
}
|
||||
p1, err := ss.Post().Save(rctx, p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify no reminders exist
|
||||
var count int
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
|
||||
// Delete should not error
|
||||
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should not error when deleting from non-existent post", func(t *testing.T) {
|
||||
nonExistentPostId := model.NewId()
|
||||
|
||||
// Delete should not error even for non-existent post
|
||||
err := ss.Post().DeleteAllPostRemindersForPost(nonExistentPostId)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should handle deleting single reminder for post", func(t *testing.T) {
|
||||
userID := NewTestID()
|
||||
|
||||
p1 := &model.Post{
|
||||
UserId: userID,
|
||||
ChannelId: NewTestID(),
|
||||
Message: "test post",
|
||||
Type: model.PostTypeDefault,
|
||||
}
|
||||
p1, err := ss.Post().Save(rctx, p1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a single reminder
|
||||
reminder := &model.PostReminder{
|
||||
TargetTime: 1000,
|
||||
PostId: p1.Id,
|
||||
UserId: userID,
|
||||
}
|
||||
require.NoError(t, ss.Post().SetPostReminder(reminder))
|
||||
|
||||
// Verify reminder was created
|
||||
var count int
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
// Delete all reminders for the post
|
||||
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify reminder was deleted
|
||||
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3757,6 +3757,17 @@ func (c *Client4) GetFlaggingConfiguration(ctx context.Context) (*ContentFlaggin
|
|||
return DecodeJSONFromResponse[*ContentFlaggingReportingConfig](r)
|
||||
}
|
||||
|
||||
func (c *Client4) GetFlaggingConfigurationForTeam(ctx context.Context, teamId string) (*ContentFlaggingReportingConfig, *Response, error) {
|
||||
values := url.Values{}
|
||||
values.Set("team_id", teamId)
|
||||
r, err := c.doAPIGetWithQuery(ctx, c.contentFlaggingRoute().Join("flag", "config"), values, "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return DecodeJSONFromResponse[*ContentFlaggingReportingConfig](r)
|
||||
}
|
||||
|
||||
func (c *Client4) GetTeamPostFlaggingFeatureStatus(ctx context.Context, teamId string) (map[string]bool, *Response, error) {
|
||||
r, err := c.doAPIGet(ctx, c.contentFlaggingRoute().Join("team", teamId, "status"), "")
|
||||
if err != nil {
|
||||
|
|
@ -3805,6 +3816,26 @@ func (c *Client4) SearchContentFlaggingReviewers(ctx context.Context, teamID, te
|
|||
return DecodeJSONFromResponse[[]*User](r)
|
||||
}
|
||||
|
||||
func (c *Client4) RemoveFlaggedPost(ctx context.Context, postId string, actionRequest *FlagContentActionRequest) (*Response, error) {
|
||||
r, err := c.doAPIPutJSON(ctx, c.contentFlaggingRoute().Join("post", postId, "remove"), actionRequest)
|
||||
if err != nil {
|
||||
return BuildResponse(r), err
|
||||
}
|
||||
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) KeepFlaggedPost(ctx context.Context, postId string, actionRequest *FlagContentActionRequest) (*Response, error) {
|
||||
r, err := c.doAPIPutJSON(ctx, c.contentFlaggingRoute().Join("post", postId, "keep"), actionRequest)
|
||||
if err != nil {
|
||||
return BuildResponse(r), err
|
||||
}
|
||||
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// SearchFiles returns any posts with matching terms string.
|
||||
func (c *Client4) SearchFiles(ctx context.Context, teamId string, terms string, isOrSearch bool) (*FileInfoList, *Response, error) {
|
||||
params := SearchParameter{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import DataSpillageFooter from './data_spillage_footer';
|
||||
|
||||
jest.mock('actions/views/rhs', () => ({
|
||||
selectPostFromRightHandSideSearch: jest.fn((post) => ({
|
||||
type: 'SELECT_POST_FROM_RHS_SEARCH',
|
||||
payload: post,
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockedSelectPostFromRightHandSideSearch = require('actions/views/rhs').selectPostFromRightHandSideSearch as jest.MockedFunction<any>;
|
||||
|
||||
describe('DataSpillageFooter', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render footer with view details button', () => {
|
||||
const post = TestHelper.getPostMock();
|
||||
|
||||
renderWithContext(
|
||||
<DataSpillageFooter post={post}/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('data-spillage-footer')).toBeVisible();
|
||||
expect(screen.getByTestId('data-spillage-action-view-details')).toBeVisible();
|
||||
expect(screen.getByText('View details')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should dispatch selectPostFromRightHandSideSearch when button is clicked', async () => {
|
||||
const post = TestHelper.getPostMock({
|
||||
id: 'test_post_id',
|
||||
message: 'test message',
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<DataSpillageFooter post={post}/>,
|
||||
);
|
||||
|
||||
const viewDetailsButton = screen.getByTestId('data-spillage-action-view-details');
|
||||
await userEvent.click(viewDetailsButton);
|
||||
|
||||
expect(mockedSelectPostFromRightHandSideSearch).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSelectPostFromRightHandSideSearch).toHaveBeenCalledWith(post);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen, waitFor} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import KeepRemoveFlaggedMessageConfirmationModal from './remove_flagged_message_confirmation_modal';
|
||||
|
||||
jest.mock('components/common/hooks/useUser');
|
||||
jest.mock('components/common/hooks/useChannel');
|
||||
jest.mock('components/common/hooks/useContentFlaggingFields');
|
||||
|
||||
const mockedUseUser = require('components/common/hooks/useUser').useUser as jest.MockedFunction<any>;
|
||||
const mockedUseChannel = require('components/common/hooks/useChannel').useChannel as jest.MockedFunction<any>;
|
||||
const mockedUseContentFlaggingConfig = require('components/common/hooks/useContentFlaggingFields').useContentFlaggingConfig as jest.MockedFunction<any>;
|
||||
|
||||
describe('KeepRemoveFlaggedMessageConfirmationModal', () => {
|
||||
const flaggedPostAuthor = TestHelper.getUserMock({
|
||||
id: 'flagged_post_author_id',
|
||||
username: 'flagged_post_author',
|
||||
});
|
||||
|
||||
const reportingUser = TestHelper.getUserMock({
|
||||
id: 'reporting_user_id',
|
||||
username: 'reporting_user',
|
||||
});
|
||||
|
||||
const flaggedPostChannel = TestHelper.getChannelMock({
|
||||
id: 'flagged_post_channel_id',
|
||||
display_name: 'Flagged Post Channel',
|
||||
team_id: 'team_id',
|
||||
});
|
||||
|
||||
const flaggedPost = TestHelper.getPostMock({
|
||||
id: 'flagged_post_id',
|
||||
message: 'Flagged message content',
|
||||
channel_id: flaggedPostChannel.id,
|
||||
user_id: flaggedPostAuthor.id,
|
||||
});
|
||||
|
||||
const defaultContentFlaggingConfig = {
|
||||
reviewer_comment_required: false,
|
||||
notify_reporter_on_removal: false,
|
||||
notify_reporter_on_dismissal: false,
|
||||
};
|
||||
|
||||
const onExited = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockedUseUser.mockReturnValue(flaggedPostAuthor);
|
||||
mockedUseChannel.mockReturnValue(flaggedPostChannel);
|
||||
mockedUseContentFlaggingConfig.mockReturnValue(
|
||||
defaultContentFlaggingConfig,
|
||||
);
|
||||
|
||||
Client4.removeFlaggedPost = jest.fn().mockResolvedValue({});
|
||||
Client4.keepFlaggedPost = jest.fn().mockResolvedValue({});
|
||||
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
describe('remove action', () => {
|
||||
test('should render modal with remove action content', () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('keep-remove-flagged-message-confirmation-modal')).toBeVisible();
|
||||
expect(screen.getByRole('heading', {name: 'Remove message from channel'})).toBeVisible();
|
||||
expect(screen.getByRole('button', {name: 'Remove message'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show notification subtext when notify_reporter_on_removal is true', () => {
|
||||
mockedUseContentFlaggingConfig.mockReturnValue({
|
||||
...defaultContentFlaggingConfig,
|
||||
notify_reporter_on_removal: true,
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const subtext = screen.getByTestId('keep-remove-flagged-message-subtext');
|
||||
expect(subtext).toBeVisible();
|
||||
expect(subtext).toHaveTextContent(/a notification will be sent to the reporter/);
|
||||
});
|
||||
|
||||
test('should show no notification subtext when notify_reporter_on_removal is false', () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const subtext = screen.getByTestId('keep-remove-flagged-message-subtext');
|
||||
expect(subtext).toBeVisible();
|
||||
expect(subtext).toHaveTextContent(/the message will be removed from the channel. This action cannot be reverted./);
|
||||
});
|
||||
|
||||
test('should call Client4.removeFlaggedPost on confirm', async () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const confirmButton = screen.getByRole('button', {name: 'Remove message'});
|
||||
await userEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Client4.removeFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
|
||||
});
|
||||
expect(onExited).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keep action', () => {
|
||||
test('should render modal with keep action content', () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='keep'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('keep-remove-flagged-message-confirmation-modal')).toBeVisible();
|
||||
expect(screen.getByRole('button', {name: 'Keep message'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show notification subtext when notify_reporter_on_dismissal is true', () => {
|
||||
mockedUseContentFlaggingConfig.mockReturnValue({
|
||||
...defaultContentFlaggingConfig,
|
||||
notify_reporter_on_dismissal: true,
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='keep'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const subtext = screen.getByTestId('keep-remove-flagged-message-subtext');
|
||||
expect(subtext).toBeVisible();
|
||||
expect(subtext).toHaveTextContent(/a notification will be sent to the reporter/);
|
||||
});
|
||||
|
||||
test('should show no notification subtext when notify_reporter_on_dismissal is false', () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='keep'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const subtext = screen.getByTestId('keep-remove-flagged-message-subtext');
|
||||
expect(subtext).toBeVisible();
|
||||
expect(subtext).toHaveTextContent(/the message will be visible to all channel members./);
|
||||
});
|
||||
|
||||
test('should call Client4.keepFlaggedPost on confirm', async () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='keep'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const confirmButton = screen.getByRole('button', {name: 'Keep message'});
|
||||
await userEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Client4.keepFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
|
||||
});
|
||||
expect(onExited).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('comment section', () => {
|
||||
test('should show optional comment label when reviewer_comment_required is false', () => {
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const commentTitle = screen.getByTestId('keep-remove-flagged-message-comment-title');
|
||||
expect(commentTitle).toBeVisible();
|
||||
expect(commentTitle).toHaveTextContent('Comment (optional)');
|
||||
});
|
||||
|
||||
test('should show required comment label when reviewer_comment_required is true', () => {
|
||||
mockedUseContentFlaggingConfig.mockReturnValue({
|
||||
...defaultContentFlaggingConfig,
|
||||
reviewer_comment_required: true,
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const commentTitle = screen.getByTestId('keep-remove-flagged-message-comment-title');
|
||||
expect(commentTitle).toBeVisible();
|
||||
expect(commentTitle).toHaveTextContent('Comment (required)');
|
||||
});
|
||||
|
||||
test('should show validation error when comment is required but empty on confirm', async () => {
|
||||
mockedUseContentFlaggingConfig.mockReturnValue({
|
||||
...defaultContentFlaggingConfig,
|
||||
reviewer_comment_required: true,
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const confirmButton = screen.getByRole('button', {name: 'Remove message'});
|
||||
await userEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please add a comment.')).toBeVisible();
|
||||
});
|
||||
expect(Client4.removeFlaggedPost).not.toHaveBeenCalled();
|
||||
expect(onExited).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
test('should show request error when API call fails', async () => {
|
||||
const errorMessage = 'Failed to remove flagged post';
|
||||
Client4.removeFlaggedPost = jest.fn().mockRejectedValue({message: errorMessage});
|
||||
|
||||
renderWithContext(
|
||||
<KeepRemoveFlaggedMessageConfirmationModal
|
||||
action='remove'
|
||||
onExited={onExited}
|
||||
flaggedPost={flaggedPost}
|
||||
reportingUser={reportingUser}
|
||||
/>,
|
||||
);
|
||||
|
||||
const confirmButton = screen.getByRole('button', {name: 'Remove message'});
|
||||
await userEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const errorElement = screen.getByTestId(
|
||||
'keep-remove-flagged-message-request-error',
|
||||
);
|
||||
expect(errorElement).toBeVisible();
|
||||
expect(errorElement).toHaveTextContent(errorMessage);
|
||||
});
|
||||
expect(onExited).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -182,16 +182,20 @@ export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExi
|
|||
isConfirmDisabled={submitting}
|
||||
>
|
||||
<div className='body'>
|
||||
<div className='section'>
|
||||
<div
|
||||
className='section'
|
||||
data-testid='keep-remove-flagged-message-body'
|
||||
>
|
||||
{body}
|
||||
<br/>
|
||||
<br/>
|
||||
{subtext}
|
||||
<span data-testid='keep-remove-flagged-message-subtext'>{subtext}</span>
|
||||
</div>
|
||||
|
||||
<div className='section comment_section'>
|
||||
<div
|
||||
className='section_title'
|
||||
data-testid='keep-remove-flagged-message-comment-title'
|
||||
>
|
||||
{contentFlaggingConfig?.reviewer_comment_required ? requiredCommentSectionTitle : optionalCommentSectionTitle}
|
||||
</div>
|
||||
|
|
@ -212,7 +216,10 @@ export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExi
|
|||
/>
|
||||
</div>
|
||||
{requestError &&
|
||||
<div className='request_error'>
|
||||
<div
|
||||
className='request_error'
|
||||
data-testid='keep-remove-flagged-message-request-error'
|
||||
>
|
||||
<i className='icon icon-alert-outline'/>
|
||||
<span>{requestError}</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue