mattermost-community-enterp.../channels/app/shared_channel_test.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

856 lines
28 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/platform/services/remotecluster"
"github.com/mattermost/mattermost/server/v8/platform/services/sharedchannel"
)
func setupSharedChannels(tb testing.TB) *TestHelper {
return SetupConfig(tb, func(cfg *model.Config) {
*cfg.ConnectedWorkspacesSettings.EnableRemoteClusterService = true
*cfg.ConnectedWorkspacesSettings.EnableSharedChannels = true
cfg.FeatureFlags.EnableSharedChannelsMemberSync = true
cfg.ClusterSettings.ClusterName = model.NewPointer("test-remote")
})
}
func TestApp_CheckCanInviteToSharedChannel(t *testing.T) {
mainHelper.Parallel(t)
th := setupSharedChannels(t).InitBasic()
channel1 := th.CreateChannel(th.Context, th.BasicTeam)
channel2 := th.CreateChannel(th.Context, th.BasicTeam)
channel3 := th.CreateChannel(th.Context, th.BasicTeam)
data := []struct {
channelID string
home bool
name string
remoteID string
}{
{channelID: channel1.Id, home: true, name: "test_home", remoteID: ""},
{channelID: channel2.Id, home: false, name: "test_remote", remoteID: model.NewId()},
}
for _, d := range data {
sc := &model.SharedChannel{
ChannelId: d.channelID,
TeamId: th.BasicTeam.Id,
Home: d.home,
ShareName: d.name,
CreatorId: th.BasicUser.Id,
RemoteId: d.remoteID,
}
_, err := th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
}
t.Run("Test checkChannelNotShared: not yet shared channel", func(t *testing.T) {
err := th.App.checkChannelNotShared(th.Context, channel3.Id)
assert.NoError(t, err, "unshared channel should not error")
})
t.Run("Test checkChannelNotShared: already shared channel", func(t *testing.T) {
err := th.App.checkChannelNotShared(th.Context, channel1.Id)
assert.Error(t, err, "already shared channel should error")
})
t.Run("Test checkChannelNotShared: invalid channel", func(t *testing.T) {
err := th.App.checkChannelNotShared(th.Context, model.NewId())
assert.Error(t, err, "invalid channel should error")
})
t.Run("Test checkChannelIsShared: not yet shared channel", func(t *testing.T) {
err := th.App.checkChannelIsShared(channel3.Id)
assert.Error(t, err, "unshared channel should error")
})
t.Run("Test checkChannelIsShared: already shared channel", func(t *testing.T) {
err := th.App.checkChannelIsShared(channel1.Id)
assert.NoError(t, err, "already channel should not error")
})
t.Run("Test checkChannelIsShared: invalid channel", func(t *testing.T) {
err := th.App.checkChannelIsShared(model.NewId())
assert.Error(t, err, "invalid channel should error")
})
t.Run("Test CheckCanInviteToSharedChannel: Home shared channel", func(t *testing.T) {
err := th.App.CheckCanInviteToSharedChannel(data[0].channelID)
assert.NoError(t, err, "home channel should allow invites")
})
t.Run("Test CheckCanInviteToSharedChannel: Remote shared channel", func(t *testing.T) {
err := th.App.CheckCanInviteToSharedChannel(data[1].channelID)
assert.Error(t, err, "home channel should not allow invites")
})
t.Run("Test CheckCanInviteToSharedChannel: Invalid shared channel", func(t *testing.T) {
err := th.App.CheckCanInviteToSharedChannel(model.NewId())
assert.Error(t, err, "invalid channel should not allow invites")
})
}
// TestApp_RemoteUnsharing tests the functionality where a shared channel is unshared on one side and triggers an unshare on the remote cluster.
// This test uses a self-referential approach where a server syncs with itself through real HTTP communication.
func TestApp_RemoteUnsharing(t *testing.T) {
th := setupSharedChannels(t).InitBasic()
defer th.TearDown()
ss := th.App.Srv().Store()
// Get the shared channel service and cast to concrete type
scsInterface := th.App.Srv().GetSharedChannelSyncService()
service, ok := scsInterface.(*sharedchannel.Service)
require.True(t, ok, "Expected sharedchannel.Service concrete type")
// Ensure services are active
err := service.Start()
require.NoError(t, err)
rcService := th.App.Srv().GetRemoteClusterService()
if rcService != nil {
_ = rcService.Start()
}
t.Run("remote-initiated unshare with single remote", func(t *testing.T) {
EnsureCleanState(t, th, ss)
var syncHandler *SelfReferentialSyncHandler
// Create a test HTTP server that acts as the "remote" cluster
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if syncHandler != nil {
syncHandler.HandleRequest(w, r)
} else {
writeOKResponse(w)
}
}))
defer testServer.Close()
// Create a self-referential remote cluster
selfCluster := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "test-remote",
DisplayName: "Test Remote",
SiteURL: testServer.URL,
Token: model.NewId(),
CreateAt: model.GetMillis(),
LastPingAt: model.GetMillis(),
CreatorId: th.BasicUser.Id,
RemoteTeamId: model.NewId(),
}
selfCluster, err = ss.RemoteCluster().Save(selfCluster)
require.NoError(t, err)
// Initialize sync handler
syncHandler = NewSelfReferentialSyncHandler(t, service, selfCluster)
// Create a shared channel
channel := th.CreateChannel(th.Context, th.BasicTeam)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: true,
ReadOnly: false,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: th.BasicUser.Id,
RemoteId: "",
}
_, err = th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
// Share the channel with the remote
scr := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: channel.Id,
CreatorId: th.BasicUser.Id,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: selfCluster.RemoteId,
LastPostUpdateAt: model.GetMillis(),
}
_, err = ss.SharedChannel().SaveRemote(scr)
require.NoError(t, err)
// Get post count before "remote-initiated unshare"
postsBeforeRemove, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: channel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
postCountBefore := len(postsBeforeRemove.Posts)
// Verify the channel is initially shared
err = th.App.checkChannelIsShared(channel.Id)
require.NoError(t, err, "Channel should be shared initially")
// Step 1: Verify the channel is initially shared
err = th.App.checkChannelIsShared(channel.Id)
require.NoError(t, err, "Channel should be shared initially")
// Step 2: Create a sync message that would be sent to the remote
syncMsg := model.NewSyncMsg(channel.Id)
syncMsg.Posts = []*model.Post{{
Id: model.NewId(),
ChannelId: channel.Id,
UserId: th.BasicUser.Id,
Message: "Test message after remote unshare",
CreateAt: model.GetMillis(),
}}
// Step 3: Simulate receiving ErrChannelIsNotShared from the remote
// This directly tests the error handling logic without async complexity
service.HandleChannelNotSharedErrorForTesting(syncMsg, selfCluster)
// Step 4: Verify the channel is no longer shared locally
err = th.App.checkChannelIsShared(channel.Id)
assert.Error(t, err, "Channel should no longer be shared after error handling")
// Verify a system message was posted to inform users the channel is no longer shared
postsAfterRemove, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: channel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Expected: only one notification post when a remote is removed
assert.Equal(t, postCountBefore+1, len(postsAfterRemove.Posts), "There should be one new post")
// Find and verify the system message content
var systemPost *model.Post
for _, p := range postsAfterRemove.Posts {
if p.Type == model.PostTypeSystemGeneric {
systemPost = p
break
}
}
require.NotNil(t, systemPost, "A system post should be created")
assert.Equal(t, "This channel is no longer shared.", systemPost.Message, "Message should match unshare message")
})
t.Run("remote-initiated unshare with multiple remotes", func(t *testing.T) {
EnsureCleanState(t, th, ss)
var syncHandler1, syncHandler2 *SelfReferentialSyncHandler
// Create test HTTP servers for both remotes
testServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if syncHandler1 != nil {
syncHandler1.HandleRequest(w, r)
} else {
writeOKResponse(w)
}
}))
defer testServer1.Close()
testServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if syncHandler2 != nil {
syncHandler2.HandleRequest(w, r)
} else {
writeOKResponse(w)
}
}))
defer testServer2.Close()
// Create two self-referential remote clusters
selfCluster1 := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "test-remote-1",
DisplayName: "Test Remote 1",
SiteURL: testServer1.URL,
Token: model.NewId(),
CreateAt: model.GetMillis(),
LastPingAt: model.GetMillis(),
CreatorId: th.BasicUser.Id,
RemoteTeamId: model.NewId(),
}
selfCluster1, err = ss.RemoteCluster().Save(selfCluster1)
require.NoError(t, err)
selfCluster2 := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "test-remote-2",
DisplayName: "Test Remote 2",
SiteURL: testServer2.URL,
Token: model.NewId(),
CreateAt: model.GetMillis(),
LastPingAt: model.GetMillis(),
CreatorId: th.BasicUser.Id,
RemoteTeamId: model.NewId(),
}
selfCluster2, err = ss.RemoteCluster().Save(selfCluster2)
require.NoError(t, err)
// Initialize sync handlers
syncHandler1 = NewSelfReferentialSyncHandler(t, service, selfCluster1)
syncHandler2 = NewSelfReferentialSyncHandler(t, service, selfCluster2)
// Create a shared channel
channel := th.CreateChannel(th.Context, th.BasicTeam)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: true,
ReadOnly: false,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: th.BasicUser.Id,
RemoteId: "",
}
_, err = th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
// Share the channel with both remotes
scr1 := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: channel.Id,
CreatorId: th.BasicUser.Id,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: selfCluster1.RemoteId,
LastPostUpdateAt: model.GetMillis(),
}
_, err = ss.SharedChannel().SaveRemote(scr1)
require.NoError(t, err)
scr2 := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: channel.Id,
CreatorId: th.BasicUser.Id,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: selfCluster2.RemoteId,
LastPostUpdateAt: model.GetMillis(),
}
_, err = ss.SharedChannel().SaveRemote(scr2)
require.NoError(t, err)
// Verify the channel is shared with both remotes
hasRemote1, err := ss.SharedChannel().HasRemote(channel.Id, selfCluster1.RemoteId)
require.NoError(t, err)
require.True(t, hasRemote1, "Channel should be shared with remote 1")
hasRemote2, err := ss.SharedChannel().HasRemote(channel.Id, selfCluster2.RemoteId)
require.NoError(t, err)
require.True(t, hasRemote2, "Channel should be shared with remote 2")
// Step 1: Verify the channel is initially shared with both remotes
err = th.App.checkChannelIsShared(channel.Id)
require.NoError(t, err, "Channel should be shared initially")
// Step 2: Create a post in the channel to trigger sync activity
post := &model.Post{
ChannelId: channel.Id,
UserId: th.BasicUser.Id,
Message: "Test message after remote 1 unshare",
}
_, appErr := th.App.CreatePost(th.Context, post, channel, model.CreatePostFlags{})
require.Nil(t, appErr)
// Get post count after creating the test post but before "remote-initiated unshare"
postsBeforeRemove, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: channel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
postCountBefore := len(postsBeforeRemove.Posts)
// Step 3: Create a sync message for remote 1
syncMsg := model.NewSyncMsg(channel.Id)
syncMsg.Posts = []*model.Post{{
Id: model.NewId(),
ChannelId: channel.Id,
UserId: th.BasicUser.Id,
Message: "Test message after remote 1 unshare",
CreateAt: model.GetMillis(),
}}
// Step 4: Simulate remote 1 returning ErrChannelIsNotShared
service.HandleChannelNotSharedErrorForTesting(syncMsg, selfCluster1)
// Verify we now have only 1 remote (remote 2)
remotes, err := ss.SharedChannel().GetRemotes(0, 10, model.SharedChannelRemoteFilterOpts{
ChannelId: channel.Id,
})
require.NoError(t, err)
require.Len(t, remotes, 1, "Expected 1 remote after removing remote 1")
t.Logf("Number of remotes after unshare: %d", len(remotes))
// The expected behavior is that only the specific remote should be removed,
// with the channel remaining shared with other remotes.
err = th.App.checkChannelIsShared(channel.Id)
// The channel should still be shared with remote2, so this should pass
assert.NoError(t, err, "Channel should still be shared with other remotes")
// Verify remote 1 is no longer in shared channel
hasRemote1After, err := ss.SharedChannel().HasRemote(channel.Id, selfCluster1.RemoteId)
require.NoError(t, err)
require.False(t, hasRemote1After, "Channel should no longer be shared with remote 1")
// Check if remote 2 is still associated with the channel
// Expected behavior: remote 2 should still be associated
hasRemote2After, err := ss.SharedChannel().HasRemote(channel.Id, selfCluster2.RemoteId)
require.NoError(t, err)
assert.True(t, hasRemote2After, "Channel should still be shared with remote 2")
// Verify a system message was posted about remote 1 unsharing
postsAfterRemove, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: channel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Expected: only one notification post when a remote is removed
assert.Equal(t, postCountBefore+1, len(postsAfterRemove.Posts), "There should be one new post")
// Find and verify the system message content
var systemPost *model.Post
for _, p := range postsAfterRemove.Posts {
if p.Type == model.PostTypeSystemGeneric {
systemPost = p
break
}
}
require.NotNil(t, systemPost, "A system post should be created")
assert.Equal(t, "This channel is no longer shared.", systemPost.Message, "Message should match unshare message")
})
}
func TestSyncMessageErrChannelNotSharedResponse(t *testing.T) {
th := setupSharedChannels(t).InitBasic()
defer th.TearDown()
// Setup: Create a shared channel and remote cluster
ss := th.App.Srv().Store()
// Get the shared channel service and cast to concrete type
scsInterface := th.App.Srv().GetSharedChannelSyncService()
service, ok := scsInterface.(*sharedchannel.Service)
require.True(t, ok, "Expected sharedchannel.Service concrete type")
channel := th.CreateChannel(th.Context, th.BasicTeam)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: th.BasicTeam.Id,
Home: true,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
CreatorId: th.BasicUser.Id,
RemoteId: "",
}
_, err := ss.SharedChannel().Save(sc)
require.NoError(t, err)
// Create a self-referential remote cluster
selfCluster := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "test-remote",
DisplayName: "Test Remote",
SiteURL: "https://test.example.com",
Token: model.NewId(),
CreateAt: model.GetMillis(),
LastPingAt: model.GetMillis(),
CreatorId: th.BasicUser.Id,
RemoteTeamId: model.NewId(),
}
selfCluster, err = ss.RemoteCluster().Save(selfCluster)
require.NoError(t, err)
// Create shared channel remote
scr := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: channel.Id,
CreatorId: th.BasicUser.Id,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: selfCluster.RemoteId,
LastPostCreateAt: model.GetMillis(),
LastPostUpdateAt: model.GetMillis(),
}
_, err = ss.SharedChannel().SaveRemote(scr)
require.NoError(t, err)
// Verify channel is initially shared
hasRemote, err := ss.SharedChannel().HasRemote(channel.Id, selfCluster.RemoteId)
require.NoError(t, err)
require.True(t, hasRemote, "Channel should be shared with remote initially")
// Test: Simulate sync message callback receiving ErrChannelNotShared response
syncMsg := model.NewSyncMsg(channel.Id)
syncMsg.Posts = []*model.Post{{
Id: model.NewId(),
ChannelId: channel.Id,
UserId: th.BasicUser.Id,
Message: "Test sync message",
CreateAt: model.GetMillis(),
}}
// Create a response that simulates the remote returning ErrChannelNotShared
response := &remotecluster.Response{
Status: "fail",
Err: "cannot process sync message; channel is no longer shared: " + channel.Id,
}
// Test the complete flow by simulating what happens in sendSyncMsgToRemote callback
// This tests the fixed error detection logic that checks rcResp.Err
var callbackTriggered bool
mockCallback := func(rcMsg model.RemoteClusterMsg, rc *model.RemoteCluster, rcResp *remotecluster.Response, errResp error) {
callbackTriggered = true
// This simulates the fixed logic in sync_send_remote.go
if rcResp != nil && !rcResp.IsSuccess() && strings.Contains(rcResp.Err, "channel is no longer shared") {
service.HandleChannelNotSharedErrorForTesting(syncMsg, rc)
}
}
// Trigger the callback with our mock response
mockCallback(model.RemoteClusterMsg{}, selfCluster, response, nil)
// Verify the callback was triggered
require.True(t, callbackTriggered, "Callback should have been triggered")
// Verify the channel is no longer shared with the remote
hasRemoteAfter, err := ss.SharedChannel().HasRemote(channel.Id, selfCluster.RemoteId)
require.NoError(t, err)
require.False(t, hasRemoteAfter, "Channel should no longer be shared with remote after error")
// Verify a system message was posted
posts, appErr := th.App.GetPostsPage(th.Context, model.GetPostsOptions{
ChannelId: channel.Id,
Page: 0,
PerPage: 10,
})
require.Nil(t, appErr)
// Find the system message
var systemPost *model.Post
for _, p := range posts.Posts {
if p.Type == model.PostTypeSystemGeneric && p.Message == "This channel is no longer shared." {
systemPost = p
break
}
}
require.NotNil(t, systemPost, "System message should be posted when channel becomes unshared")
}
// TestTransformMentionsOnReceive provides comprehensive unit testing for the mention transformation logic
// using explicit mentionTransforms. This tests ONLY the receiver-side transformation logic
// without requiring complex end-to-end cross-cluster setup.
func TestTransformMentionsOnReceive(t *testing.T) {
mainHelper.Parallel(t)
th := setupSharedChannels(t).InitBasic()
// Setup shared channel
sharedChannel := th.CreateChannel(th.Context, th.BasicTeam)
sc := &model.SharedChannel{
ChannelId: sharedChannel.Id,
TeamId: th.BasicTeam.Id,
Home: true,
ShareName: "testchannel",
CreatorId: th.BasicUser.Id,
}
_, err := th.App.ShareChannel(th.Context, sc)
require.NoError(t, err)
// Setup remote cluster representing the sender
remoteCluster := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: "remote1",
DisplayName: "Remote 1",
SiteURL: "http://remote1.example.com",
Token: model.NewId(),
CreatorId: th.BasicUser.Id,
CreateAt: model.GetMillis(),
LastPingAt: model.GetMillis(),
}
savedRemoteCluster, appErr := th.App.AddRemoteCluster(remoteCluster)
require.Nil(t, appErr)
// Get shared channel service
scs := th.App.Srv().Platform().GetSharedChannelService()
require.NotNil(t, scs)
concreteScs, ok := scs.(*sharedchannel.Service)
require.True(t, ok)
// Helper to create test users
createUser := func(username string, remoteId *string) *model.User {
user := th.CreateUser()
user.Username = username
if remoteId != nil {
user.RemoteId = remoteId
}
user, updateErr := th.App.UpdateUser(th.Context, user, false)
require.Nil(t, updateErr)
th.LinkUserToTeam(user, th.BasicTeam)
th.AddUserToChannel(user, sharedChannel)
return user
}
// Helper to test transformation
testTransformation := func(originalMessage string, mentionTransforms map[string]string, expectedMessage string, description string) {
post := &model.Post{
Id: model.NewId(),
ChannelId: sharedChannel.Id,
UserId: th.BasicUser.Id,
Message: originalMessage,
}
t.Logf("Testing: %s", description)
t.Logf(" Original: %s", originalMessage)
t.Logf(" Transforms: %v", mentionTransforms)
// Call the transformation function directly
concreteScs.TransformMentionsOnReceiveForTesting(th.Context, post, sharedChannel, savedRemoteCluster, mentionTransforms)
t.Logf(" Result: %s", post.Message)
t.Logf(" Expected: %s", expectedMessage)
require.Equal(t, expectedMessage, post.Message, description)
}
t.Run("Scenario 1.1: Remote mentions local user (simple mention)", func(t *testing.T) {
// Create remote user that was synced to receiver
remoteUser := createUser("admin:remote1", &savedRemoteCluster.RemoteId)
// Scenario: remote1 mentions "@admin" (their local user) → sent to receiver
// mentionTransforms["admin"] = remote1AdminUserId
mentionTransforms := map[string]string{
"admin": remoteUser.Id,
}
testTransformation(
"Hello @admin, can you help?",
mentionTransforms,
"Hello @admin:remote1, can you help?", // Use synced username
"Simple mention of synced remote user should use synced username",
)
})
t.Run("Scenario 1.2: Remote mentions local user (different username)", func(t *testing.T) {
// Create remote user that was synced to receiver
remoteUser := createUser("user:remote1", &savedRemoteCluster.RemoteId)
mentionTransforms := map[string]string{
"user": remoteUser.Id,
}
testTransformation(
"Hello @user, can you help?",
mentionTransforms,
"Hello @user:remote1, can you help?",
"Simple mention of different synced remote user should use synced username",
)
})
t.Run("Scenario 2.1: Remote mentions with colon (local user)", func(t *testing.T) {
// Create local user on receiver
localUser := createUser("admin", nil)
// Scenario: remote2 mentions "@admin:remote1" → sent to remote1
// mentionTransforms["admin:remote1"] = remote1AdminUserId
mentionTransforms := map[string]string{
"admin:remote1": localUser.Id,
}
testTransformation(
"Hello @admin:remote1, can you help?",
mentionTransforms,
"Hello @admin, can you help?", // Strip suffix for local user
"Colon mention of local user should strip cluster suffix",
)
})
t.Run("Scenario 2.2: Remote mentions with colon (different local user)", func(t *testing.T) {
// Create local user on receiver
localUser := createUser("user", nil)
mentionTransforms := map[string]string{
"user:remote1": localUser.Id,
}
testTransformation(
"Hello @user:remote1, can you help?",
mentionTransforms,
"Hello @user, can you help?",
"Colon mention of different local user should strip cluster suffix",
)
})
t.Run("Scenario A1: Name clash - remote user mention, local user exists", func(t *testing.T) {
// Create local user with same name
_ = createUser("alice", nil) // Create name clash scenario
// Create remote user that was synced
remoteUser := createUser("alice:remote1", &savedRemoteCluster.RemoteId)
// When remote1 mentions "@alice" (their local user), receiver gets explicit transform
mentionTransforms := map[string]string{
"alice": remoteUser.Id, // Points to synced remote user, not local user
}
testTransformation(
"Hello @alice, can you help?",
mentionTransforms,
"Hello @alice:remote1, can you help?",
"Matrix A1: Remote user mention with local name clash should add cluster suffix",
)
})
t.Run("Scenario A2: Same user - previously synced", func(t *testing.T) {
// Create user that was synced from sender
syncedUser := createUser("bob:remote1", &savedRemoteCluster.RemoteId)
mentionTransforms := map[string]string{
"bob": syncedUser.Id,
}
testTransformation(
"Hello @bob, can you help?",
mentionTransforms,
"Hello @bob:remote1, can you help?",
"Matrix A2: Previously synced user should display synced username",
)
})
t.Run("Scenario A3: No user exists on receiver", func(t *testing.T) {
// Use non-existent user ID
nonExistentUserId := model.NewId()
mentionTransforms := map[string]string{
"charlie": nonExistentUserId,
}
testTransformation(
"Hello @charlie, can you help?",
mentionTransforms,
"Hello @charlie:remote1, can you help?",
"Matrix A3: Unknown user should get cluster suffix",
)
})
t.Run("Scenario B1: User exists on origin with same ID", func(t *testing.T) {
// Create local user (representing user on their home cluster)
localUser := createUser("dave", nil)
// Remote mentions "@dave:remote1" pointing to local user ID
mentionTransforms := map[string]string{
"dave:remote1": localUser.Id,
}
testTransformation(
"Hello @dave:remote1, can you help?",
mentionTransforms,
"Hello @dave, can you help?",
"Matrix B1: Remote mention of local user should strip cluster suffix",
)
})
t.Run("Scenario B2: User does not exist on origin", func(t *testing.T) {
// Use non-existent user ID
nonExistentUserId := model.NewId()
mentionTransforms := map[string]string{
"eve:remote1": nonExistentUserId,
}
testTransformation(
"Hello @eve:remote1, can you help?",
mentionTransforms,
"Hello @eve:remote1, can you help?",
"Matrix B2: Unknown colon mention should remain unchanged",
)
})
t.Run("Empty mentionTransforms", func(t *testing.T) {
// No transforms provided
mentionTransforms := map[string]string{}
testTransformation(
"Hello @anyone, can you help?",
mentionTransforms,
"Hello @anyone, can you help?",
"Message without transforms should remain unchanged",
)
})
t.Run("Mixed scenarios in single message", func(t *testing.T) {
// Setup users
localUser := createUser("frank", nil)
remoteUser := createUser("george:remote1", &savedRemoteCluster.RemoteId)
// Multiple transforms in one message
mentionTransforms := map[string]string{
"frank:remote1": localUser.Id, // Colon mention → strip suffix
"george": remoteUser.Id, // Simple mention → use synced username
}
testTransformation(
"Hello @frank:remote1 and @george, let's collaborate!",
mentionTransforms,
"Hello @frank and @george:remote1, let's collaborate!",
"Mixed mention types should transform correctly",
)
})
t.Run("Colon mention of remote user", func(t *testing.T) {
// Create remote user that was synced
remoteUser := createUser("guest:remote1", &savedRemoteCluster.RemoteId)
// Colon mention pointing to remote user (edge case)
mentionTransforms := map[string]string{
"guest:remote1": remoteUser.Id,
}
testTransformation(
"Hello @guest:remote1, welcome!",
mentionTransforms,
"Hello @guest:remote1, welcome!",
"Colon mention of remote user should use synced username",
)
})
t.Run("Performance: Large message with many mentions", func(t *testing.T) {
// Create users for performance test
user1 := createUser("user1:remote1", &savedRemoteCluster.RemoteId)
user2 := createUser("user2:remote1", &savedRemoteCluster.RemoteId)
user3 := createUser("user3:remote1", &savedRemoteCluster.RemoteId)
mentionTransforms := map[string]string{
"user1": user1.Id,
"user2": user2.Id,
"user3": user3.Id,
}
testTransformation(
"Meeting with @user1, @user2, and @user3 about @user1's proposal. @user2 will present, @user3 will take notes.",
mentionTransforms,
"Meeting with @user1:remote1, @user2:remote1, and @user3:remote1 about @user1:remote1's proposal. @user2:remote1 will present, @user3:remote1 will take notes.",
"Multiple mentions should transform efficiently",
)
})
}