mattermost/server/channels/app/content_flagging_test.go
Miguel de la Cruz 58dd9e1bb4
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Add property system app layer architecture (#35157)
* Refactor property system with app layer routing and access control separation

Establish the app layer as the primary entry point for property operations
with intelligent routing based on group type. This architecture separates
access-controlled operations (CPA groups) from standard operations,
improving performance and code clarity.

Architecture Changes:
- App layer now routes operations based on group type:
  - CPA groups -> PropertyAccessService (enforces access control)
  - Non-CPA groups -> PropertyService (direct, no access control)
- PropertyAccessService simplified to handle only CPA operations
- Eliminated redundant group type checks throughout the codebase

* Move access control routing into PropertyService

This change makes the PropertyService the main entrypoint for property
related operations, and adds a routing mechanism to decide if extra
behaviors or checks should run for each operation, in this case, the
property access service logic.

To add specific payloads that pluggable checks and operations may
need, we use the request context. When the request comes from the API,
the endpoints are in charge of adding the caller ID to the payload,
and in the case of the plugin API, on receiving a request, the server
automatically tags the context with the plugin ID so the property
service can react accordingly.

Finally, the new design enforces all these checks migrating the actual
property logic to internal, non-exposed methods, so any caller from
the App layer needs to go through the service checks that decide if
pluggable logic is needed, avoiding any possibility of a bypass.

* Fix i18n

* Fix bad error string

* Added nil guards to property methods

* Add check for multiple group IDs on value operations

* Add nil guard to the plugin checker

* Fix build error

* Update value tests

* Fix linter

* Adds early return when content flaggin a thread with no replies

* Fix mocks

* Clean the state of plugin property tests before each run

* Do not wrap appErr on API response and fix i18n

* Fix create property field test

* Remove the need to cache cpaGroupID as part of the property service

* Split the property.go file into multiple

* Not found group doesn't bypass access control check

* Unexport SetPluginCheckerForTests

* Rename plugin context getter to be more PSA specific

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
2026-03-26 07:54:50 +00:00

3095 lines
110 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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
values, appErr2 := th.App.SearchPropertyValues(th.Context, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{postId},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[fieldName].ID,
})
require.Nil(t, appErr2)
return values
}
func setupFlaggedPost(t *testing.T, th *TestHelper) *model.Post {
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr := th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
time.Sleep(2 * time.Second)
return post
}
func TestContentFlaggingEnabledForTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("should return true for common reviewers", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: 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))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
t.Run("should successfully assign reviewer to pending flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was created
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should successfully reassign reviewer to already assigned flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
// First assignment
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Second assignment (reassignment)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser2.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status remains assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was updated
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should fail when trying to assign reviewer to non-flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(t, th.BasicChannel)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should handle assignment with same reviewer ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
// Assign reviewer
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Assign same reviewer again
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status remains assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property still exists with correct value
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should handle assignment with empty reviewer ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
appErr := th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, "", th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was created with empty value
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.Nil(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `""`, string(reviewerValues[0].Value))
})
t.Run("should handle assignment with invalid post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
appErr := th.App.AssignFlaggedPostReviewer(th.Context, "invalid_post_id", th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should allow assigning reviewer at all stages", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
// Set the status to Assigned
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusAssigned))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Set the status to Removed
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Set the status to Retained
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
})
}
func TestSaveContentFlaggingConfig(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should save content flagging config successfully", func(t *testing.T) {
config := model.ContentFlaggingSettingsRequest{
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
EnableContentFlagging: 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))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
t.Run("should be able to flag post which has not already been flagged", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
appErr := th.App.canFlagPost(groupId, post.Id, "en")
require.Nil(t, appErr)
})
t.Run("should not be able to flag post which has already been flagged", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusField, err := th.App.GetPropertyFieldByName(rctx, groupId, "", ContentFlaggingPropertyNameStatus)
require.Nil(t, err)
propertyValue, err := th.App.CreatePropertyValue(rctx, &model.PropertyValue{
TargetID: post.Id,
GroupID: groupId,
FieldID: statusField.ID,
TargetType: "post",
Value: json.RawMessage(`"` + model.ContentFlaggingStatusPending + `"`),
})
require.Nil(t, err)
// Can't fleg when post already flagged in pending status
appErr := th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
require.Equal(t, "Cannot quarantine this post as it is already quarantined for review.", appErr.Id)
// Can't fleg when post already flagged in assigned status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusAssigned + `"`)
_, err = th.App.UpdatePropertyValue(rctx, groupId, propertyValue)
require.Nil(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
// Can't fleg when post already flagged in retained status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusRetained + `"`)
_, err = th.App.UpdatePropertyValue(rctx, groupId, propertyValue)
require.Nil(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
// Can't fleg when post already flagged in removed status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusRemoved + `"`)
_, err = th.App.UpdatePropertyValue(rctx, groupId, propertyValue)
require.Nil(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
})
}
func TestFlagPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
cfg := model.ContentFlaggingSettingsRequest{}
cfg.SetDefaults()
cfg.ReviewerSettings.CommonReviewers = 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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check status property
statusValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[ContentFlaggingPropertyNameStatus].ID,
})
require.Nil(t, err)
require.Len(t, statusValues, 1)
require.Equal(t, `"`+model.ContentFlaggingStatusPending+`"`, string(statusValues[0].Value))
// Check reporting user property
userValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingUserID].ID,
})
require.Nil(t, err)
require.Len(t, userValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(userValues[0].Value))
// Check reason property
reasonValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingReason].ID,
})
require.Nil(t, err)
require.Len(t, reasonValues, 1)
require.Equal(t, `"spam"`, string(reasonValues[0].Value))
// Check comment property
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `"This is spam content"`, string(commentValues[0].Value))
})
t.Run("should fail with invalid reason", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "invalid_reason",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.reason_invalid", appErr.Id)
})
t.Run("should fail when comment is required but not provided", func(t *testing.T) {
config := getBaseConfig()
config.AdditionalSettings.ReporterCommentRequired = 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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `""`, string(commentValues[0].Value))
})
t.Run("should set reporting time property", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "\"Test comment\"",
}
beforeTime := model.GetMillis()
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
afterTime := model.GetMillis()
require.Nil(t, appErr)
// Verify reporting time property was set
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
timeValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingTime].ID,
})
require.Nil(t, err)
require.Len(t, timeValues, 1)
var reportingTime int64
unmarshalErr := json.Unmarshal(timeValues[0].Value, &reportingTime)
require.NoError(t, unmarshalErr)
require.True(t, reportingTime >= beforeTime && reportingTime <= afterTime)
})
}
func TestSearchReviewers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
config := model.ContentFlaggingSettingsRequest{}
config.SetDefaults()
return config
}
allProfilesSanitized := func(reviewers []*model.User) {
for _, reviewer := range reviewers {
require.Empty(t, reviewer.LastPasswordUpdate)
require.Empty(t, reviewer.NotifyProps)
}
}
t.Run("should return common reviewers when searching", func(t *testing.T) {
config := getBaseConfig()
config.ReviewerSettings.CommonReviewers = 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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 1)
// Verify the reviewer post exists and has the correct properties
reviewerPost, appErr := th.App.GetSinglePost(th.Context, reviewerPostIds[0], false)
require.Nil(t, appErr)
require.Equal(t, model.ContentFlaggingPostType, reviewerPost.Type)
require.Contains(t, reviewerPost.GetProps(), POST_PROP_KEY_FLAGGED_POST_ID)
require.Equal(t, post.Id, reviewerPost.GetProps()[POST_PROP_KEY_FLAGGED_POST_ID])
})
t.Run("should return empty list when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(t, th.BasicChannel)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 0)
})
t.Run("should handle multiple reviewer posts for same flagged post", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.SystemAdminUser.Id, flagData)
require.Nil(t, appErr)
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 2)
// Verify both reviewer posts exist and have correct properties
for _, postId := range reviewerPostIds {
reviewerPost, appErr := th.App.GetSinglePost(th.Context, postId, false)
require.Nil(t, appErr)
require.Equal(t, model.ContentFlaggingPostType, reviewerPost.Type)
require.Contains(t, reviewerPost.GetProps(), POST_PROP_KEY_FLAGGED_POST_ID)
require.Equal(t, post.Id, reviewerPost.GetProps()[POST_PROP_KEY_FLAGGED_POST_ID])
}
})
t.Run("should handle invalid flagged post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, "invalid_post_id", flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 0)
})
}
func TestPostReviewerMessage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should post reviewer message to thread", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test reviewer message"
_, appErr := th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Verify message was posted to the reviewer thread
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Find the original review post and the test message
var reviewPost *model.Post
var testMessagePost *model.Post
for _, p := range posts.Posts {
if p.Type == "custom_spillage_report" {
reviewPost = p
} else if p.RootId != "" && p.Message == testMessage {
testMessagePost = p
}
}
require.NotNil(t, reviewPost)
require.NotNil(t, testMessagePost)
require.Equal(t, reviewPost.Id, testMessagePost.RootId)
require.Equal(t, contentReviewBot.UserId, testMessagePost.UserId)
})
t.Run("should handle multiple reviewer channels", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(t, th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.SystemAdminUser.Id, flagData)
require.Nil(t, appErr)
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test message for multiple reviewers"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test message for non-flagged post"
_, appErr := th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
})
t.Run("should handle message with special characters", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
testMessage := "Test message with special chars: @user #channel ~team & <script>alert('xss')</script>"
_, appErr := th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Verify message was posted correctly with special characters preserved
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Find the test message
var testMessagePost *model.Post
for _, p := range posts.Posts {
if p.RootId != "" && p.Message == testMessage {
testMessagePost = p
break
}
}
require.NotNil(t, testMessagePost)
require.Equal(t, testMessage, testMessagePost.Message)
})
}
// Helper function to setup notification config for testing
func setupNotificationConfig(th *TestHelper, eventTargetMapping map[model.ContentFlaggingEvent][]model.NotificationTarget) *model.AppError {
config := getBaseConfig(th)
config.NotificationSettings = &model.ContentFlaggingNotificationSettings{
EventTargetMapping: eventTargetMapping,
}
return th.App.SaveContentFlaggingConfig(config)
}
// Helper function to verify post message content and properties
func verifyNotificationPost(t *testing.T, post *model.Post, expectedMessage string, expectedUserId string, expectedChannelId string) {
require.NotNil(t, post)
require.Equal(t, expectedMessage, post.Message)
require.Equal(t, expectedUserId, post.UserId)
require.Equal(t, expectedChannelId, post.ChannelId)
require.True(t, post.CreateAt > 0)
require.True(t, post.UpdateAt > 0)
}
func TestSendFlaggedPostRemovalNotification(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should send notifications to all configured targets", func(t *testing.T) {
// Setup notification config for all targets
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers, model.TargetAuthor, model.TargetReporter},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
actorComment := "This post violates community guidelines"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The quarantined message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, actorComment)
var reviewerPost *model.Post
for _, p := range createdPosts {
if p.Message == reviewerMessage {
reviewerPost = p
break
}
}
require.NotNil(t, reviewerPost)
verifyNotificationPost(t, reviewerPost, reviewerMessage, contentReviewBot.UserId, reviewerPost.ChannelId)
require.NotEmpty(t, reviewerPost.RootId) // Should be a thread reply to the flag review post
// Verify author message
authorMessage := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was quarantined for review has been permanently removed by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var authorPost *model.Post
for _, p := range createdPosts {
if p.Message == authorMessage {
authorPost = p
break
}
}
require.NotNil(t, authorPost)
verifyNotificationPost(t, authorPost, authorMessage, contentReviewBot.UserId, authorPost.ChannelId)
// Verify reporter message
reporterMessage := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you quarantined for review has been permanently removed by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var reporterPost *model.Post
for _, p := range createdPosts {
if p.Message == reporterMessage {
reporterPost = p
break
}
}
require.NotNil(t, reporterPost)
verifyNotificationPost(t, reporterPost, reporterMessage, contentReviewBot.UserId, reporterPost.ChannelId)
})
t.Run("should send notifications only to configured targets", func(t *testing.T) {
// Setup notification config for only author
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
// Setup notification config for only author
appErr = setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "Test comment", groupId)
// Should create only 1 post for author
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The quarantined message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, "Test comment")
verifyNotificationPost(t, createdPosts[0], expectedMessage, contentReviewBot.UserId, createdPosts[0].ChannelId)
})
t.Run("should handle empty comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was removed by @%s", th.SystemAdminUser.Username)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle special characters in comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was removed by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, specialComment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
}
func TestSendKeepFlaggedPostNotification(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should send notifications to all configured targets", func(t *testing.T) {
// Setup notification config for all targets
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers, model.TargetAuthor, model.TargetReporter},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
actorComment := "This post is acceptable after review"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, actorComment)
var reviewerPost *model.Post
for _, p := range createdPosts {
if p.Message == reviewerMessage {
reviewerPost = p
break
}
}
require.NotNil(t, reviewerPost)
verifyNotificationPost(t, reviewerPost, reviewerMessage, contentReviewBot.UserId, reviewerPost.ChannelId)
require.NotEmpty(t, reviewerPost.RootId) // Should be a thread reply
// Verify author message
authorMessage := fmt.Sprintf("Your post having ID `%s` in the channel `%s` which was quarantined for review has been restored by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var authorPost *model.Post
for _, p := range createdPosts {
if p.Message == authorMessage {
authorPost = p
break
}
}
require.NotNil(t, authorPost)
verifyNotificationPost(t, authorPost, authorMessage, contentReviewBot.UserId, authorPost.ChannelId)
// Verify reporter message
reporterMessage := fmt.Sprintf("The post having ID `%s` in the channel `%s` which you quarantined for review has been restored by a reviewer.", post.Id, th.BasicChannel.DisplayName)
var reporterPost *model.Post
for _, p := range createdPosts {
if p.Message == reporterMessage {
reporterPost = p
break
}
}
require.NotNil(t, reporterPost)
verifyNotificationPost(t, reporterPost, reporterMessage, contentReviewBot.UserId, reporterPost.ChannelId)
})
t.Run("should send notifications only to configured targets", func(t *testing.T) {
// Setup notification config for only reporter
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
comment := "Test comment"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, comment, groupId)
// Should create only 1 post for reporter
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, comment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, contentReviewBot.UserId, createdPosts[0].ChannelId)
})
t.Run("should handle empty comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s", th.SystemAdminUser.Username)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle special characters in comment", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.SystemAdminUser.Username, specialComment)
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
t.Run("should handle different actor users", func(t *testing.T) {
appErr := setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentDismissed: {model.TargetReviewers},
})
require.Nil(t, appErr)
post := setupFlaggedPost(t, th)
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
// Use BasicUser as actor instead of SystemAdminUser
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.BasicUser.Id, "Reviewed by different user", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The quarantined message was retained by @%s\n\nWith comment:\n\n> %s", th.BasicUser.Username, "Reviewed by different user")
verifyNotificationPost(t, createdPosts[0], expectedMessage, createdPosts[0].UserId, createdPosts[0].ChannelId)
})
}
func TestPermanentDeleteFlaggedPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.Nil(t, setBaseConfig(th))
rctx := RequestContextWithCallerID(th.Context, anonymousCallerId)
t.Run("should successfully permanently delete pending flagged post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
actionRequest := &model.FlagContentActionRequest{
Comment: "This post violates community guidelines",
}
appErr := th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify post content was scrubbed
updatedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, true)
require.Nil(t, appErr) // Post should be deleted
require.Greater(t, updatedPost.DeleteAt, int64(0))
// Verify status was updated to removed
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Verify actor properties were created
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check actor user property
actorValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorUserID].ID,
})
require.Nil(t, err)
require.Len(t, actorValues, 1)
require.Equal(t, `"`+th.SystemAdminUser.Id+`"`, string(actorValues[0].Value))
// Check actor comment property
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `"This post violates community guidelines"`, string(commentValues[0].Value))
// Check action time property
timeValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActionTime].ID,
})
require.Nil(t, err)
require.Len(t, timeValues, 1)
var actionTime int64
unmarshalErr := json.Unmarshal(timeValues[0].Value, &actionTime)
require.NoError(t, unmarshalErr)
require.True(t, actionTime > 0)
})
t.Run("should successfully permanently delete pending flagged post when flagged posts are hidden", func(t *testing.T) {
baseConfig := getBaseConfig(th)
baseConfig.AdditionalSettings.HideFlaggedContent = 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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete already removed post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.post_not_in_progress", appErr.Id)
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should fail when trying to delete already retained post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
// Set status to retained
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err = th.App.UpdatePropertyValue(rctx, groupId, statusValue)
require.Nil(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete retained post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.post_not_in_progress", appErr.Id)
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should fail when trying to delete non-flagged post", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete non-flagged post",
}
appErr := th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should handle empty comment", func(t *testing.T) {
post := setupFlaggedPost(t, th)
actionRequest := &model.FlagContentActionRequest{
Comment: "",
}
appErr := th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify empty comment was stored
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `""`, string(commentValues[0].Value))
})
t.Run("should handle special characters in comment", func(t *testing.T) {
post := setupFlaggedPost(t, th)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
actionRequest := &model.FlagContentActionRequest{
Comment: specialComment,
}
appErr := th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify special characters were properly escaped and stored
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.App.SearchPropertyValues(rctx, groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.Nil(t, err)
require.Len(t, commentValues, 1)
var storedComment string
unmarshalErr := json.Unmarshal(commentValues[0].Value, &storedComment)
require.NoError(t, unmarshalErr)
require.Equal(t, specialComment, storedComment)
})
t.Run("should handle post with file attachments", func(t *testing.T) {
// Create a post with file attachments
post := th.CreatePost(t, th.BasicChannel)
// Create some file infos and associate them with the post
fileInfo1 := &model.FileInfo{
Id: model.NewId(),
PostId: post.Id,
CreatorId: post.UserId,
Path: "test/file1.txt",
Name: "file1.txt",
Size: 100,
}
fileInfo2 := &model.FileInfo{
Id: model.NewId(),
PostId: post.Id,
CreatorId: post.UserId,
Path: "test/file2.txt",
Name: "file2.txt",
Size: 200,
}
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo1)
require.NoError(t, err)
_, err = th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo2)
require.NoError(t, err)
// Update post to include file IDs
post.FileIds = []string{fileInfo1.Id, fileInfo2.Id}
_, _, appErr := th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{})
require.Nil(t, appErr)
// Flag the post
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "Post with files needs removal",
}
require.Eventually(t, func() bool {
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
return appErr == nil
}, 5*time.Second, 200*time.Millisecond)
// Verify post was deleted and status updated
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Verify file infos were also deleted
files, err := th.App.Srv().Store().FileInfo().GetByIds([]string{fileInfo1.Id, fileInfo2.Id}, true, false)
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, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, appErr = th.App.UpdatePropertyValue(th.Context, groupId, statusValue)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to keep already removed post",
}
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.post_not_in_progress", appErr.Id)
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should fail when trying to keep already retained post", func(t *testing.T) {
post := setupFlaggedPost(t, th)
// Set status to retained
groupId, err := th.App.ContentFlaggingGroupId()
require.Nil(t, err)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, appErr = th.App.UpdatePropertyValue(th.Context, groupId, statusValue)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to keep already retained post",
}
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.data_spillage.error.post_not_in_progress", appErr.Id)
require.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should fail when trying to keep non-flagged post", func(t *testing.T) {
post := th.CreatePost(t, th.BasicChannel)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to keep non-flagged post",
}
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should handle empty comment", func(t *testing.T) {
post := setupFlaggedPost(t, th)
actionRequest := &model.FlagContentActionRequest{
Comment: "",
}
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify empty comment was stored
commentValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorComment)
require.Len(t, commentValues, 1)
require.Equal(t, `""`, string(commentValues[0].Value))
})
t.Run("should handle special characters in comment", func(t *testing.T) {
post := setupFlaggedPost(t, th)
specialComment := "Comment with @mentions #channels ~teams & <script>alert('xss')</script>"
actionRequest := &model.FlagContentActionRequest{
Comment: specialComment,
}
appErr := th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify special characters were properly escaped and stored
commentValues := searchPropertyValue(t, th, post.Id, contentFlaggingPropertyNameActorComment)
require.Len(t, commentValues, 1)
var storedComment string
err := json.Unmarshal(commentValues[0].Value, &storedComment)
require.NoError(t, err)
require.Equal(t, specialComment, storedComment)
})
t.Run("should preserve file attachments when keeping flagged post", func(t *testing.T) {
// Create a post with file attachments
post := th.CreatePost(t, th.BasicChannel)
// Create some file infos and associate them with the post
fileInfo1 := &model.FileInfo{
Id: model.NewId(),
PostId: post.Id,
CreatorId: post.UserId,
Path: "test/file1.txt",
Name: "file1.txt",
Size: 100,
}
fileInfo2 := &model.FileInfo{
Id: model.NewId(),
PostId: post.Id,
CreatorId: post.UserId,
Path: "test/file2.txt",
Name: "file2.txt",
Size: 200,
}
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo1)
require.NoError(t, err)
_, err = th.App.Srv().Store().FileInfo().Save(th.Context, fileInfo2)
require.NoError(t, err)
// Update post to include file IDs
post.FileIds = []string{fileInfo1.Id, fileInfo2.Id}
_, _, appErr := th.App.UpdatePost(th.Context, post, &model.UpdatePostOptions{})
require.Nil(t, appErr)
// Flag the post
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "Post with files is acceptable",
}
require.Eventually(t, func() bool {
appErr = th.App.KeepFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
return appErr == nil
}, 5*time.Second, 200*time.Millisecond)
// Verify post was retained
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
// Verify file infos are still present (not deleted)
files, err := th.App.Srv().Store().FileInfo().GetByIds([]string{fileInfo1.Id, fileInfo2.Id}, false, false)
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)
})
}