Files
team/server/channels/app/content_flagging_test.go
22104d294e Rename Content Flagging to Data Spillage Handling (#35407)
* Rename Content Flagging to Data Spillage Handling

Update all user-facing text to use "Data Spillage Handling" and
"Quarantine for Review" terminology. Rename i18n keys that referenced
content flagging. Auto-patch bot display name on pre-existing servers.

* Fixed searchable stringgs

* Revert unintended package-lock.json changes

* Fix i18n extract check: correct typo and key ordering

Fix "posed" -> "posted" typo in keep/remove quarantine modal
defaultMessages. Move admin.contentFlagging.title to correct
alphabetical position in en.json.

* Fix webapp tests for Data Spillage Handling rename

Update test assertions to match renamed i18n strings:
notification settings, content reviewers, and additional
settings tests now expect the new quarantine terminology.

* Use translatable i18n strings for notification messages

Replace hardcoded "flagged for review" notification templates with
i18n.T() calls using "quarantined for review" terminology. Add six
new server i18n keys for author, reporter, and reviewer notifications.

* Fix server i18n key mismatches and update test assertions

Rename remaining app.content_flagging.* keys to app.data_spillage.*
in server/i18n/en.json to match Go code references. Fix the
quarantine_post_confirmation key name. Update test assertions to
match new "quarantined for review" terminology.

* Fix gofmt formatting in content_flagging.go

* Prevent nil bot on PatchBot failure in getContentReviewBot

Use a separate variable for PatchBot result so the original bot
is preserved if the display name update fails.

* Reorder server i18n keys after extract

Run mmgotool i18n extract to sort entries into correct
alphabetical order.

* Replace i18n.T() with fmt.Sprintf for notification messages and fix test assertions

Use direct string formatting for bot notification messages instead of
i18n translation keys, which were being removed by mmgotool i18n extract
due to indirect key references. Also update test expectations for renamed
error keys (content_flagging -> data_spillage).

* Update default quarantine reasons to DISC-aligned terminology

Replace generic content moderation reasons with defense/intelligence
sector terminology: Classification mismatch, Need-to-know violation,
PII exposure, OPSEC concern, CUI violation, Unauthorized disclosure,
and Other. Updated across model, API tests, webapp tests, and e2e tests.

* Adding a string missing from bad merge

* Update remaining flagged terminology and icon for data spillage rename

- Change post menu icon from flag-outline to alert-outline
- Update reviewer notification: "quarantined" -> "submitted" a message
- Update action notifications: "flagged message" -> "quarantined message"
- Update modal errors: "flagging" -> "quarantining" this message
- Update report title: "flagged" -> "submitted" a message for review
- Update e2e page object locator for renamed menu item

Made-with: Cursor

* Fix tests

* Fix quarantine icon alignment in post dot menu

Use AlertOutlineIcon React component with size={18} instead of raw
<i> tag to match the sizing of all other menu item icons.

Made-with: Cursor

* Fixed E2E tests

* Missing test fix

* Fix E2E tests

---------

Co-authored-by: Mattermost Build <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-03-06 21:15:01 -05:00

3092 lines
111 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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
config.AdditionalSettings.HideFlaggedContent = model.NewPointer(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
values, err := th.Server.propertyAccessService.SearchPropertyValues("", groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{postId},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[fieldName].ID,
})
require.NoError(t, err)
return values
}
func setupFlaggedPost(t *testing.T, th *TestHelper) *model.Post {
post := th.CreatePost(t, th.BasicChannel)
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: model.NewPointer(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: model.NewPointer(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
"team1": {
Enabled: model.NewPointer(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: model.NewPointer(false),
TeamAdminsAsReviewers: model.NewPointer(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
"team1": {
Enabled: model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(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))
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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.PropertyAccessService().UpdatePropertyValue(anonymousCallerId, groupId, statusValue)
require.NoError(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.PropertyAccessService().UpdatePropertyValue(anonymousCallerId, groupId, statusValue)
require.NoError(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.PropertyAccessService().UpdatePropertyValue(anonymousCallerId, groupId, statusValue)
require.NoError(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: model.NewPointer(true),
AdditionalSettings: &model.AdditionalContentFlaggingSettings{
ReporterCommentRequired: model.NewPointer(true),
HideFlaggedContent: model.NewPointer(false),
Reasons: &[]string{"spam", "harassment", "inappropriate"},
},
},
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: model.NewPointer(true),
SystemAdminsAsReviewers: model.NewPointer(true),
TeamAdminsAsReviewers: model.NewPointer(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: model.NewPointer(true),
},
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: model.NewPointer(false),
SystemAdminsAsReviewers: model.NewPointer(false),
TeamAdminsAsReviewers: model.NewPointer(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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: model.NewPointer(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: model.NewPointer(false),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
TeamReviewersSetting: map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(true),
ReviewerIds: []string{th.BasicUser.Id},
},
"team2": {
Enabled: model.NewPointer(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: model.NewPointer(true),
SystemAdminsAsReviewers: model.NewPointer(true),
CommonReviewers: model.NewPointer(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 = model.NewPointer(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 = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.CommonReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.SystemAdminUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(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))
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusField, err := th.Server.propertyAccessService.GetPropertyFieldByName(anonymousCallerId, groupId, "", ContentFlaggingPropertyNameStatus)
require.NoError(t, err)
propertyValue, err := th.Server.propertyAccessService.CreatePropertyValue(anonymousCallerId, &model.PropertyValue{
TargetID: post.Id,
GroupID: groupId,
FieldID: statusField.ID,
TargetType: "post",
Value: json.RawMessage(`"` + model.ContentFlaggingStatusPending + `"`),
})
require.NoError(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.Server.propertyAccessService.UpdatePropertyValue(anonymousCallerId, groupId, propertyValue)
require.NoError(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.Server.propertyAccessService.UpdatePropertyValue(anonymousCallerId, groupId, propertyValue)
require.NoError(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.Server.propertyAccessService.UpdatePropertyValue(anonymousCallerId, groupId, propertyValue)
require.NoError(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))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
cfg := model.ContentFlaggingSettingsRequest{}
cfg.SetDefaults()
cfg.ReviewerSettings.CommonReviewers = model.NewPointer(true)
cfg.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
cfg.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
cfg.AdditionalSettings.HideFlaggedContent = model.NewPointer(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check status property
statusValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[ContentFlaggingPropertyNameStatus].ID,
})
require.NoError(t, err)
require.Len(t, statusValues, 1)
require.Equal(t, `"`+model.ContentFlaggingStatusPending+`"`, string(statusValues[0].Value))
// Check reporting user property
userValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingUserID].ID,
})
require.NoError(t, err)
require.Len(t, userValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(userValues[0].Value))
// Check reason property
reasonValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingReason].ID,
})
require.NoError(t, err)
require.Len(t, reasonValues, 1)
require.Equal(t, `"spam"`, string(reasonValues[0].Value))
// Check comment property
commentValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.NoError(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 = model.NewPointer(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 = model.NewPointer(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
timeValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingTime].ID,
})
require.NoError(t, err)
require.Len(t, timeValues, 1)
var reportingTime int64
err = json.Unmarshal(timeValues[0].Value, &reportingTime)
require.NoError(t, err)
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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(true),
ReviewerIds: []string{th.BasicUser2.Id},
},
}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.SystemAdminUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(false)
config.ReviewerSettings.TeamReviewersSetting = map[string]*model.TeamReviewerSetting{
th.BasicTeam.Id: {
Enabled: model.NewPointer(true),
ReviewerIds: []string{th.BasicUser.Id},
},
}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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 = model.NewPointer(true)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
config.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(false)
config.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test reviewer message"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message for multiple reviewers"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
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 handle case when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(t, th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message for non-flagged post"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message with special chars: @user #channel ~team & <script>alert('xss')</script>"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
// 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))
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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check actor user property
actorValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorUserID].ID,
})
require.NoError(t, err)
require.Len(t, actorValues, 1)
require.Equal(t, `"`+th.SystemAdminUser.Id+`"`, string(actorValues[0].Value))
// Check actor comment property
commentValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.NoError(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.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActionTime].ID,
})
require.NoError(t, err)
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 permanently delete pending flagged post when flagged posts are hidden", func(t *testing.T) {
baseConfig := getBaseConfig(th)
baseConfig.AdditionalSettings.HideFlaggedContent = model.NewPointer(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err := th.App.PropertyAccessService().UpdatePropertyValue(anonymousCallerId, groupId, statusValue)
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err := th.App.PropertyAccessService().UpdatePropertyValue(anonymousCallerId, groupId, statusValue)
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.NoError(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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.Server.propertyAccessService.SearchPropertyValues(anonymousCallerId, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.NoError(t, err)
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 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)
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 = model.NewPointer(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 TestKeepFlaggedPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.Nil(t, setBaseConfig(th))
t.Run("should successfully keep pending flagged post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
actionRequest := &model.FlagContentActionRequest{
Comment: "This post is acceptable after review",
}
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify post still exists and is not deleted
updatedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
require.Nil(t, appErr)
require.Equal(t, int64(0), updatedPost.DeleteAt)
// Verify status was updated to retained
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
// Verify actor properties were created
// Check actor user property
actorValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorUserID)
require.Len(t, actorValues, 1)
require.Equal(t, `"`+th.SystemAdminUser.Id+`"`, string(actorValues[0].Value))
// Check actor comment property
commentValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorComment)
require.Len(t, commentValues, 1)
require.Equal(t, `"This post is acceptable after review"`, string(commentValues[0].Value))
// Check action time property
timeValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActionTime)
require.Len(t, timeValues, 1)
var actionTime int64
err := json.Unmarshal(timeValues[0].Value, &actionTime)
require.NoError(t, err)
require.True(t, actionTime > 0)
})
t.Run("should successfully keep and restore hidden flagged post", func(t *testing.T) {
baseConfig := getBaseConfig(th)
baseConfig.AdditionalSettings.HideFlaggedContent = model.NewPointer(true)
appErr := th.App.SaveContentFlaggingConfig(baseConfig)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
// Flag the post (this will hide/delete it due to HideFlaggedContent setting)
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
var hiddenPost *model.Post
require.Eventually(t, func() bool {
// Get the updated post (should be deleted/hidden)
hiddenPost, appErr = th.App.GetSinglePost(th.Context, post.Id, true)
require.Nil(t, appErr)
return hiddenPost.DeleteAt > 0
}, 5*time.Second, 200*time.Millisecond)
actionRequest := &model.FlagContentActionRequest{
Comment: "Restoring this post after review",
}
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, hiddenPost)
require.Nil(t, appErr)
// Verify post was restored (DeleteAt should be 0)
restoredPost, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
require.Nil(t, appErr)
require.Equal(t, int64(0), restoredPost.DeleteAt)
// Verify status was updated to retained
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
})
t.Run("should successfully keep assigned flagged post", func(t *testing.T) {
// Reset config to not hide flagged content
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
// Assign the post to a reviewer first
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "Assigned post is acceptable",
}
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify status was updated to retained
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
})
t.Run("should fail when trying to keep already removed post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
// Set status to removed
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err := th.App.Srv().propertyAccessService.UpdatePropertyValue("", groupId, statusValue)
require.NoError(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to keep already removed post",
}
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.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, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err := th.App.Srv().propertyAccessService.UpdatePropertyValue("", groupId, statusValue)
require.NoError(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to keep already retained post",
}
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.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)
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)
})
}