// 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 setupFlaggedPost(th *TestHelper) (*model.Post, *model.AppError) {
post := th.CreatePost(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)
if appErr != nil {
return nil, appErr
}
time.Sleep(2 * time.Second)
return post, nil
}
func TestContentFlaggingEnabledForTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
defer th.TearDown()
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()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should successfully assign reviewer to pending flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was created
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should successfully reassign reviewer to already assigned flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// First assignment
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Second assignment (reassignment)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser2.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status remains assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was updated
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should fail when trying to assign reviewer to non-flagged post", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// Assign reviewer
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Assign same reviewer again
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status remains assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property still exists with correct value
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `"`+th.BasicUser.Id+`"`, string(reviewerValues[0].Value))
})
t.Run("should handle assignment with empty reviewer ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, "", th.SystemAdminUser.Id)
require.Nil(t, appErr)
// Verify status was updated to assigned
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Verify reviewer property was created with empty value
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
reviewerValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReviewerUserID].ID,
})
require.NoError(t, err)
require.Len(t, reviewerValues, 1)
require.Equal(t, `""`, string(reviewerValues[0].Value))
})
t.Run("should handle assignment with invalid post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
appErr := th.App.AssignFlaggedPostReviewer(th.Context, "invalid_post_id", th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.NotNil(t, appErr)
require.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("should allow assigning reviewer at all stages", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
// Set the status to Assigned
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusAssigned))
_, err := th.App.Srv().propertyService.UpdatePropertyValue(groupId, statusValue)
require.NoError(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusAssigned+`"`, string(statusValue.Value))
// Set the status to Removed
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err = th.App.Srv().propertyService.UpdatePropertyValue(groupId, statusValue)
require.NoError(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Set the status to Retained
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err = th.App.Srv().propertyService.UpdatePropertyValue(groupId, statusValue)
require.NoError(t, err)
appErr = th.App.AssignFlaggedPostReviewer(th.Context, post.Id, th.BasicChannel.TeamId, th.BasicUser.Id, th.SystemAdminUser.Id)
require.Nil(t, appErr)
statusValue, appErr = th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRetained+`"`, string(statusValue.Value))
})
}
func TestSaveContentFlaggingConfig(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
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()
defer th.TearDown()
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()
defer th.TearDown()
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()
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.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()
defer th.TearDown()
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()
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.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()
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()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Run("should be able to flag post which has not already been flagged", func(t *testing.T) {
post := th.CreatePost(th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.Nil(t, appErr)
})
t.Run("should not be able to flag post which has already been flagged", func(t *testing.T) {
post := th.CreatePost(th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusField, err := th.Server.propertyService.GetPropertyFieldByName(groupId, "", ContentFlaggingPropertyNameStatus)
require.NoError(t, err)
propertyValue, err := th.Server.propertyService.CreatePropertyValue(&model.PropertyValue{
TargetID: post.Id,
GroupID: groupId,
FieldID: statusField.ID,
TargetType: "post",
Value: json.RawMessage(`"` + model.ContentFlaggingStatusPending + `"`),
})
require.NoError(t, err)
// Can't fleg when post already flagged in pending status
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
require.Equal(t, "Cannot flag this post as it is already flagged.", appErr.Id)
// Can't fleg when post already flagged in assigned status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusAssigned + `"`)
_, err = th.Server.propertyService.UpdatePropertyValue(groupId, propertyValue)
require.NoError(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
// Can't fleg when post already flagged in retained status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusRetained + `"`)
_, err = th.Server.propertyService.UpdatePropertyValue(groupId, propertyValue)
require.NoError(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
// Can't fleg when post already flagged in removed status
propertyValue.Value = json.RawMessage(`"` + model.ContentFlaggingStatusRemoved + `"`)
_, err = th.Server.propertyService.UpdatePropertyValue(groupId, propertyValue)
require.NoError(t, err)
appErr = th.App.canFlagPost(groupId, post.Id, "en")
require.NotNil(t, appErr)
})
}
func TestFlagPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
getBaseConfig := func() model.ContentFlaggingSettingsRequest {
cfg := model.ContentFlaggingSettingsRequest{}
cfg.SetDefaults()
cfg.ReviewerSettings.CommonReviewers = model.NewPointer(true)
cfg.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id}
cfg.AdditionalSettings.ReporterCommentRequired = model.NewPointer(false)
cfg.AdditionalSettings.HideFlaggedContent = model.NewPointer(false)
cfg.AdditionalSettings.Reasons = &[]string{"spam", "harassment", "inappropriate"}
return cfg
}
t.Run("should successfully flag a post with valid data", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
// Verify property values were created
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check status property
statusValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[ContentFlaggingPropertyNameStatus].ID,
})
require.NoError(t, err)
require.Len(t, statusValues, 1)
require.Equal(t, `"`+model.ContentFlaggingStatusPending+`"`, string(statusValues[0].Value))
// Check reporting user property
userValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingUserID].ID,
})
require.NoError(t, err)
require.Len(t, userValues, 1)
require.Equal(t, `"`+th.BasicUser2.Id+`"`, string(userValues[0].Value))
// Check reason property
reasonValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingReason].ID,
})
require.NoError(t, err)
require.Len(t, reasonValues, 1)
require.Equal(t, `"spam"`, string(reasonValues[0].Value))
// Check comment property
commentValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.NoError(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `"This is spam content"`, string(commentValues[0].Value))
})
t.Run("should fail with invalid reason", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(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.content_flagging.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(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(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 flag this post as it is already flagged.", 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(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(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(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "inappropriate",
Comment: "",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
require.Nil(t, appErr)
// Verify property values were created with empty comment
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingComment].ID,
})
require.NoError(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `""`, string(commentValues[0].Value))
})
t.Run("should set reporting time property", func(t *testing.T) {
appErr := th.App.SaveContentFlaggingConfig(getBaseConfig())
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "\"Test comment\"",
}
beforeTime := model.GetMillis()
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.BasicUser2.Id, flagData)
afterTime := model.GetMillis()
require.Nil(t, appErr)
// Verify reporting time property was set
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
timeValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameReportingTime].ID,
})
require.NoError(t, err)
require.Len(t, timeValues, 1)
var reportingTime int64
err = json.Unmarshal(timeValues[0].Value, &reportingTime)
require.NoError(t, err)
require.True(t, reportingTime >= beforeTime && reportingTime <= afterTime)
})
}
func TestSearchReviewers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
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()
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.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()
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.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()
defer th.TearDown()
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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 1)
// Verify the reviewer post exists and has the correct properties
reviewerPost, appErr := th.App.GetSinglePost(th.Context, reviewerPostIds[0], false)
require.Nil(t, appErr)
require.Equal(t, model.ContentFlaggingPostType, reviewerPost.Type)
require.Contains(t, reviewerPost.GetProps(), POST_PROP_KEY_FLAGGED_POST_ID)
require.Equal(t, post.Id, reviewerPost.GetProps()[POST_PROP_KEY_FLAGGED_POST_ID])
})
t.Run("should return empty list when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 0)
})
t.Run("should handle multiple reviewer posts for same flagged post", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.SystemAdminUser.Id, flagData)
require.Nil(t, appErr)
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, post.Id, flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 2)
// Verify both reviewer posts exist and have correct properties
for _, postId := range reviewerPostIds {
reviewerPost, appErr := th.App.GetSinglePost(th.Context, postId, false)
require.Nil(t, appErr)
require.Equal(t, model.ContentFlaggingPostType, reviewerPost.Type)
require.Contains(t, reviewerPost.GetProps(), POST_PROP_KEY_FLAGGED_POST_ID)
require.Equal(t, post.Id, reviewerPost.GetProps()[POST_PROP_KEY_FLAGGED_POST_ID])
}
})
t.Run("should handle invalid flagged post ID", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
flaggedPostIdField, ok := mappedFields[contentFlaggingPropertyNameFlaggedPostId]
require.True(t, ok)
reviewerPostIds, appErr := th.App.getReviewerPostsForFlaggedPost(groupId, "invalid_post_id", flaggedPostIdField.ID)
require.Nil(t, appErr)
require.Len(t, reviewerPostIds, 0)
})
}
func TestPostReviewerMessage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test reviewer message"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Verify message was posted to the reviewer thread
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
dmChannel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Find the original review post and the test message
var reviewPost *model.Post
var testMessagePost *model.Post
for _, p := range posts.Posts {
if p.Type == "custom_spillage_report" {
reviewPost = p
} else if p.RootId != "" && p.Message == testMessage {
testMessagePost = p
}
}
require.NotNil(t, reviewPost)
require.NotNil(t, testMessagePost)
require.Equal(t, reviewPost.Id, testMessagePost.RootId)
require.Equal(t, contentReviewBot.UserId, testMessagePost.UserId)
})
t.Run("should handle multiple reviewer channels", func(t *testing.T) {
// Create a config with multiple reviewers
config := getBaseConfig(th)
config.ReviewerSettings.CommonReviewerIds = []string{th.BasicUser.Id, th.BasicUser2.Id}
appErr := th.App.SaveContentFlaggingConfig(config)
require.Nil(t, appErr)
post := th.CreatePost(th.BasicChannel)
flagData := model.FlagContentRequest{
Reason: "spam",
Comment: "This is spam content",
}
appErr = th.App.FlagPost(th.Context, post, th.BasicTeam.Id, th.SystemAdminUser.Id, flagData)
require.Nil(t, appErr)
// Wait for async reviewer post creation to complete
time.Sleep(2 * time.Second)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message for multiple reviewers"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
// Verify message was posted to both reviewer threads
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Check first reviewer's channel
dmChannel1, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts1, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel1.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Check second reviewer's channel
dmChannel2, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser2.Id, contentReviewBot.UserId)
require.Nil(t, appErr)
posts2, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: dmChannel2.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Verify test message exists in both channels
var testMessagePost1, testMessagePost2 *model.Post
for _, p := range posts1.Posts {
if p.RootId != "" && p.Message == testMessage {
testMessagePost1 = p
break
}
}
for _, p := range posts2.Posts {
if p.RootId != "" && p.Message == testMessage {
testMessagePost2 = p
break
}
}
require.NotNil(t, testMessagePost1)
require.NotNil(t, testMessagePost2)
require.Equal(t, contentReviewBot.UserId, testMessagePost1.UserId)
require.Equal(t, contentReviewBot.UserId, testMessagePost2.UserId)
})
t.Run("should handle case when no reviewer posts exist", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post := th.CreatePost(th.BasicChannel)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message for non-flagged post"
_, appErr = th.App.postReviewerMessage(th.Context, testMessage, groupId, post.Id)
require.Nil(t, appErr)
})
t.Run("should handle message with special characters", func(t *testing.T) {
require.Nil(t, setBaseConfig(th))
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
testMessage := "Test message with special chars: @user #channel ~team & "
_, 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()
defer th.TearDown()
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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
actorComment := "This post violates community guidelines"
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The flagged 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 flagged 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 flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// Setup notification config for only author
appErr = setupNotificationConfig(th, map[model.ContentFlaggingEvent][]model.NotificationTarget{
model.EventContentRemoved: {model.TargetReviewers},
})
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "Test comment", groupId)
// Should create only 1 post for author
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
specialComment := "Comment with @mentions #channels ~teams & "
createdPosts := th.App.sendFlaggedPostRemovalNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged 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()
defer th.TearDown()
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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
actorComment := "This post is acceptable after review"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, actorComment, groupId)
// Should create 3 posts: reviewer message, author message, reporter message
require.Len(t, createdPosts, 3)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
// Verify reviewer message
reviewerMessage := fmt.Sprintf("The flagged 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 flagged 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 flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
comment := "Test comment"
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, comment, groupId)
// Should create only 1 post for reporter
require.Len(t, createdPosts, 1)
contentReviewBot, appErr := th.App.getContentReviewBot(th.Context)
require.Nil(t, appErr)
expectedMessage := fmt.Sprintf("The flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, "", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
specialComment := "Comment with @mentions #channels ~teams & "
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.SystemAdminUser.Id, specialComment, groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
// Use BasicUser as actor instead of SystemAdminUser
createdPosts := th.App.sendKeepFlaggedPostNotification(th.Context, post, th.BasicUser.Id, "Reviewed by different user", groupId)
require.Len(t, createdPosts, 1)
expectedMessage := fmt.Sprintf("The flagged 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()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.Nil(t, setBaseConfig(th))
t.Run("should successfully permanently delete pending flagged post", func(t *testing.T) {
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "This post violates community guidelines",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify post content was scrubbed
updatedPost, appErr := th.App.GetSinglePost(th.Context, post.Id, true)
require.Nil(t, appErr) // Post should be deleted
require.Greater(t, updatedPost.DeleteAt, int64(0))
// Verify status was updated to removed
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
require.Equal(t, `"`+model.ContentFlaggingStatusRemoved+`"`, string(statusValue.Value))
// Verify actor properties were created
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
// Check actor user property
actorValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorUserID].ID,
})
require.NoError(t, err)
require.Len(t, actorValues, 1)
require.Equal(t, `"`+th.SystemAdminUser.Id+`"`, string(actorValues[0].Value))
// Check actor comment property
commentValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.NoError(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `"This post violates community guidelines"`, string(commentValues[0].Value))
// Check action time property
timeValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActionTime].ID,
})
require.NoError(t, err)
require.Len(t, timeValues, 1)
var actionTime int64
err = json.Unmarshal(timeValues[0].Value, &actionTime)
require.NoError(t, err)
require.True(t, actionTime > 0)
})
t.Run("should successfully permanently delete pending flagged post when flagged posts are hidden", func(t *testing.T) {
baseConfig := getBaseConfig(th)
baseConfig.AdditionalSettings.HideFlaggedContent = model.NewPointer(true)
appErr := th.App.SaveContentFlaggingConfig(baseConfig)
require.Nil(t, appErr)
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// 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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// Set status to removed
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRemoved))
_, err := th.App.Srv().propertyService.UpdatePropertyValue(groupId, statusValue)
require.NoError(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete already removed post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.content_flagging.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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
// Set status to retained
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
statusValue, appErr := th.App.GetPostContentFlaggingPropertyValue(post.Id, ContentFlaggingPropertyNameStatus)
require.Nil(t, appErr)
statusValue.Value = json.RawMessage(fmt.Sprintf(`"%s"`, model.ContentFlaggingStatusRetained))
_, err := th.App.Srv().propertyService.UpdatePropertyValue(groupId, statusValue)
require.NoError(t, err)
actionRequest := &model.FlagContentActionRequest{
Comment: "Trying to delete retained post",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.NotNil(t, appErr)
require.Equal(t, "api.content_flagging.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(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, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
actionRequest := &model.FlagContentActionRequest{
Comment: "",
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify empty comment was stored
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.NoError(t, err)
require.Len(t, commentValues, 1)
require.Equal(t, `""`, string(commentValues[0].Value))
})
t.Run("should handle special characters in comment", func(t *testing.T) {
post, appErr := setupFlaggedPost(th)
require.Nil(t, appErr)
specialComment := "Comment with @mentions #channels ~teams & "
actionRequest := &model.FlagContentActionRequest{
Comment: specialComment,
}
appErr = th.App.PermanentDeleteFlaggedPost(th.Context, actionRequest, th.SystemAdminUser.Id, post)
require.Nil(t, appErr)
// Verify special characters were properly escaped and stored
groupId, appErr := th.App.ContentFlaggingGroupId()
require.Nil(t, appErr)
mappedFields, appErr := th.App.GetContentFlaggingMappedFields(groupId)
require.Nil(t, appErr)
commentValues, err := th.Server.propertyService.SearchPropertyValues(groupId, model.PropertyValueSearchOpts{
TargetIDs: []string{post.Id},
PerPage: CONTENT_FLAGGING_MAX_PROPERTY_VALUES,
FieldID: mappedFields[contentFlaggingPropertyNameActorComment].ID,
})
require.NoError(t, err)
require.Len(t, commentValues, 1)
var storedComment string
err = json.Unmarshal(commentValues[0].Value, &storedComment)
require.NoError(t, err)
require.Equal(t, specialComment, storedComment)
})
t.Run("should handle post with file attachments", func(t *testing.T) {
// Create a post with file attachments
post := th.CreatePost(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 true
}, 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(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 true
}, 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(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 true
}, 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))
})
}