mattermost/server/channels/app/content_flagging_test.go
Jesse Hallam e3fbf8711f
MM-68149: Upgrade to Go 1.26.2 (#36418)
* MM-68149: upgrade to Go 1.26.2

Update go directive in go.mod and .go-version.

* MM-68149: replace pointer helpers with Go 1.26 new()

Go 1.26 extends the built-in new() to accept an initial value expression,
making typed-pointer helpers like model.NewPointer(x), bToP(x), and boolPtr(x)
redundant. Replace every call site with new(x) and remove the now-unused
helper functions and their //go:fix inline directives.

* MM-68149: apply go fix for reflect API and format-string changes

- reflect.Ptr → reflect.Pointer (renamed in Go 1.18, deprecated alias removed in 1.26)
- reflect range-over-struct: for i := 0; i < t.NumField(); i++ → for field := range t.Fields()
  and the equivalent for Methods() and interface types
- Fix format-string concatenation and variadic-arg mismatches flagged by go vet

* MM-68149: update JPEG fixtures and test infrastructure for Go 1.26 encoder

Go 1.26 ships a new image/jpeg encoder that produces slightly different output.
Regenerate all JPEG fixture files and switch the comparison helpers from
byte-equality to pixel-level comparison with a small per-channel tolerance,
so minor encoder drift across patch versions is handled automatically.

Add -update-fixtures flag to make it easy to regenerate fixtures after future
major Go upgrades. Document the update procedure in tests/README.md.

* MM-68149: CI check that go fix ./... produces no changes

* Fix real bugs flagged by CodeRabbit review

- group.go: set newGroup.MemberCount not group.MemberCount (member count
  was populated on the wrong variable and lost before publish/return)
- file_test.go: guard compareImage(GetFilePreview) on the preview slice
  length, not the thumbnail slice length (copy-paste error)
- config_test.go: remove duplicate MinimumLength assignment

* fixup! Fix real bugs flagged by CodeRabbit review
2026-05-12 15:59:12 +00:00

3331 lines
118 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/require"
)
func getBaseConfig(th *TestHelper) model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.ReviewerSettings.TeamAdminsAsReviewers = new(false)
config.ReviewerSettings.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.AdditionalSettings.ReporterCommentRequired = new(false)
config.AdditionalSettings.HideFlaggedContent = new(false)
config.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return config
}
func setBaseConfig(th *TestHelper) *model.AppError {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig(th))
if appErr != nil {
return appErr
}
return nil
}
func searchPropertyValue(t *testing.T, th *TestHelper, postId, fieldName string) []*model.PropertyValue {
t.Helper()
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
values, appErr2 := th.App.SearchPropertyValues(th.Context, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{postId},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[fieldName].ID,
})
require.Nil(t, appErr2)
return values
}
func setupFlaggedPost(t *testing.T, th *TestHelper) *model.Post {
post := th.CreatePost(t, th.BasicChannel)
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)
time.Sleep(2 * time.Second)
return post
}
func TestContentFlaggingEnabledForTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("should return true for common reviewers", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
CommonReviewerIds: []string{"reviewer_user_id_1", "reviewer_user_id_2"},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
status, appErr := th.App.ContentFlaggingEnabledForTeam("team1")
require.Nil(t, appErr)
require.True(t, status, "expected team post reporting feature to be enabled for common reviewers")
})
t.Run("should return true when configured for specified team", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
"team1": {
Enabled: new(true),
ReviewerIds: []string{"reviewer_user_id_1"},
},
},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
status, appErr := th.App.ContentFlaggingEnabledForTeam("team1")
require.Nil(t, appErr)
require.True(t, status, "expected team post reporting feature to be disabled for team without reviewers")
})
t.Run("should return true when using Additional Reviewers", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(false),
TeamAdminsAsReviewers: new(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
"team1": {
Enabled: new(true),
},
},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
status, appErr := th.App.ContentFlaggingEnabledForTeam("team1")
require.Nil(t, appErr)
require.True(t, status)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr = th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
status, appErr = th.App.ContentFlaggingEnabledForTeam("team1")
require.Nil(t, appErr)
require.True(t, status)
config.ReviewerSettings.TeamAdminsAsReviewers = new(true)
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr = th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
status, appErr = th.App.ContentFlaggingEnabledForTeam("team1")
require.Nil(t, appErr)
require.True(t, status)
})
t.Run("should return true for default state", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
status, appErr := th.App.ContentFlaggingEnabledForTeam("team1")
require.Nil(t, appErr)
require.True(t, status, "expected team post reporting feature to be enabled for common reviewers")
})
}
func TestAssignFlaggedPostReviewer(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
t.Run("should successfully assign reviewer to pending flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was created
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should successfully reassign reviewer to already assigned flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
// First assignment
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Second assignment (reassignment)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser2.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status remains assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was updated
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should fail when trying to assign reviewer to non-flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(t, th.BasicChannel)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should handle assignment with same reviewer ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
// Assign reviewer
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Assign same reviewer again
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status remains assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property still exists with correct value
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should handle assignment with empty reviewer ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, "", th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was created with empty value
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `""`, string(reviewerValues[0].Value))
})
t.Run("should handle assignment with invalid post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
appErr := th.App.AssignFlaggedPostReviewer(th.Context, "invalid_post_id", th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should allow assigning reviewer at all stages", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
// Set the status to Assigned
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusAssigned))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Set the status to Removed
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Set the status to Retained
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
})
}
func TestSaveContentFlaggingConfig(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should save content flagging config successfully", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
EnableContentFlagging: new(true),
AdditionalSettings: &model.AdditionalContentFlaggingSettings{
ReporterCommentRequired: new(true),
HideFlaggedContent: new(false),
Reasons: &[]string{"spam", "harassment", "inappropriate"},
},
},
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(true),
SystemAdminsAsReviewers: new(true),
TeamAdminsAsReviewers: new(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
CommonReviewerIds: []string{th.BasicUser.Id, th.BasicUser2.Id},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Verify system config was updated
savedConfig := th.App.Config()
require.Equal(t, *config.EnableContentFlagging, *savedConfig.ContentFlaggingSettings.EnableContentFlagging)
require.Equal(t, *config.ReviewerSettings.CommonReviewers, *savedConfig.ContentFlaggingSettings.ReviewerSettings.CommonReviewers)
require.Equal(t, *config.ReviewerSettings.SystemAdminsAsReviewers, *savedConfig.ContentFlaggingSettings.ReviewerSettings.SystemAdminsAsReviewers)
require.Equal(t, *config.ReviewerSettings.TeamAdminsAsReviewers, *savedConfig.ContentFlaggingSettings.ReviewerSettings.TeamAdminsAsReviewers)
require.Equal(t, *config.AdditionalSettings.ReporterCommentRequired, *savedConfig.ContentFlaggingSettings.AdditionalSettings.ReporterCommentRequired)
require.Equal(t, *config.AdditionalSettings.HideFlaggedContent, *savedConfig.ContentFlaggingSettings.AdditionalSettings.HideFlaggedContent)
require.Equal(t, *config.AdditionalSettings.Reasons, *savedConfig.ContentFlaggingSettings.AdditionalSettings.Reasons)
// Verify reviewer IDs were saved separately
reviewerIDs, appErr := th.App.GetContentFlaggingConfigReviewerIDs()
require.Nil(t, appErr)
require.Equal(t, config.ReviewerSettings.CommonReviewerIds, reviewerIDs.CommonReviewerIds)
})
t.Run("should save config with team reviewers", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
EnableContentFlagging: new(true),
},
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(false),
SystemAdminsAsReviewers: new(false),
TeamAdminsAsReviewers: new(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser.Id},
},
},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Verify team reviewers were saved
reviewerIDs, appErr := th.App.GetContentFlaggingConfigReviewerIDs()
require.Nil(t, appErr)
require.NotNil(t, reviewerIDs.TeamReviewersSetting)
teamSettings := (reviewerIDs.TeamReviewersSetting)[th.BasicTeam.Id]
require.True(t, *teamSettings.Enabled)
require.Equal(t, []string{th.BasicUser.Id}, teamSettings.ReviewerIds)
})
t.Run("should handle empty config", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Verify defaults were applied
savedConfig := th.App.Config()
require.NotNil(t, savedConfig.ContentFlaggingSettings.EnableContentFlagging)
require.NotNil(t, savedConfig.ContentFlaggingSettings.ReviewerSettings.CommonReviewers)
})
}
func TestGetContentFlaggingConfigReviewerIDs(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should return reviewer IDs after saving config", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
CommonReviewerIds: []string{th.BasicUser.Id, th.BasicUser2.Id},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
reviewerIDs, appErr := th.App.GetContentFlaggingConfigReviewerIDs()
require.Nil(t, appErr)
require.NotNil(t, reviewerIDs)
require.Equal(t, []string{th.BasicUser.Id, th.BasicUser2.Id}, reviewerIDs.CommonReviewerIds)
})
t.Run("should return team reviewer settings", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: new(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser.Id},
},
"team2": {
Enabled: new(false),
ReviewerIds: []string{th.BasicUser2.Id},
},
},
},
},
}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
reviewerIDs, appErr := th.App.GetContentFlaggingConfigReviewerIDs()
require.Nil(t, appErr)
require.NotNil(t, reviewerIDs.TeamReviewersSetting)
teamSettings := reviewerIDs.TeamReviewersSetting
require.Len(t, teamSettings, 2)
// Check first team
team1Settings := teamSettings[th.BasicTeam.Id]
require.True(t, *team1Settings.Enabled)
require.Equal(t, []string{th.BasicUser.Id}, team1Settings.ReviewerIds)
// Check second team
team2Settings := teamSettings["team2"]
require.False(t, *team2Settings.Enabled)
require.Equal(t, []string{th.BasicUser2.Id}, team2Settings.ReviewerIds)
})
t.Run("should return empty settings when no config saved", func(t *testing.T) {
// Clear any existing config by saving empty config
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
reviewerIDs, appErr := th.App.GetContentFlaggingConfigReviewerIDs()
require.Nil(t, appErr)
require.NotNil(t, reviewerIDs)
// Should have default empty values
if reviewerIDs.CommonReviewerIds != nil {
require.Empty(t, reviewerIDs.CommonReviewerIds)
}
if reviewerIDs.TeamReviewersSetting != nil {
require.Empty(t, reviewerIDs.TeamReviewersSetting)
}
})
}
func TestGetContentReviewChannels(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
TeamAdminsAsReviewers: new(true),
SystemAdminsAsReviewers: new(true),
CommonReviewers: new(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
CommonReviewerIds: []string{th.BasicUser.Id, th.BasicUser2.Id},
},
},
}
config.SetDefaults()
return config
}
t.Run("should return channels for common reviewers", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
require.NotNil(t, contentReviewBot)
channels, appErr := th.App.getContentReviewChannels(th.Context, th.BasicTeam.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
require.Len(t, channels, 2)
for _, channel := range channels {
require.Equal(t, model.ChannelTypeDirect, channel.Type)
otherUserId := channel.GetOtherUserIdForDM(contentReviewBot.UserId)
require.True(t, otherUserId == th.BasicUser.Id || otherUserId == th.BasicUser2.Id)
}
})
t.Run("should return channels for system admins as additional reviewers", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Sysadmin explicitly need to be a team member to be returned as reviewer
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
defer func() {
_ = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
}()
require.Nil(t, appErr)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
require.NotNil(t, contentReviewBot)
channels, appErr := th.App.getContentReviewChannels(th.Context, th.BasicTeam.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
require.Len(t, channels, 3)
require.Equal(t, model.ChannelTypeDirect, channels[0].Type)
require.Equal(t, model.ChannelTypeDirect, channels[1].Type)
require.Equal(t, model.ChannelTypeDirect, channels[2].Type)
reviewerIds := []string{
channels[0].GetOtherUserIdForDM(contentReviewBot.UserId),
channels[1].GetOtherUserIdForDM(contentReviewBot.UserId),
channels[2].GetOtherUserIdForDM(contentReviewBot.UserId),
}
require.Contains(t, reviewerIds, th.BasicUser.Id)
require.Contains(t, reviewerIds, th.BasicUser2.Id)
require.Contains(t, reviewerIds, th.SystemAdminUser.Id)
})
t.Run("should return channels for team admins as additional reviewers", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.TeamAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Create a new user and make them team admin
teamAdmin := th.CreateUser(t)
defer func() {
_ = th.App.PermanentDeleteUser(th.Context, teamAdmin)
}()
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, teamAdmin.Id, "")
require.Nil(t, appErr)
_, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, teamAdmin.Id, model.TeamUserRoleId+" "+model.TeamAdminRoleId)
require.Nil(t, appErr)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
require.NotNil(t, contentReviewBot)
channels, appErr := th.App.getContentReviewChannels(th.Context, th.BasicTeam.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
require.Len(t, channels, 3)
require.Equal(t, model.ChannelTypeDirect, channels[0].Type)
require.Equal(t, model.ChannelTypeDirect, channels[1].Type)
require.Equal(t, model.ChannelTypeDirect, channels[2].Type)
reviewerIds := []string{
channels[0].GetOtherUserIdForDM(contentReviewBot.UserId),
channels[1].GetOtherUserIdForDM(contentReviewBot.UserId),
channels[2].GetOtherUserIdForDM(contentReviewBot.UserId),
}
require.Contains(t, reviewerIds, th.BasicUser.Id)
require.Contains(t, reviewerIds, th.BasicUser2.Id)
require.Contains(t, reviewerIds, teamAdmin.Id)
})
t.Run("should return channels for team reviewers", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser2.Id},
},
}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
require.NotNil(t, contentReviewBot)
channels, appErr := th.App.getContentReviewChannels(th.Context, th.BasicTeam.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
require.Len(t, channels, 1)
require.Equal(t, model.ChannelTypeDirect, channels[0].Type)
otherUserId := channels[0].GetOtherUserIdForDM(contentReviewBot.UserId)
require.Equal(t, th.BasicUser2.Id, otherUserId)
})
t.Run("should not return channels for team reviewers when disabled for the team", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(false),
ReviewerIds: []string{th.BasicUser.Id},
},
}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
require.NotNil(t, contentReviewBot)
channels, appErr := th.App.getContentReviewChannels(th.Context, th.BasicTeam.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
require.Len(t, channels, 0)
})
t.Run("should return channels for additional reviewers with team reviewers", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamAdminsAsReviewers = new(true)
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser2.Id},
},
}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
defer func() {
_ = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
}()
require.Nil(t, appErr)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
require.NotNil(t, contentReviewBot)
channels, appErr := th.App.getContentReviewChannels(th.Context, th.BasicTeam.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
require.Len(t, channels, 2)
reviewerIds := []string{
channels[0].GetOtherUserIdForDM(contentReviewBot.UserId),
channels[1].GetOtherUserIdForDM(contentReviewBot.UserId),
}
require.Contains(t, reviewerIds, th.BasicUser2.Id)
require.Contains(t, reviewerIds, th.SystemAdminUser.Id)
})
}
func TestGetReviewersForTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should return common reviewers", func(t *testing.T) {
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 2)
require.Contains(t, reviewers, th.BasicUser.Id)
require.Contains(t, reviewers, th.BasicUser2.Id)
})
t.Run("should return system admins as additional reviewers", func(t *testing.T) {
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
// Sysadmin explicitly need to be a team member to be returned as reviewer
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 2)
require.Contains(t, reviewers, th.BasicUser.Id)
require.Contains(t, reviewers, th.SystemAdminUser.Id)
config.ReviewerSettings.CommonReviewerIds = []string{}
appErr = th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
reviewers, appErr = th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Contains(t, reviewers, th.SystemAdminUser.Id)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
appErr = th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
// If sysadmin is not a team member, they should not be returned as a reviewer
appErr = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
reviewers, appErr = th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Contains(t, reviewers, th.BasicUser.Id)
})
t.Run("should return team admins as additional reviewers", func(t *testing.T) {
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
// Create a new user and make them team admin
teamAdmin := th.CreateUser(t)
defer func() {
_ = th.App.PermanentDeleteUser(th.Context, teamAdmin)
}()
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, teamAdmin.Id, "")
require.Nil(t, appErr)
_, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, teamAdmin.Id, model.TeamUserRoleId+" "+model.TeamAdminRoleId)
require.Nil(t, appErr)
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 2)
require.Contains(t, reviewers, th.BasicUser.Id)
require.Contains(t, reviewers, teamAdmin.Id)
config.ReviewerSettings.CommonReviewerIds = []string{}
appErr = th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
reviewers, appErr = th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Contains(t, reviewers, teamAdmin.Id)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
appErr = th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
// If team admin is not a team member, they should not be returned as a reviewer
appErr = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, teamAdmin.Id, "")
require.Nil(t, appErr)
reviewers, appErr = th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Contains(t, reviewers, th.BasicUser.Id)
})
t.Run("should return team reviewers", func(t *testing.T) {
team2 := th.CreateTeam(t)
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser2.Id},
},
}
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
// Reviewers configured for th.BasicTeam
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Contains(t, reviewers, th.BasicUser2.Id)
// NO reviewers configured for team2
reviewers, appErr = th.App.getReviewersForTeam(team2.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 0)
})
t.Run("should not return reviewers when disabled for the team", func(t *testing.T) {
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(false),
ReviewerIds: []string{th.BasicUser.Id},
},
}
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 0)
})
t.Run("should return additional reviewers with team reviewers", func(t *testing.T) {
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser2.Id},
},
}
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 2)
require.Contains(t, reviewers, th.BasicUser2.Id)
require.Contains(t, reviewers, th.SystemAdminUser.Id)
})
t.Run("should return unique reviewers", func(t *testing.T) {
config := &model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.SystemAdminUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(*config)
require.Nil(t, appErr)
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
reviewers, appErr := th.App.getReviewersForTeam(th.BasicTeam.Id, true)
require.Nil(t, appErr)
require.Len(t, reviewers, 2)
require.Contains(t, reviewers, th.BasicUser.Id)
require.Contains(t, reviewers, th.SystemAdminUser.Id)
})
}
func TestCanFlagPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
t.Run("should be able to flag post which has not already been flagged", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
appErr := th.App.canFlagPost(groupId, post.Id, "en")
require.Nil(t, appErr)
})
t.Run("should not be able to flag post which has already been flagged", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusField, err := th.App.GetPropertyFieldByName(rctx, groupId, "", ContentFlaggingPropertyNameStatus)
require.Nil(t, err)
propertyValue, err := th.App.CreatePropertyValue(rctx, &model.PropertyValue{
TargetID: post.Id,
GroupID: groupId,
FieldID: statusField.ID,
TargetType: "post",
Value: json.RawMessage(`"` + model.ContentFlaggingStatusPending + `"`),
})
require.Nil(t, err)
// Can't fleg when post already flagged in pending status
appErr := th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
require.Equal(t, "Cannot quarantine this post as it is already quarantined for review.", appErr.Id)
// Can't fleg when post already flagged in assigned status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusAssigned + `"`)
_, err = th.App.UpdatePropertyValue(rctx, groupId, propertyValue)
require.Nil(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
// Can't fleg when post already flagged in retained status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusRetained + `"`)
_, err = th.App.UpdatePropertyValue(rctx, groupId, propertyValue)
require.Nil(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
// Can't fleg when post already flagged in removed status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusRemoved + `"`)
_, err = th.App.UpdatePropertyValue(rctx, groupId, propertyValue)
require.Nil(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
})
}
func TestFlagPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
cfg := model.ContentFlaggingSettingsRequest{}
cfg.SetDefaults()
cfg.ReviewerSettings.CommonReviewers = new(true)
cfg.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
cfg.AdditionalSettings.ReporterCommentRequired = new(false)
cfg.AdditionalSettings.HideFlaggedContent = new(false)
cfg.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return cfg
}
t.Run("should successfully flag a post with valid data", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
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)
// Verify property values were created
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check status property
statusValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[ContentFlaggingPropertyNameStatus].ID,
})
require.Nil(t, err)
require.Len(t, statusValues, 1)
require.Equal(t, `"`+model.ContentFlaggingStatusPending+`"`, string(statusValues[0].Value))
// Check reporting user property
userValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingUserID].ID,
})
require.Nil(t, err)
require.Len(t, userValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(userValues[0].Value))
// Check reason property
reasonValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingReason].ID,
})
require.Nil(t, err)
require.Len(t, reasonValues, 1)
require.Equal(t, `"spam"`, string(reasonValues[0].Value))
// Check comment property
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `"This is spam content"`, string(commentValues[0].Value))
})
t.Run("should fail with invalid reason", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "invalid_reason",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.reason_invalid", appErr.Id)
})
t.Run("should fail when comment is required but not provided", func(t *testing.T) {
config := getBaseConfig()
config.AdditionalSettings.ReporterCommentRequired = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.NotNil(t, appErr)
})
t.Run("should fail when trying to flag already flagged post", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "\"This is spam content\"",
}
// Flag the post first time
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
// Try to flag the same post again
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.NotNil(t, appErr)
require.Equal(t, "Cannot quarantine this post as it is already quarantined for review.", appErr.Id)
})
t.Run("should hide flagged content when configured", func(t *testing.T) {
config := getBaseConfig()
config.AdditionalSettings.HideFlaggedContent = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
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)
// Verify post was deleted
deletedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
require.NotNil(t, appErr)
require.Nil(t, deletedPost)
})
t.Run("should create content review post for reviewers", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "harassment",
Comment: "\"This is harassment\"",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
// The reviewer posts are created async in a go routine. Wait for a short time to allow it to complete.
// 2 seconds is the minimum time when the test consistently passes locally and in CI.
time.Sleep(5 * time.Second)
// Get the content review bot
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Get direct channel between reviewer and bot
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
// Check if review post was created in the DM channel
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
require.NotEmpty(t, posts.Posts)
// Find the content review post
var reviewPost *model.Post
for _, p := range posts.Posts {
if p.Type == "custom_spillage_report" {
reviewPost = p
break
}
}
require.NotNil(t, reviewPost)
})
t.Run("should work with empty comment when not required", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "inappropriate",
Comment: "",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
// Verify property values were created with empty comment
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `""`, string(commentValues[0].Value))
})
t.Run("should set reporting time property", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "\"Test comment\"",
}
beforeTime := model.GetMillis()
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
afterTime := model.GetMillis()
require.Nil(t, appErr)
// Verify reporting time property was set
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
timeValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingTime].ID,
})
require.Nil(t, err)
require.Len(t, timeValues, 1)
var reportingTime int64
unmarshalErr := json.Unmarshal(timeValues[0].Value, &reportingTime)
require.NoError(t, unmarshalErr)
require.True(t, reportingTime >= beforeTime && reportingTime <= afterTime)
})
}
func TestSearchReviewers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
return config
}
allProfilesSanitized := func(reviewers []*model.User) {
for _, reviewer := range reviewers {
require.Empty(t, reviewer.LastPasswordUpdate)
require.Empty(t, reviewer.NotifyProps)
}
}
t.Run("should return common reviewers when searching", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Search for users by username
reviewers, appErr := th.App.SearchReviewers(th.Context, th.BasicUser.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Equal(t, th.BasicUser.Id, reviewers[0].Id)
// Search for users by partial username
reviewers, appErr = th.App.SearchReviewers(th.Context, th.BasicUser.Username[:3], th.BasicTeam.Id)
require.Nil(t, appErr)
require.True(t, len(reviewers) >= 1)
allProfilesSanitized(reviewers)
// Verify the basic user is in the results
found := false
for _, reviewer := range reviewers {
if reviewer.Id == th.BasicUser.Id {
found = true
break
}
}
require.True(t, found)
})
t.Run("should return team reviewers when common reviewers disabled", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser2.Id},
},
}
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Search for team reviewer
reviewers, appErr := th.App.SearchReviewers(th.Context, th.BasicUser2.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Equal(t, th.BasicUser2.Id, reviewers[0].Id)
allProfilesSanitized(reviewers)
// Search should not return users not configured as team reviewers
reviewers, appErr = th.App.SearchReviewers(th.Context, th.BasicUser.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 0)
})
t.Run("should return system admins as additional reviewers", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Add system admin to team
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
defer func() {
_ = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
}()
// Search for system admin
reviewers, appErr := th.App.SearchReviewers(th.Context, th.SystemAdminUser.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Equal(t, th.SystemAdminUser.Id, reviewers[0].Id)
allProfilesSanitized(reviewers)
})
t.Run("should return team admins as additional reviewers", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Create a new user and make them team admin
teamAdmin := th.CreateUser(t)
defer func() {
_ = th.App.PermanentDeleteUser(th.Context, teamAdmin)
}()
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, teamAdmin.Id, "")
require.Nil(t, appErr)
_, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, teamAdmin.Id, model.TeamUserRoleId+" "+model.TeamAdminRoleId)
require.Nil(t, appErr)
// Search for team admin
reviewers, appErr := th.App.SearchReviewers(th.Context, teamAdmin.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Equal(t, teamAdmin.Id, reviewers[0].Id)
allProfilesSanitized(reviewers)
})
t.Run("should return combined reviewers from multiple sources", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamAdminsAsReviewers = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Add system admin to team
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
defer func() {
_ = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
}()
// Create a team admin
teamAdmin := th.CreateUser(t)
defer func() {
_ = th.App.PermanentDeleteUser(th.Context, teamAdmin)
}()
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, teamAdmin.Id, "")
require.Nil(t, appErr)
_, appErr = th.App.UpdateTeamMemberRoles(th.Context, th.BasicTeam.Id, teamAdmin.Id, model.TeamUserRoleId+" "+model.TeamAdminRoleId)
require.Nil(t, appErr)
// Search with empty term should return all reviewers
reviewers, appErr := th.App.SearchReviewers(th.Context, "", th.BasicTeam.Id)
require.Nil(t, appErr)
require.Equal(t, 3, len(reviewers))
allProfilesSanitized(reviewers)
// Verify all expected reviewers are present
reviewerIds := make([]string, len(reviewers))
for i, reviewer := range reviewers {
reviewerIds[i] = reviewer.Id
}
require.Contains(t, reviewerIds, th.BasicUser.Id)
require.Contains(t, reviewerIds, th.SystemAdminUser.Id)
require.Contains(t, reviewerIds, teamAdmin.Id)
})
t.Run("should deduplicate reviewers from multiple sources", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.SystemAdminUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Add system admin to team
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
defer func() {
_ = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
}()
// Search for system admin (who is both common reviewer and system admin)
reviewers, appErr := th.App.SearchReviewers(th.Context, th.SystemAdminUser.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 1)
require.Equal(t, th.SystemAdminUser.Id, reviewers[0].Id)
allProfilesSanitized(reviewers)
})
t.Run("should return empty results when no reviewers match search term", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Search for non-existent user
reviewers, appErr := th.App.SearchReviewers(th.Context, "nonexistentuser", th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 0)
allProfilesSanitized(reviewers)
})
t.Run("should return empty results when no reviewers configured", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Search should return no results
reviewers, appErr := th.App.SearchReviewers(th.Context, th.BasicUser.Username, th.BasicTeam.Id)
require.Nil(t, appErr)
require.Len(t, reviewers, 0)
allProfilesSanitized(reviewers)
})
t.Run("should work with team reviewers and additional reviewers combined", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: new(true),
ReviewerIds: []string{th.BasicUser.Id},
},
}
config.ReviewerSettings.SystemAdminsAsReviewers = new(true)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Add system admin to team
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
require.Nil(t, appErr)
defer func() {
_ = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, th.SystemAdminUser.Id, "")
}()
// Search with empty term should return both team reviewer and system admin
reviewers, appErr := th.App.SearchReviewers(th.Context, "", th.BasicTeam.Id)
require.Nil(t, appErr)
require.True(t, len(reviewers) >= 2)
allProfilesSanitized(reviewers)
reviewerIds := make([]string, len(reviewers))
for i, reviewer := range reviewers {
reviewerIds[i] = reviewer.Id
}
require.Contains(t, reviewerIds, th.BasicUser.Id)
require.Contains(t, reviewerIds, th.SystemAdminUser.Id)
})
t.Run("should handle search by email and full name", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = new(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = new(false)
config.ReviewerSettings.TeamAdminsAsReviewers = new(false)
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
// Search by first name (if the user has one set)
if th.BasicUser.FirstName != "" {
reviewers, appErr := th.App.SearchReviewers(th.Context, th.BasicUser.FirstName, th.BasicTeam.Id)
require.Nil(t, appErr)
require.True(t, len(reviewers) >= 1)
found := false
for _, reviewer := range reviewers {
if reviewer.Id == th.BasicUser.Id {
found = true
break
}
}
require.True(t, found)
}
})
}
func TestGetReviewerPostsForFlaggedPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should return reviewer posts for flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 1)
// Verify the reviewer post exists and has the correct properties
reviewerPost, appErr := th.App.GetSinglePost(th.Context, reviewerPostIds[0], false)
require.Nil(t, appErr)
require.Equal(t, model.ContentFlaggingPostType, reviewerPost.Type)
require.Contains(t, reviewerPost.GetProps(), POST_PROP_KEY_FLAGGED_POST_ID)
require.Equal(t, post.Id, reviewerPost.GetProps()[POST_PROP_KEY_FLAGGED_POST_ID])
})
t.Run("should return empty list when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 0)
})
t.Run("should handle multiple reviewer posts for same flagged post", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.SystemAdminUser.Id, flagData)
require.Nil(t, appErr)
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 2)
// Verify both reviewer posts exist and have correct properties
for _, postId := range reviewerPostIds {
reviewerPost, appErr := th.App.GetSinglePost(th.Context, postId, false)
require.Nil(t, appErr)
require.Equal(t, model.ContentFlaggingPostType, reviewerPost.Type)
require.Contains(t, reviewerPost.GetProps(), POST_PROP_KEY_FLAGGED_POST_ID)
require.Equal(t, post.Id, reviewerPost.GetProps()[POST_PROP_KEY_FLAGGED_POST_ID])
}
})
t.Run("should handle invalid flagged post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, "invalid_post_id", flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 0)
})
}
func TestPostReviewerMessage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should post reviewer message to thread", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test reviewer message"
_, appErr := th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id, nil, "")
require.Nil(t, appErr)
// Verify message was posted to the reviewer thread
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Find the original review post and the test message
var reviewPost *model.Post
var testMessagePost *model.Post
for _, p := range posts.Posts {
if p.Type == "custom_spillage_report" {
reviewPost = p
} else if p.RootId != "" && p.Message == testMessage {
testMessagePost = p
}
}
require.NotNil(t, reviewPost)
require.NotNil(t, testMessagePost)
require.Equal(t, reviewPost.Id, testMessagePost.RootId)
require.Equal(t, contentReviewBot.UserId, testMessagePost.UserId)
})
t.Run("should handle multiple reviewer channels", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.SystemAdminUser.Id, flagData)
require.Nil(t, appErr)
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test message for multiple reviewers"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id, nil, "")
require.Nil(t, appErr)
// Verify message was posted to both reviewer threads
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Check first reviewer's channel
dmChannel1, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts1, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel1.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Check second reviewer's channel
dmChannel2, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser2.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts2, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel2.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Verify test message exists in both channels
var testMessagePost1, testMessagePost2 *model.Post
for _, p := range posts1.Posts {
if p.RootId != "" && p.Message == testMessage {
testMessagePost1 = p
break
}
}
for _, p := range posts2.Posts {
if p.RootId != "" && p.Message == testMessage {
testMessagePost2 = p
break
}
}
require.NotNil(t, testMessagePost1)
require.NotNil(t, testMessagePost2)
require.Equal(t, contentReviewBot.UserId, testMessagePost1.UserId)
require.Equal(t, contentReviewBot.UserId, testMessagePost2.UserId)
})
t.Run("should post message from report with file attachment", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
report := &model.PostDeletionReport{
PostID: post.Id,
Timestamp: time.Now(),
}
report.AddStep("app.data_spillage.report.step.post_content", model.StepSuccess, "app.data_spillage.report.detail.cleared", nil)
report.AddStep("app.data_spillage.report.step.file_attachments", model.StepFailed, "app.data_spillage.report.detail.failed", []string{"file not found"})
reportFileName := "deletion_report.md"
createdPosts, appErr := th.App.postReviewerMessage(th.Context, "", groupId, post.Id, report, reportFileName)
require.Nil(t, appErr)
require.NotEmpty(t, createdPosts)
// Verify the message content is derived from report.RenderSummary, not the passed-in message string
createdPost := createdPosts[0]
require.NotEmpty(t, createdPost.Message)
// Verify it contains summary table markers that RenderSummary produces
require.Contains(t, createdPost.Message, "📊")
// Verify file attachment was created
require.NotEmpty(t, createdPost.FileIds, "expected a file attachment from the report")
// Verify the file info exists and has the correct name
fileInfo, appErr := th.App.GetFileInfo(th.Context, createdPost.FileIds[0])
require.Nil(t, appErr)
require.Equal(t, reportFileName, fileInfo.Name)
})
t.Run("should handle case when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test message for non-flagged post"
_, appErr := th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id, nil, "")
require.Nil(t, appErr)
})
t.Run("should handle message with special characters", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test message with special chars: @user #channel ~team & <script>alert('xss')</script>"
_, appErr := th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id, nil, "")
require.Nil(t, appErr)
// Verify message was posted correctly with special characters preserved
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Find the test message
var testMessagePost *model.Post
for _, p := range posts.Posts {
if p.RootId != "" && p.Message == testMessage {
testMessagePost = p
break
}
}
require.NotNil(t, testMessagePost)
require.Equal(t, testMessage, testMessagePost.Message)
})
}
// Helper function to setup notification config for testing
func setupNotificationConfig(th *TestHelper, eventTargetMapping map[model.ContentFlaggingEvent][]model.NotificationTarget) *model.AppError {
config := getBaseConfig(th)
config.NotificationSettings = &model.ContentFlaggingNotificationSettings{
EventTargetMapping: eventTargetMapping,
}
return th.App.SaveContentFlaggingConfig(config)
}
// Helper function to verify post message content and properties
func verifyNotificationPost(t *testing.T, post *model.Post, expectedMessage string, expectedUserId string, expectedChannelId string) {
require.NotNil(t, post)
require.Equal(t, expectedMessage, post.Message)
require.Equal(t, expectedUserId, post.UserId)
require.Equal(t, expectedChannelId, post.ChannelId)
require.True(t, post.CreateAt > 0)
require.True(t, post.UpdateAt > 0)
}
func TestSendFlaggedPostRemovalNotification(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should send notifications to all configured targets", func(t *testing.T) {
// Setup notification config for all targets
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers, model.TargetAuthor, model.TargetReporter},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
actorComment := "This post violates community guidelines"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The quarantined message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, actorComment)
var reviewerPost *model.Post
for _, p := range createdPosts {
if p.Message == reviewerMessage {
reviewerPost = p
break
}
}
require.NotNil(t, reviewerPost)
verifyNotificationPost(t, reviewerPost, reviewerMessage, contentReviewBot.UserId, reviewerPost.ChannelId)
require.NotEmpty(t, reviewerPost.RootId) // Should be a thread reply to the flag review post
// Verify author message
authorMessage := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was quarantined for review has been permanently removed by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var authorPost *model.Post
for _, p := range createdPosts {
if p.Message == authorMessage {
authorPost = p
break
}
}
require.NotNil(t, authorPost)
verifyNotificationPost(t, authorPost, authorMessage, contentReviewBot.UserId, authorPost.ChannelId)
// Verify reporter message
reporterMessage := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you quarantined for review has been permanently removed by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var reporterPost *model.Post
for _, p := range createdPosts {
if p.Message == reporterMessage {
reporterPost = p
break
}
}
require.NotNil(t, reporterPost)
verifyNotificationPost(t, reporterPost, reporterMessage, contentReviewBot.UserId, reporterPost.ChannelId)
})
t.Run("should send notifications only to configured targets", func(t *testing.T) {
// Setup notification config for only author
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
// Setup notification config for only author
appErr = setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "Test comment", groupId)
// Should create only 1 post for author
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The quarantined message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, "Test comment")
verifyNotificationPost(t, createdPosts[0], expectedMessage, contentReviewBot.UserId, createdPosts[0].ChannelId)
})
t.Run("should handle empty comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was removed by @%s", th.SystemAdminUser.Username)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle special characters in comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, specialComment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
}
func TestSendKeepFlaggedPostNotification(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should send notifications to all configured targets", func(t *testing.T) {
// Setup notification config for all targets
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers, model.TargetAuthor, model.TargetReporter},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
actorComment := "This post is acceptable after review"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, actorComment)
var reviewerPost *model.Post
for _, p := range createdPosts {
if p.Message == reviewerMessage {
reviewerPost = p
break
}
}
require.NotNil(t, reviewerPost)
verifyNotificationPost(t, reviewerPost, reviewerMessage, contentReviewBot.UserId, reviewerPost.ChannelId)
require.NotEmpty(t, reviewerPost.RootId) // Should be a thread reply
// Verify author message
authorMessage := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was quarantined for review has been restored by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var authorPost *model.Post
for _, p := range createdPosts {
if p.Message == authorMessage {
authorPost = p
break
}
}
require.NotNil(t, authorPost)
verifyNotificationPost(t, authorPost, authorMessage, contentReviewBot.UserId, authorPost.ChannelId)
// Verify reporter message
reporterMessage := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you quarantined for review has been restored by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var reporterPost *model.Post
for _, p := range createdPosts {
if p.Message == reporterMessage {
reporterPost = p
break
}
}
require.NotNil(t, reporterPost)
verifyNotificationPost(t, reporterPost, reporterMessage, contentReviewBot.UserId, reporterPost.ChannelId)
})
t.Run("should send notifications only to configured targets", func(t *testing.T) {
// Setup notification config for only reporter
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
comment := "Test comment"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, comment, groupId)
// Should create only 1 post for reporter
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, comment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, contentReviewBot.UserId, createdPosts[0].ChannelId)
})
t.Run("should handle empty comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s", th.SystemAdminUser.Username)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle special characters in comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, specialComment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle different actor users", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
// Use BasicUser as actor instead of SystemAdminUser
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.BasicUser.Id, "Reviewed by different user", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.BasicUser.Username, "Reviewed by different user")
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
}
func TestPermanentDeleteFlaggedPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.Nil(t, setBaseConfig(th))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
t.Run("should successfully permanently delete pending flagged post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
actionRequest := &model.FlagContentActionRequest{
Comment: "This post violates community guidelines",
}
appErr := th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify post content was scrubbed
updatedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, true)
require.Nil(t, appErr) // Post should be deleted
require.Greater(t, updatedPost.DeleteAt, int64(0))
// Verify status was updated to removed
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Verify actor properties were created
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check actor user property
actorValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorUserID].ID,
})
require.Nil(t, err)
require.Len(t, actorValues, 1)
require.Equal(t, `"`+th.SystemAdminUser.Id+`"`, string(actorValues[0].Value))
// Check actor comment property
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `"This post violates community guidelines"`, string(commentValues[0].Value))
// Check action time property
timeValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActionTime].ID,
})
require.Nil(t, err)
require.Len(t, timeValues, 1)
var actionTime int64
unmarshalErr := json.Unmarshal(timeValues[0].Value, &actionTime)
require.NoError(t, unmarshalErr)
require.True(t, actionTime > 0)
})
t.Run("should successfully permanently delete pending flagged post when flagged posts are hidden", func(t *testing.T) {
baseConfig := getBaseConfig(th)
baseConfig.AdditionalSettings.HideFlaggedContent = new(true)
appErr := th.App.SaveContentFlaggingConfig(baseConfig)
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
actionRequest := &model.FlagContentActionRequest{
Comment: "This post violates community guidelines",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify post content was scrubbed
updatedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, true)
require.Nil(t, appErr) // Post should be deleted
require.Greater(t, updatedPost.DeleteAt, int64(0))
})
t.Run("should successfully permanently delete assigned flagged post", func(t *testing.T) {
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 needs removal",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify status was updated to removed
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
})
t.Run("should fail when trying to delete already removed post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
// Set status to removed
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete already removed post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.post_not_in_progress", appErr.Id)
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should fail when trying to delete already retained post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
// Set status to retained
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete retained post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.post_not_in_progress", appErr.Id)
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should fail when trying to delete non-flagged post", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete non-flagged post",
}
appErr := th.App.PermanentDeleteFlaggedPost(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.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify empty comment was stored
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.Nil(t, err)
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.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify special characters were properly escaped and stored
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
var storedComment string
unmarshalErr := json.Unmarshal(commentValues[0].Value, &storedComment)
require.NoError(t, unmarshalErr)
require.Equal(t, specialComment, storedComment)
})
t.Run("should handle post with file attachments", 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 needs removal",
}
require.Eventually(t, func() bool {
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
return appErr == nil
}, 5*time.Second, 200*time.Millisecond)
// Verify post was deleted and status updated
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Verify file infos were also deleted
files, err := th.App.Srv().Store().FileInfo().GetByIds([]string{fileInfo1.Id, fileInfo2.Id}, true, false, false)
require.NoError(t, err)
require.Empty(t, files)
})
t.Run("should handle post with edit history", 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)
// 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 needs removal",
}
require.Eventually(t, func() bool {
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, editedPost)
require.Nil(t, appErr)
return appErr == nil
}, 5*time.Second, 200*time.Millisecond)
// Verify status was updated
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(editedPost.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
// Verify statusValue.Value is a string
var stringValue string
err := json.Unmarshal(statusValue.Value, &stringValue)
require.NoError(t, err)
require.Equal(t, model.ContentFlaggingStatusRemoved, stringValue)
})
t.Run("should handle post that was already deleted", func(t *testing.T) {
config := getBaseConfig(th)
config.AdditionalSettings.HideFlaggedContent = new(true)
appErr := th.App.SaveContentFlaggingConfig(config)
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 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 deletedPost *model.Post
require.Eventually(t, func() bool {
// Get the updated post (should be deleted)
deletedPost, appErr = th.App.GetSinglePost(th.Context, post.Id, true)
require.Nil(t, appErr)
require.True(t, deletedPost.DeleteAt > 0)
return appErr == nil
}, 5*time.Second, 200*time.Millisecond)
actionRequest := &model.FlagContentActionRequest{
Comment: "Permanently delete already hidden post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, deletedPost)
require.Nil(t, appErr)
// Verify status was updated to removed
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(deletedPost.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
// Verify statusValue.Value is a string
var stringValue string
err := json.Unmarshal(statusValue.Value, &stringValue)
require.NoError(t, err)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
})
}
func TestPermanentDeleteFlaggedPostReport(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.Nil(t, setBaseConfig(th))
// Upload real files and create a post with those attachments to exercise all report steps
fileInfo1, appErr := th.App.DoUploadFile(th.Context, time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "report_file1.txt", []byte("file content 1"), true)
require.Nil(t, appErr)
fileInfo2, appErr := th.App.DoUploadFile(th.Context, time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "report_file2.txt", []byte("file content 2"), true)
require.Nil(t, appErr)
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "post with real file attachments",
FileIds: []string{fileInfo1.Id, fileInfo2.Id},
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
// Create edit history
editedPost := post.Clone()
editedPost.Message = "Edited for report test"
editedPost.EditAt = model.GetMillis()
_, _, appErr = th.App.UpdatePost(th.Context, editedPost, &model.UpdatePostOptions{})
require.Nil(t, appErr)
report, appErr := th.App.PermanentDeletePostDataRetainStub(th.Context, editedPost, th.SystemAdminUser.Id)
require.Nil(t, appErr)
require.NotNil(t, report)
// Validate report metadata
require.Equal(t, post.Id, report.PostID)
require.False(t, report.Timestamp.IsZero())
// All steps should succeed (or be marked not-applicable)
successCount, failedCount, partialCount, notApplicableCount := report.CountStatuses()
require.Equal(t, len(report.Steps), successCount+notApplicableCount, "all steps should succeed or be not applicable")
require.Equal(t, 0, failedCount)
require.Equal(t, 0, partialCount)
// Collect steps into a map for targeted assertions
stepsByName := make(map[string]*model.DeletionStepResult)
for i := range report.Steps {
stepsByName[report.Steps[i].Name] = &report.Steps[i]
}
// Verify all expected step names are present
for _, name := range []string{
"app.data_spillage.report.step.file_attachments",
"app.data_spillage.report.step.fileinfo_rows",
"app.data_spillage.report.step.edit_histories",
"app.data_spillage.report.step.priority_data",
"app.data_spillage.report.step.persistent_notifications",
"app.data_spillage.report.step.acknowledgements",
"app.data_spillage.report.step.reminders",
"app.data_spillage.report.step.thread_data",
"app.data_spillage.report.step.post_itself",
} {
require.Contains(t, stepsByName, name, "report should contain step %s", name)
}
// File steps should report details about the deleted files
fileCount, ok := stepsByName["app.data_spillage.report.step.file_attachments"].DetailParams["Count"].(int)
require.True(t, ok)
require.Equal(t, 2, fileCount)
// Edit history step should have sub-steps for each revision
editStep := stepsByName["app.data_spillage.report.step.edit_histories"]
require.NotEmpty(t, editStep.SubSteps, "edit_histories step should have sub-steps for each revision")
for _, subStep := range editStep.SubSteps {
require.Equal(t, model.StepSuccess, subStep.Status)
require.NotEmpty(t, subStep.Name, "sub-step should reference the edit history post ID")
}
// Verify report renders to non-empty markdown containing key sections
T := func(id string, args ...any) string { return id }
rendered := report.Render(T)
require.Contains(t, rendered, post.Id)
require.Contains(t, rendered, "app.data_spillage.report.title")
require.Contains(t, rendered, "app.data_spillage.report.summary")
summary := report.RenderSummary(T)
require.Contains(t, summary, "app.data_spillage.report.summary")
}
func TestDeleteEditHistories(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
findEditStep := func(report *model.PostDeletionReport) *model.DeletionStepResult {
for i := range report.Steps {
if report.Steps[i].Name == "app.data_spillage.report.step.edit_histories" {
return &report.Steps[i]
}
}
return nil
}
t.Run("no edit history", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
report := &model.PostDeletionReport{PostID: post.Id}
th.App.deleteEditHistories(th.Context, post.Id, th.SystemAdminUser.Id, report)
step := findEditStep(report)
require.NotNil(t, step)
require.Equal(t, model.StepNotApplicable, step.Status)
require.Equal(t, "app.data_spillage.report.detail.no_data_found", step.Detail)
require.Empty(t, step.SubSteps)
})
t.Run("single revision deleted successfully", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
edited := post.Clone()
edited.Message = "edited v1"
edited.EditAt = model.GetMillis()
_, _, appErr := th.App.UpdatePost(th.Context, edited, &model.UpdatePostOptions{})
require.Nil(t, appErr)
report := &model.PostDeletionReport{PostID: post.Id}
th.App.deleteEditHistories(th.Context, post.Id, th.SystemAdminUser.Id, report)
step := findEditStep(report)
require.NotNil(t, step)
require.Equal(t, model.StepSuccess, step.Status)
require.Len(t, step.SubSteps, 1)
require.Equal(t, model.StepSuccess, step.SubSteps[0].Status)
require.NotEmpty(t, step.SubSteps[0].Name)
require.Equal(t, 1, step.DetailParams["Count"])
require.Equal(t, 1, step.DetailParams["Total"])
})
t.Run("multiple revisions all deleted", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
for i := range 3 {
edited := post.Clone()
edited.Message = fmt.Sprintf("edit %d", i)
edited.EditAt = model.GetMillis()
_, _, appErr := th.App.UpdatePost(th.Context, edited, &model.UpdatePostOptions{})
require.Nil(t, appErr)
}
report := &model.PostDeletionReport{PostID: post.Id}
th.App.deleteEditHistories(th.Context, post.Id, th.SystemAdminUser.Id, report)
step := findEditStep(report)
require.NotNil(t, step)
require.Equal(t, model.StepSuccess, step.Status)
require.Len(t, step.SubSteps, 3)
for _, sub := range step.SubSteps {
require.Equal(t, model.StepSuccess, sub.Status)
}
require.Equal(t, 3, step.DetailParams["Count"])
require.Equal(t, 3, step.DetailParams["Total"])
})
t.Run("revision with file attachments reports file names", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
edited := post.Clone()
edited.Message = "edited with files"
edited.EditAt = model.GetMillis()
_, _, appErr := th.App.UpdatePost(th.Context, edited, &model.UpdatePostOptions{})
require.Nil(t, appErr)
// Attach a file to the edit history post to exercise the file-reporting code path
editHistories, appErr := th.App.GetEditHistoryForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, editHistories, 1)
fi := &model.FileInfo{
Id: model.NewId(),
PostId: editHistories[0].Id,
CreatorId: post.UserId,
Path: "test/edit_file.txt",
Name: "edit_file.txt",
Size: 50,
}
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, fi)
require.NoError(t, err)
report := &model.PostDeletionReport{PostID: post.Id}
th.App.deleteEditHistories(th.Context, post.Id, th.SystemAdminUser.Id, report)
step := findEditStep(report)
require.NotNil(t, step)
require.Len(t, step.SubSteps, 1)
})
t.Run("nonexistent post reports no revisions", func(t *testing.T) {
report := &model.PostDeletionReport{PostID: model.NewId()}
th.App.deleteEditHistories(th.Context, model.NewId(), th.SystemAdminUser.Id, report)
step := findEditStep(report)
require.NotNil(t, step)
require.Equal(t, model.StepNotApplicable, step.Status)
require.Equal(t, "app.data_spillage.report.detail.no_data_found", step.Detail)
})
}
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 = new(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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, appErr = th.App.UpdatePropertyValue(th.Context, groupId, statusValue)
require.Nil(t, appErr)
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.data_spillage.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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, appErr = th.App.UpdatePropertyValue(th.Context, groupId, statusValue)
require.Nil(t, appErr)
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.data_spillage.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, 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)
})
}