mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* MM-68952: Resolve public channel mentions for non-members under Compliance Channel mention name resolution reused HasPermissionToReadChannel, a content-read check that returns false for non-members of a public channel when Compliance Monitoring is enabled (MM-45272) or when the channel is on another team (MM-66791). As a result, the channel_mentions post prop was stripped per-viewer (since #34235), and the webapp fell back to rendering the raw (anonymized) channel slug instead of a clickable link. Introduce HasPermissionToResolveChannelMention, which exposes only a public channel display name and link (not content) and is therefore independent of ComplianceSettings, while still requiring team membership for public channels (blocks cross-team disclosure) and channel membership for private/DM/GM channels. Switch the three mention call sites (FillInPostProps, sanitizeChannelMentionsForUser, channelMentionsBroadcastHook) to the new helper. HasPermissionToReadChannel and all content-read paths are unchanged. * MM-68952: Add author-side and E2E coverage for channel mention resolution Add a Go test (TestFillInPostPropsChannelMentionResolution) that exercises the author-side persistence of the channel_mentions prop in FillInPostProps. It locks in the new behavior: an author who is a team member but not a member of a referenced public channel now persists the mention prop even when Compliance Monitoring is enabled, while public channels on other teams and private channels the author is not in are still dropped. Add a Playwright spec (channel_mention_resolution.spec.ts) with a license-free cross-team case (a public channel mention stays unresolved for a viewer not on the channel's team) and a license-gated case (with Compliance enabled, a team member who is not in the channel sees the resolved mention link). * MM-68952: Tighten channel mention test assertions and fix lint Use strings.Builder when assembling the test message to avoid the golangci-lint stringsbuilder (modernize) warning about string += string in a loop. Tighten the cross-team public and private non-member cases to assert.Nil on the resolved mentions so the contract requires no persisted channel_mentions map rather than merely an empty one. --------- Co-authored-by: Hanzo Dev <[email protected]>
1159 lines
38 KiB
Go
1159 lines
38 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
|
|
platform_mocks "github.com/mattermost/mattermost/server/v8/channels/app/platform/mocks"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAddMentionsHook_Process(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
hook := &addMentionsBroadcastHook{}
|
|
|
|
userID := model.NewId()
|
|
otherUserID := model.NewId()
|
|
|
|
webConn := &platform.WebConn{
|
|
UserId: userID,
|
|
}
|
|
|
|
t.Run("should add a mentions entry for the current user", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
require.Nil(t, msg.Event().GetData()["mentions"])
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"mentions": model.StringArray{userID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, `["`+userID+`"]`, msg.Event().GetData()["mentions"])
|
|
assert.Nil(t, msg.Event().GetData()["followers"])
|
|
})
|
|
|
|
t.Run("should not add a mentions entry for another user", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
require.Nil(t, msg.Event().GetData()["mentions"])
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"mentions": model.StringArray{otherUserID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, msg.Event().GetData()["mentions"])
|
|
})
|
|
}
|
|
|
|
func TestAddFollowersHook_Process(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
hook := &addFollowersBroadcastHook{}
|
|
|
|
userID := model.NewId()
|
|
otherUserID := model.NewId()
|
|
|
|
webConn := &platform.WebConn{
|
|
UserId: userID,
|
|
}
|
|
|
|
t.Run("should add a followers entry for the current user", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
require.Nil(t, msg.Event().GetData()["followers"])
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"followers": model.StringArray{userID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, `["`+userID+`"]`, msg.Event().GetData()["followers"])
|
|
})
|
|
|
|
t.Run("should not add a followers entry for another user", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
require.Nil(t, msg.Event().GetData()["followers"])
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"followers": model.StringArray{otherUserID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, msg.Event().GetData()["followers"])
|
|
})
|
|
}
|
|
|
|
func TestPostedAckHook_Process(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
hook := &postedAckBroadcastHook{}
|
|
userID := model.NewId()
|
|
webConn := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
PostedAck: true,
|
|
}
|
|
webConn.Active.Store(true)
|
|
webConn.SetSession(&model.Session{})
|
|
|
|
t.Run("should ack if user is in the list of users to notify", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"posted_user_id": model.NewId(),
|
|
"channel_type": model.ChannelTypeOpen,
|
|
"users": []string{userID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, msg.Event().GetData()["should_ack"].(bool))
|
|
})
|
|
|
|
t.Run("should not ack if user is not in the list of users to notify", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"posted_user_id": model.NewId(),
|
|
"channel_type": model.ChannelTypeOpen,
|
|
"users": []string{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, msg.Event().GetData()["should_ack"])
|
|
})
|
|
|
|
t.Run("should not ack if you are the user who posted", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"posted_user_id": userID,
|
|
"channel_type": model.ChannelTypeOpen,
|
|
"users": []string{userID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, msg.Event().GetData()["should_ack"])
|
|
})
|
|
|
|
t.Run("should ack if the channel is a DM", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
err := hook.Process(msg, webConn, map[string]any{
|
|
"posted_user_id": model.NewId(),
|
|
"channel_type": model.ChannelTypeDirect,
|
|
"users": []string{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, msg.Event().GetData()["should_ack"].(bool))
|
|
})
|
|
|
|
t.Run("should not ack if posted ack is false", func(t *testing.T) {
|
|
noAckWebConn := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
PostedAck: false,
|
|
}
|
|
noAckWebConn.Active.Store(true)
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
err := hook.Process(msg, noAckWebConn, map[string]any{
|
|
"posted_user_id": model.NewId(),
|
|
"channel_type": model.ChannelTypeDirect,
|
|
"users": []string{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, msg.Event().GetData()["should_ack"])
|
|
})
|
|
|
|
t.Run("should not ack if connection is not active", func(t *testing.T) {
|
|
inactiveWebConn := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
PostedAck: false,
|
|
}
|
|
inactiveWebConn.Active.Store(true)
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
err := hook.Process(msg, inactiveWebConn, map[string]any{
|
|
"posted_user_id": model.NewId(),
|
|
"channel_type": model.ChannelTypeDirect,
|
|
"users": []string{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, msg.Event().GetData()["should_ack"])
|
|
})
|
|
}
|
|
|
|
func TestAddMentionsAndAddFollowersHooks(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
addMentionsHook := &addMentionsBroadcastHook{}
|
|
addFollowersHook := &addFollowersBroadcastHook{}
|
|
|
|
userID := model.NewId()
|
|
|
|
webConn := &platform.WebConn{
|
|
UserId: userID,
|
|
}
|
|
|
|
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
|
|
|
|
originalData := msg.Event().GetData()
|
|
|
|
require.Nil(t, originalData["mentions"])
|
|
require.Nil(t, originalData["followers"])
|
|
|
|
err := addMentionsHook.Process(msg, webConn, map[string]any{
|
|
"mentions": model.StringArray{userID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = addFollowersHook.Process(msg, webConn, map[string]any{
|
|
"followers": model.StringArray{userID},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("should be able to add both mentions and followers to a single event", func(t *testing.T) {
|
|
assert.Equal(t, `["`+userID+`"]`, msg.Event().GetData()["followers"])
|
|
assert.Equal(t, `["`+userID+`"]`, msg.Event().GetData()["mentions"])
|
|
})
|
|
}
|
|
|
|
func TestPermalinkBroadcastHook(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
session, err := th.Server.Platform().CreateSession(th.Context, &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
wc := &platform.WebConn{
|
|
Platform: th.Server.Platform(),
|
|
Suite: th.App,
|
|
UserId: session.UserId,
|
|
}
|
|
hook := &permalinkBroadcastHook{}
|
|
|
|
refPost := th.CreatePost(t, th.BasicChannel)
|
|
previewPost := model.NewPreviewPost(refPost, th.BasicTeam, th.BasicChannel)
|
|
|
|
// Create a clean post (no metadata)
|
|
cleanPost := th.BasicPost.Clone()
|
|
cleanPost.Metadata = &model.PostMetadata{}
|
|
cleanJSON, err := cleanPost.ToJSON()
|
|
require.NoError(t, err)
|
|
|
|
wsEvent := model.NewWebSocketEvent(model.WebsocketEventPosted, "", th.BasicPost.ChannelId, "", nil, "")
|
|
wsEvent.Add("post", cleanJSON)
|
|
|
|
t.Run("should add permalink metadata when user has permission", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"preview_channel": th.BasicChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": refPost.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
// Verify permalink metadata was added
|
|
assert.Equal(t, refPost.Id, gotPost.GetPreviewedPostProp())
|
|
assert.Len(t, gotPost.Metadata.Embeds, 1)
|
|
assert.Equal(t, model.PostEmbedPermalink, gotPost.Metadata.Embeds[0].Type)
|
|
})
|
|
|
|
t.Run("should not add permalink metadata when user lacks permission", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
|
|
// User does not exist, and thus won't have permission to the channel
|
|
noPermWc := &platform.WebConn{
|
|
Platform: th.Server.Platform(),
|
|
Suite: th.App,
|
|
UserId: "otheruser",
|
|
}
|
|
|
|
err = hook.Process(msg, noPermWc, map[string]any{
|
|
"preview_channel": th.BasicChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": refPost.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
// Post should remain clean (no metadata added)
|
|
assert.Equal(t, cleanJSON, gotJSON)
|
|
})
|
|
}
|
|
|
|
func TestChannelMentionsBroadcastHook(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
session, err := th.Server.Platform().CreateSession(th.Context, &model.Session{
|
|
UserId: th.BasicUser.Id,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
wc := &platform.WebConn{
|
|
Platform: th.Server.Platform(),
|
|
Suite: th.App,
|
|
UserId: session.UserId,
|
|
}
|
|
hook := &channelMentionsBroadcastHook{}
|
|
|
|
// Create a private channel that BasicUser doesn't have access to
|
|
// Use BasicUser2 to create it so BasicUser is never added
|
|
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
// Remove BasicUser if they were added automatically
|
|
_ = th.App.RemoveUserFromChannel(th.Context, th.BasicUser.Id, "", privateChannel)
|
|
// Ensure BasicUser2 is a member
|
|
th.AddUserToChannel(t, th.BasicUser2, privateChannel)
|
|
|
|
// Create channel mentions map with both channels
|
|
channelMentions := map[string]any{
|
|
th.BasicChannel.Name: map[string]any{
|
|
"id": th.BasicChannel.Id,
|
|
"display_name": th.BasicChannel.DisplayName,
|
|
},
|
|
privateChannel.Name: map[string]any{
|
|
"id": privateChannel.Id,
|
|
"display_name": privateChannel.DisplayName,
|
|
},
|
|
}
|
|
|
|
// Create a clean post
|
|
cleanPost := th.BasicPost.Clone()
|
|
cleanPost.Metadata = &model.PostMetadata{}
|
|
cleanJSON, err := cleanPost.ToJSON()
|
|
require.NoError(t, err)
|
|
|
|
wsEvent := model.NewWebSocketEvent(model.WebsocketEventPosted, "", th.BasicPost.ChannelId, "", nil, "")
|
|
wsEvent.Add("post", cleanJSON)
|
|
|
|
t.Run("should filter channel mentions based on user permissions", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": channelMentions,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only the permitted channel mention was added
|
|
mentions := gotPost.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
|
|
// Should have access to BasicChannel
|
|
assert.Contains(t, mentionsMap, th.BasicChannel.Name)
|
|
// Should NOT have access to privateChannel
|
|
assert.NotContains(t, mentionsMap, privateChannel.Name)
|
|
})
|
|
|
|
t.Run("should not add channel mentions when user has no permission to any", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
|
|
// Only include the private channel
|
|
privateOnlyMentions := map[string]any{
|
|
privateChannel.Name: map[string]any{
|
|
"id": privateChannel.Id,
|
|
"display_name": privateChannel.DisplayName,
|
|
},
|
|
}
|
|
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": privateOnlyMentions,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
// Should have no channel mentions
|
|
mentions := gotPost.GetProp(model.PostPropsChannelMentions)
|
|
assert.Nil(t, mentions)
|
|
})
|
|
|
|
t.Run("should accumulate with existing post metadata", func(t *testing.T) {
|
|
// Create a post that already has some metadata (e.g., from permalink hook)
|
|
refPost := th.CreatePost(t, th.BasicChannel)
|
|
postWithMetadata := th.BasicPost.Clone()
|
|
postWithMetadata.Metadata = &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{Type: model.PostEmbedPermalink, Data: model.NewPreviewPost(refPost, th.BasicTeam, th.BasicChannel)},
|
|
},
|
|
}
|
|
postWithMetadataJSON, jsonErr := postWithMetadata.ToJSON()
|
|
require.NoError(t, jsonErr)
|
|
|
|
wsEventWithMeta := model.NewWebSocketEvent(model.WebsocketEventPosted, "", th.BasicPost.ChannelId, "", nil, "")
|
|
wsEventWithMeta.Add("post", postWithMetadataJSON)
|
|
msg := platform.MakeHookedWebSocketEvent(wsEventWithMeta)
|
|
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": map[string]any{
|
|
th.BasicChannel.Name: map[string]any{
|
|
"id": th.BasicChannel.Id,
|
|
"display_name": th.BasicChannel.DisplayName,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
// Should have BOTH the permalink embed AND channel mentions
|
|
assert.Len(t, gotPost.Metadata.Embeds, 1, "Permalink embed should be preserved")
|
|
assert.Equal(t, model.PostEmbedPermalink, gotPost.Metadata.Embeds[0].Type)
|
|
|
|
mentions := gotPost.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions, "Channel mentions should be added")
|
|
})
|
|
|
|
t.Run("should handle empty channel mentions", func(t *testing.T) {
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": map[string]any{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Should return early without error
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
assert.Equal(t, cleanJSON, gotJSON)
|
|
})
|
|
|
|
t.Run("retains same-team public channel mention for non-member recipient under compliance", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(c *model.Config) {
|
|
c.ComplianceSettings.Enable = model.NewPointer(true)
|
|
})
|
|
defer th.App.UpdateConfig(func(c *model.Config) {
|
|
c.ComplianceSettings.Enable = model.NewPointer(false)
|
|
})
|
|
|
|
// BasicUser is a BasicTeam member (via InitBasic) but NOT a member of this fresh
|
|
// channel (created without adding the creator as a member).
|
|
pub, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "name_" + model.NewId(),
|
|
DisplayName: "Public Channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, appErr)
|
|
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": map[string]any{
|
|
pub.Name: map[string]any{
|
|
"id": pub.Id,
|
|
"display_name": pub.DisplayName,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
mentions := gotPost.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
assert.Contains(t, mentionsMap, pub.Name)
|
|
})
|
|
|
|
t.Run("strips cross-team public channel mention for non-member recipient regardless of compliance", func(t *testing.T) {
|
|
team2 := th.CreateTeam(t)
|
|
// Created without adding the creator so BasicUser is neither a team nor channel member.
|
|
ch, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
|
TeamId: team2.Id,
|
|
Name: "name_" + model.NewId(),
|
|
DisplayName: "Cross Team Channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, appErr)
|
|
|
|
for _, compliance := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("compliance=%t", compliance), func(t *testing.T) {
|
|
th.App.UpdateConfig(func(c *model.Config) {
|
|
c.ComplianceSettings.Enable = model.NewPointer(compliance)
|
|
})
|
|
defer th.App.UpdateConfig(func(c *model.Config) {
|
|
c.ComplianceSettings.Enable = model.NewPointer(false)
|
|
})
|
|
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": map[string]any{
|
|
ch.Name: map[string]any{
|
|
"id": ch.Id,
|
|
"display_name": ch.DisplayName,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, gotPost.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("strips private channel mention for non-member recipient regardless of compliance", func(t *testing.T) {
|
|
for _, compliance := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("compliance=%t", compliance), func(t *testing.T) {
|
|
th.App.UpdateConfig(func(c *model.Config) {
|
|
c.ComplianceSettings.Enable = model.NewPointer(compliance)
|
|
})
|
|
defer th.App.UpdateConfig(func(c *model.Config) {
|
|
c.ComplianceSettings.Enable = model.NewPointer(false)
|
|
})
|
|
|
|
msg := platform.MakeHookedWebSocketEvent(wsEvent)
|
|
err = hook.Process(msg, wc, map[string]any{
|
|
"channel_mentions": map[string]any{
|
|
privateChannel.Name: map[string]any{
|
|
"id": privateChannel.Id,
|
|
"display_name": privateChannel.DisplayName,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
|
|
var gotPost model.Post
|
|
err = json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, gotPost.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPermalinkBroadcastHook_AbacFileStripping(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
hook := &permalinkBroadcastHook{}
|
|
|
|
userID := model.NewId()
|
|
postChannelID := model.NewId()
|
|
refChannelID := model.NewId()
|
|
|
|
previewChannel := &model.Channel{Id: refChannelID, Name: "ref-channel", Type: model.ChannelTypeOpen}
|
|
|
|
// makeRefPost builds a PreviewPost with optional file attachments.
|
|
makeRefPost := func(fileIDs []string, files []*model.FileInfo) *model.PreviewPost {
|
|
refPost := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: refChannelID,
|
|
FileIds: fileIDs,
|
|
Metadata: &model.PostMetadata{Files: files},
|
|
}
|
|
return &model.PreviewPost{
|
|
PostID: refPost.Id,
|
|
Post: refPost,
|
|
}
|
|
}
|
|
|
|
// makeOuterPostJSON creates a clean outer post JSON to seed the WS event.
|
|
makeOuterPostJSON := func(t *testing.T) string {
|
|
t.Helper()
|
|
outerPost := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: postChannelID,
|
|
Metadata: &model.PostMetadata{},
|
|
}
|
|
postJSON, err := outerPost.ToJSON()
|
|
require.NoError(t, err)
|
|
return postJSON
|
|
}
|
|
|
|
makeMessage := func(postJSON string) *platform.HookedWebSocketEvent {
|
|
event := model.NewWebSocketEvent(model.WebsocketEventPosted, "", postChannelID, "", nil, "")
|
|
event.Add("post", postJSON)
|
|
return platform.MakeHookedWebSocketEvent(event)
|
|
}
|
|
|
|
makeWebConn := func(t *testing.T, allowed bool) *platform.WebConn {
|
|
t.Helper()
|
|
mockSuite := &platform_mocks.SuiteIFace{}
|
|
mockSuite.On("HasPermissionToReadChannel", mock.Anything, userID, previewChannel).Return(true, true)
|
|
mockSuite.On("HasPermissionToFileAction", mock.Anything, userID, mock.AnythingOfType("string"), refChannelID, model.AccessControlPolicyActionDownloadFileAttachment).Return(allowed)
|
|
mockSuite.On("MakeAuditRecord", mock.Anything, mock.Anything, mock.Anything).Return(&model.AuditRecord{})
|
|
mockSuite.On("LogAuditRec", mock.Anything, mock.Anything, mock.Anything).Return()
|
|
wc := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
Suite: mockSuite,
|
|
}
|
|
session := &model.Session{UserId: userID, Roles: model.SystemUserRoleId}
|
|
wc.SetSession(session)
|
|
return wc
|
|
}
|
|
|
|
// makeWebConnNoFileCheck builds a webConn whose mock does NOT expect HasPermissionToFileAction.
|
|
makeWebConnNoFileCheck := func(t *testing.T) *platform.WebConn {
|
|
t.Helper()
|
|
mockSuite := &platform_mocks.SuiteIFace{}
|
|
mockSuite.On("HasPermissionToReadChannel", mock.Anything, userID, previewChannel).Return(true, true)
|
|
mockSuite.On("MakeAuditRecord", mock.Anything, mock.Anything, mock.Anything).Return(&model.AuditRecord{})
|
|
mockSuite.On("LogAuditRec", mock.Anything, mock.Anything, mock.Anything).Return()
|
|
wc := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
Suite: mockSuite,
|
|
}
|
|
session := &model.Session{UserId: userID, Roles: model.SystemUserRoleId}
|
|
wc.SetSession(session)
|
|
return wc
|
|
}
|
|
|
|
extractPost := func(t *testing.T, msg *platform.HookedWebSocketEvent) *model.Post {
|
|
t.Helper()
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
var gotPost model.Post
|
|
err := json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
return &gotPost
|
|
}
|
|
|
|
t.Run("files stripped from embed when user denied download", func(t *testing.T) {
|
|
postJSON := makeOuterPostJSON(t)
|
|
msg := makeMessage(postJSON)
|
|
wc := makeWebConn(t, false)
|
|
|
|
fileID := model.NewId()
|
|
previewPost := makeRefPost(
|
|
model.StringArray{fileID},
|
|
[]*model.FileInfo{{Id: fileID, Name: "photo.png", Extension: "png"}},
|
|
)
|
|
// Save the original Post pointer to verify the hook does not replace it.
|
|
origPostPtr := previewPost.Post
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"preview_channel": previewChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": previewPost.PostID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
require.Len(t, gotPost.Metadata.Embeds, 1)
|
|
assert.Equal(t, model.PostEmbedPermalink, gotPost.Metadata.Embeds[0].Type)
|
|
|
|
// The embed's data is serialized/deserialized as JSON — inspect via raw JSON.
|
|
gotJSON, _ := msg.Get("post").(string)
|
|
assert.NotContains(t, gotJSON, fileID, "file ID should be stripped from embed")
|
|
assert.Contains(t, gotJSON, `"redacted_file_count":1`, "redacted count should reflect the stripped file")
|
|
|
|
// The hook creates a previewCopy with a cloned Post; the original PreviewPost.Post
|
|
// pointer must not be swapped out.
|
|
assert.Same(t, origPostPtr, previewPost.Post, "original PreviewPost.Post pointer must not be replaced")
|
|
})
|
|
|
|
t.Run("files preserved in embed when user allowed download", func(t *testing.T) {
|
|
postJSON := makeOuterPostJSON(t)
|
|
msg := makeMessage(postJSON)
|
|
wc := makeWebConn(t, true)
|
|
|
|
fileID := model.NewId()
|
|
previewPost := makeRefPost(
|
|
model.StringArray{fileID},
|
|
[]*model.FileInfo{{Id: fileID, Name: "photo.png", Extension: "png"}},
|
|
)
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"preview_channel": previewChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": previewPost.PostID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
require.Len(t, gotPost.Metadata.Embeds, 1)
|
|
|
|
gotJSON, _ := msg.Get("post").(string)
|
|
assert.Contains(t, gotJSON, fileID, "file ID should be present in the embed")
|
|
assert.NotContains(t, gotJSON, `"redacted_file_count"`, "no redaction should occur")
|
|
})
|
|
|
|
t.Run("files stripped when FileIds non-empty but Metadata.Files already nil", func(t *testing.T) {
|
|
postJSON := makeOuterPostJSON(t)
|
|
msg := makeMessage(postJSON)
|
|
wc := makeWebConn(t, false)
|
|
|
|
fileID := model.NewId()
|
|
// FileIds set but Metadata.Files nil — simulates upstream stripping.
|
|
previewPost := makeRefPost(
|
|
model.StringArray{fileID},
|
|
nil,
|
|
)
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"preview_channel": previewChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": previewPost.PostID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
require.Len(t, gotPost.Metadata.Embeds, 1)
|
|
|
|
gotJSON, _ := msg.Get("post").(string)
|
|
assert.NotContains(t, gotJSON, fileID, "file ID should be cleared")
|
|
// RedactedFileCount falls back to len(FileIds) when Metadata.Files is nil,
|
|
// so the placeholder count is 1 even though Files was already cleared upstream.
|
|
assert.Contains(t, gotJSON, `"redacted_file_count":1`, "file count falls back to FileIds length")
|
|
})
|
|
|
|
t.Run("no file stripping when embed has no files", func(t *testing.T) {
|
|
postJSON := makeOuterPostJSON(t)
|
|
msg := makeMessage(postJSON)
|
|
// Use webConn that does NOT expect HasPermissionToFileAction to be called.
|
|
wc := makeWebConnNoFileCheck(t)
|
|
|
|
previewPost := makeRefPost(model.StringArray{}, nil)
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"preview_channel": previewChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": previewPost.PostID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
require.Len(t, gotPost.Metadata.Embeds, 1)
|
|
|
|
gotJSON, _ := msg.Get("post").(string)
|
|
assert.NotContains(t, gotJSON, `"redacted_file_count"`, "no redaction metadata should be present")
|
|
})
|
|
|
|
t.Run("nil session: fail-secure, files stripped from permalink preview", func(t *testing.T) {
|
|
postJSON := makeOuterPostJSON(t)
|
|
msg := makeMessage(postJSON)
|
|
|
|
// WebConn with no session — cannot evaluate permissions, must strip files.
|
|
mockSuite := &platform_mocks.SuiteIFace{}
|
|
mockSuite.On("HasPermissionToReadChannel", mock.Anything, userID, previewChannel).Return(true, true)
|
|
mockSuite.On("MakeAuditRecord", mock.Anything, mock.Anything, mock.Anything).Return(&model.AuditRecord{})
|
|
mockSuite.On("LogAuditRec", mock.Anything, mock.Anything, mock.Anything).Return()
|
|
wc := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
Suite: mockSuite,
|
|
}
|
|
// Do NOT call wc.SetSession — session remains nil
|
|
|
|
fileID := model.NewId()
|
|
previewPost := makeRefPost(
|
|
model.StringArray{fileID},
|
|
[]*model.FileInfo{{Id: fileID, Name: "doc.pdf", Extension: "pdf"}},
|
|
)
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"preview_channel": previewChannel,
|
|
"permalink_previewed_post": previewPost,
|
|
"preview_prop": previewPost.PostID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
require.Len(t, gotPost.Metadata.Embeds, 1)
|
|
|
|
gotJSON, _ := msg.Get("post").(string)
|
|
assert.NotContains(t, gotJSON, fileID, "file ID should be stripped when session is nil")
|
|
assert.Contains(t, gotJSON, `"redacted_file_count":1`, "redacted count should be set when session is nil")
|
|
})
|
|
}
|
|
|
|
func TestAbacFilesBroadcastHook_Process(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
hook := &abacFilesBroadcastHook{}
|
|
|
|
userID := model.NewId()
|
|
channelID := model.NewId()
|
|
|
|
makePostWithFiles := func(t *testing.T, fileCount int) (*model.Post, string) {
|
|
t.Helper()
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: channelID,
|
|
FileIds: make(model.StringArray, fileCount),
|
|
Metadata: &model.PostMetadata{
|
|
Files: make([]*model.FileInfo, fileCount),
|
|
},
|
|
}
|
|
for i := range fileCount {
|
|
fid := model.NewId()
|
|
post.FileIds[i] = fid
|
|
post.Metadata.Files[i] = &model.FileInfo{Id: fid, Name: "file.txt", Extension: "txt"}
|
|
}
|
|
postJSON, err := post.ToJSON()
|
|
require.NoError(t, err)
|
|
return post, postJSON
|
|
}
|
|
|
|
makeMessage := func(channelID, postJSON string) *platform.HookedWebSocketEvent {
|
|
event := model.NewWebSocketEvent(model.WebsocketEventPosted, "", channelID, "", nil, "")
|
|
event.Add("post", postJSON)
|
|
return platform.MakeHookedWebSocketEvent(event)
|
|
}
|
|
|
|
makeArgs := func(channelID string, fileCount int) map[string]any {
|
|
return map[string]any{
|
|
"channel_id": channelID,
|
|
"file_count": fileCount,
|
|
}
|
|
}
|
|
|
|
makeWebConn := func(t *testing.T, userID string, allowed bool) *platform.WebConn {
|
|
t.Helper()
|
|
mockSuite := &platform_mocks.SuiteIFace{}
|
|
mockSuite.On("HasPermissionToFileAction", mock.Anything, userID, mock.AnythingOfType("string"), channelID, model.AccessControlPolicyActionDownloadFileAttachment).Return(allowed)
|
|
wc := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
Suite: mockSuite,
|
|
}
|
|
session := &model.Session{UserId: userID, Roles: model.SystemUserRoleId}
|
|
wc.SetSession(session)
|
|
return wc
|
|
}
|
|
|
|
extractPost := func(t *testing.T, msg *platform.HookedWebSocketEvent) *model.Post {
|
|
t.Helper()
|
|
gotJSON, ok := msg.Get("post").(string)
|
|
require.True(t, ok)
|
|
var gotPost model.Post
|
|
err := json.Unmarshal([]byte(gotJSON), &gotPost)
|
|
require.NoError(t, err)
|
|
return &gotPost
|
|
}
|
|
|
|
t.Run("allowed user: post is unchanged", func(t *testing.T) {
|
|
_, postJSON := makePostWithFiles(t, 2)
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, true)
|
|
|
|
err := hook.Process(msg, wc, makeArgs(channelID, 2))
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
assert.Len(t, gotPost.Metadata.Files, 2)
|
|
assert.Len(t, gotPost.FileIds, 2)
|
|
assert.Equal(t, 0, gotPost.Metadata.RedactedFileCount)
|
|
})
|
|
|
|
t.Run("denied user: files stripped, RedactedFileCount set", func(t *testing.T) {
|
|
_, postJSON := makePostWithFiles(t, 2)
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, false)
|
|
|
|
err := hook.Process(msg, wc, makeArgs(channelID, 2))
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
assert.Nil(t, gotPost.Metadata.Files)
|
|
assert.Empty(t, gotPost.FileIds)
|
|
assert.Equal(t, 2, gotPost.Metadata.RedactedFileCount)
|
|
})
|
|
|
|
t.Run("denied user: count from Metadata.Files when FileIds empty", func(t *testing.T) {
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: channelID,
|
|
FileIds: model.StringArray{},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{
|
|
{Id: model.NewId(), Name: "a.txt", Extension: "txt"},
|
|
{Id: model.NewId(), Name: "b.txt", Extension: "txt"},
|
|
{Id: model.NewId(), Name: "c.txt", Extension: "txt"},
|
|
},
|
|
},
|
|
}
|
|
postJSON, err := post.ToJSON()
|
|
require.NoError(t, err)
|
|
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, false)
|
|
|
|
err = hook.Process(msg, wc, makeArgs(channelID, 3))
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
assert.Equal(t, 3, gotPost.Metadata.RedactedFileCount)
|
|
assert.Nil(t, gotPost.Metadata.Files)
|
|
})
|
|
|
|
t.Run("denied user: fallback to registered fileCount arg when post has no files", func(t *testing.T) {
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: channelID,
|
|
FileIds: model.StringArray{},
|
|
Metadata: &model.PostMetadata{},
|
|
}
|
|
postJSON, err := post.ToJSON()
|
|
require.NoError(t, err)
|
|
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, false)
|
|
|
|
err = hook.Process(msg, wc, makeArgs(channelID, 2))
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
assert.Equal(t, 2, gotPost.Metadata.RedactedFileCount)
|
|
assert.Nil(t, gotPost.Metadata.Files)
|
|
assert.Empty(t, gotPost.FileIds)
|
|
})
|
|
|
|
t.Run("nil session: fail-secure, files stripped", func(t *testing.T) {
|
|
_, postJSON := makePostWithFiles(t, 2)
|
|
msg := makeMessage(channelID, postJSON)
|
|
|
|
// WebConn with no session set — cannot evaluate permissions, must strip files
|
|
wc := &platform.WebConn{
|
|
UserId: userID,
|
|
Platform: &platform.PlatformService{},
|
|
}
|
|
// Do NOT call wc.SetSession — session remains nil
|
|
|
|
err := hook.Process(msg, wc, makeArgs(channelID, 2))
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
assert.Empty(t, gotPost.Metadata.Files)
|
|
assert.Empty(t, gotPost.FileIds)
|
|
assert.Equal(t, 2, gotPost.Metadata.RedactedFileCount)
|
|
})
|
|
|
|
t.Run("missing channel_id arg: returns error", func(t *testing.T) {
|
|
_, postJSON := makePostWithFiles(t, 1)
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, true)
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"file_count": 1,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "channel_id")
|
|
})
|
|
|
|
t.Run("missing file_count arg: returns error", func(t *testing.T) {
|
|
_, postJSON := makePostWithFiles(t, 1)
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, true)
|
|
|
|
err := hook.Process(msg, wc, map[string]any{
|
|
"channel_id": channelID,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "file_count")
|
|
})
|
|
|
|
t.Run("denied user: Metadata.Images is preserved", func(t *testing.T) {
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: channelID,
|
|
FileIds: model.StringArray{model.NewId()},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{
|
|
{Id: model.NewId(), Name: "photo.png", Extension: "png"},
|
|
},
|
|
Images: map[string]*model.PostImage{
|
|
"https://example.com/img.png": {Width: 100, Height: 200},
|
|
},
|
|
},
|
|
}
|
|
postJSON, err := post.ToJSON()
|
|
require.NoError(t, err)
|
|
|
|
msg := makeMessage(channelID, postJSON)
|
|
wc := makeWebConn(t, userID, false)
|
|
|
|
err = hook.Process(msg, wc, makeArgs(channelID, 1))
|
|
require.NoError(t, err)
|
|
|
|
gotPost := extractPost(t, msg)
|
|
assert.Nil(t, gotPost.Metadata.Files)
|
|
assert.Equal(t, 1, gotPost.Metadata.RedactedFileCount)
|
|
assert.NotNil(t, gotPost.Metadata.Images, "Images should be preserved when files are stripped")
|
|
assert.Contains(t, gotPost.Metadata.Images, "https://example.com/img.png")
|
|
})
|
|
}
|
|
|
|
func TestSetupBroadcastHookForAbacFiles(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
t.Run("when ABAC is disabled: hook NOT registered", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = new(false)
|
|
})
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
FileIds: model.StringArray{model.NewId()},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{{Id: model.NewId(), Name: "file.txt", Extension: "txt"}},
|
|
},
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
|
|
|
|
th.App.setupBroadcastHookForAbacFiles(post, message)
|
|
|
|
hooks := message.GetBroadcast().BroadcastHooks
|
|
assert.Empty(t, hooks, "No hooks should be registered when ABAC is disabled")
|
|
})
|
|
|
|
t.Run("when post has no files: hook NOT registered", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = new(true)
|
|
})
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
FileIds: model.StringArray{},
|
|
Metadata: &model.PostMetadata{},
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
|
|
|
|
th.App.setupBroadcastHookForAbacFiles(post, message)
|
|
|
|
hooks := message.GetBroadcast().BroadcastHooks
|
|
assert.Empty(t, hooks, "No hooks should be registered when post has no files")
|
|
})
|
|
|
|
t.Run("when post is burn-on-read: hook NOT registered", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = new(true)
|
|
})
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
Type: model.PostTypeBurnOnRead,
|
|
FileIds: model.StringArray{model.NewId()},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{{Id: model.NewId(), Name: "file.txt", Extension: "txt"}},
|
|
},
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
|
|
|
|
th.App.setupBroadcastHookForAbacFiles(post, message)
|
|
|
|
hooks := message.GetBroadcast().BroadcastHooks
|
|
assert.Empty(t, hooks, "No hooks should be registered for burn-on-read posts")
|
|
})
|
|
|
|
t.Run("when AccessControl is nil: hook NOT registered", func(t *testing.T) {
|
|
// In test setup without enterprise, AccessControl is nil.
|
|
// Even with ABAC config enabled, the nil check on AccessControl
|
|
// prevents registration.
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = new(true)
|
|
})
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
FileIds: model.StringArray{model.NewId()},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{{Id: model.NewId(), Name: "file.txt", Extension: "txt"}},
|
|
},
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
|
|
|
|
th.App.setupBroadcastHookForAbacFiles(post, message)
|
|
|
|
// AccessControl is nil in test env (no enterprise), so hook should not register
|
|
hooks := message.GetBroadcast().BroadcastHooks
|
|
assert.Empty(t, hooks, "No hooks should be registered when AccessControl is nil")
|
|
})
|
|
|
|
t.Run("when post has Metadata.Files but empty FileIds: hook IS registered (when AccessControl available)", func(t *testing.T) {
|
|
// This test verifies the logic path: if AccessControl were non-nil and ABAC enabled,
|
|
// a post with only Metadata.Files (no FileIds) would still trigger hook registration.
|
|
// Since AccessControl is nil in test env, we verify the fileCount logic directly
|
|
// by checking that the function does not register when AccessControl is nil, but the
|
|
// fileCount derivation is tested through the Process tests above.
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.AccessControlSettings.EnableAttributeBasedAccessControl = new(true)
|
|
})
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
FileIds: model.StringArray{},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{
|
|
{Id: model.NewId(), Name: "file.txt", Extension: "txt"},
|
|
},
|
|
},
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
|
|
|
|
th.App.setupBroadcastHookForAbacFiles(post, message)
|
|
|
|
// Without enterprise, AccessControl is nil so hook won't register.
|
|
// The file-count logic is verified via TestAbacFilesBroadcastHook_Process.
|
|
hooks := message.GetBroadcast().BroadcastHooks
|
|
assert.Empty(t, hooks, "AccessControl is nil in test env — hook not registered")
|
|
})
|
|
}
|