Files
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

251 lines
10 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"strings"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/require"
)
func Test_mungUsername(t *testing.T) {
type args struct {
username string
remotename string
suffix string
maxLen int
}
tests := []struct {
name string
args args
want string
}{
{"everything empty", args{username: "", remotename: "", suffix: "", maxLen: 64}, ":"},
{"no trunc, no suffix", args{username: "bart", remotename: "example.com", suffix: "", maxLen: 64}, "bart:example.com"},
{"no trunc, suffix", args{username: "bart", remotename: "example.com", suffix: "2", maxLen: 64}, "bart-2:example.com"},
{"trunc remote, no suffix", args{username: "bart", remotename: "example1234567890.com", suffix: "", maxLen: 24}, "bart:example123456789..."},
{"trunc remote, suffix", args{username: "bart", remotename: "example1234567890.com", suffix: "2", maxLen: 24}, "bart-2:example1234567..."},
{"trunc both, no suffix", args{username: R(24, "A"), remotename: R(24, "B"), suffix: "", maxLen: 24}, "AAAAAAAAA...:BBBBBBBB..."},
{"trunc both, suffix", args{username: R(24, "A"), remotename: R(24, "B"), suffix: "10", maxLen: 24}, "AAAAAA-10...:BBBBBBBB..."},
{"trunc user, no suffix", args{username: R(40, "A"), remotename: "abc", suffix: "", maxLen: 24}, "AAAAAAAAAAAAAAAAA...:abc"},
{"trunc user, suffix", args{username: R(40, "A"), remotename: "abc", suffix: "11", maxLen: 24}, "AAAAAAAAAAAAAA-11...:abc"},
{"trunc user, remote, no suffix", args{username: R(40, "A"), remotename: "abcdefghijk", suffix: "", maxLen: 24}, "AAAAAAAAA...:abcdefghijk"},
{"trunc user, remote, suffix", args{username: R(40, "A"), remotename: "abcdefghijk", suffix: "19", maxLen: 24}, "AAAAAA-19...:abcdefghijk"},
{"short user, long remote, no suffix", args{username: "bart", remotename: R(40, "B"), suffix: "", maxLen: 24}, "bart:BBBBBBBBBBBBBBBB..."},
{"long user, short remote, no suffix", args{username: R(40, "A"), remotename: "abc.com", suffix: "", maxLen: 24}, "AAAAAAAAAAAAA...:abc.com"},
{"short user, long remote, suffix", args{username: "bart", remotename: R(40, "B"), suffix: "12", maxLen: 24}, "bart-12:BBBBBBBBBBBBB..."},
{"long user, short remote, suffix", args{username: R(40, "A"), remotename: "abc.com", suffix: "12", maxLen: 24}, "AAAAAAAAAA-12...:abc.com"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := mungUsername(tt.args.username, tt.args.remotename, tt.args.suffix, tt.args.maxLen); got != tt.want {
t.Errorf("mungUsername() = %v, want %v", got, tt.want)
}
})
}
}
func Test_mungUsernameFuzz(t *testing.T) {
// ensure no index out of bounds panic for any combination
for i := range 70 {
for j := range 70 {
for k := range 3 {
username := R(i, "A")
remotename := R(j, "B")
suffix := R(k, "1")
result := mungUsername(username, remotename, suffix, 64)
require.LessOrEqual(t, len(result), 64)
}
}
}
}
func Test_fixMention(t *testing.T) {
tests := []struct {
name string
post *model.Post
mentionMap model.UserMentionMap
user *model.User
expected string
}{
{
name: "nil post",
post: nil,
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid"},
expected: "",
},
{
name: "empty mentionMap",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{},
user: &model.User{Id: "userid"},
expected: "hello @user:remote",
},
{
name: "no RemoteUsername prop",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid"},
expected: "hello @user:remote",
},
// Username clash test cases (same username on both local and remote)
// With the new behavior, when syncing to a user's home cluster, we always remove the remote suffix
{
name: "username clash - simple mention",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "user"}},
expected: "hello @user",
},
{
name: "username clash - mention at start",
post: &model.Post{Message: "@user:remote hello"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "user"}},
expected: "@user hello",
},
{
name: "username clash - multiple mentions",
post: &model.Post{Message: "hello @user:remote and @user:remote again"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "user"}},
expected: "hello @user and @user again",
},
{
name: "simple mention different username",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "hello @realuser",
},
{
name: "simple mention same username",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "user"}},
expected: "hello @user",
},
{
name: "mention at start",
post: &model.Post{Message: "@user:remote hello"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "@realuser hello",
},
{
name: "mention at end",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "hello @realuser",
},
{
name: "multiple mentions of same user",
post: &model.Post{Message: "hello @user:remote and @user:remote again"},
mentionMap: model.UserMentionMap{"user:remote": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "hello @realuser and @realuser again",
},
{
name: "multiple different mentions",
post: &model.Post{Message: "hello @user:remote and @other:remote"},
mentionMap: model.UserMentionMap{"user:remote": "userid", "other:remote": "otherid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "hello @realuser and @other:remote",
},
{
name: "mention without colon",
post: &model.Post{Message: "hello @user"},
mentionMap: model.UserMentionMap{"user": "userid"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "hello @user",
},
{
name: "mention different user id",
post: &model.Post{Message: "hello @user:remote"},
mentionMap: model.UserMentionMap{"user:remote": "different"},
user: &model.User{Id: "userid", RemoteId: new("remoteid"), Props: model.StringMap{model.UserPropsKeyRemoteUsername: "realuser"}},
expected: "hello @user:remote",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fixMention(tt.post, tt.mentionMap, tt.user)
if tt.post != nil {
require.Equal(t, tt.expected, tt.post.Message)
}
})
}
}
// R returns a string with the specified string repeated `count` times.
func R(count int, s string) string {
return strings.Repeat(s, count)
}
func TestHandleUserMentions(t *testing.T) {
t.Run("should format mentions across clusters", func(t *testing.T) {
// This test verifies the behavior when same username exists on multiple clusters
// Define remote cluster names (not server names)
clusterBName := "remote_cluster_B"
// Create user with admin username on remote cluster B
adminUserB := &model.User{
Id: "adminB",
Username: "admin",
RemoteId: new(clusterBName),
Props: model.StringMap{model.UserPropsKeyRemoteUsername: "admin"},
}
// Scenario: User on Cluster B mentions local admin
// The mention should show as @admin on Cluster B
postOnClusterB := &model.Post{
Message: "Hey @admin, please review this.",
}
// Mentions are preserved when synced between clusters
// so that the local admin on Cluster A would receive a notification
// We're verifying the post will keep its mention format after being synced
require.Contains(t, postOnClusterB.Message, "@admin",
"Message should preserve @admin mention when synced between clusters")
// Scenario: User on Cluster A explicitly mentions admin on Cluster B
// It will show as @admin:remote_cluster_B on Cluster A
postWithRemoteMention := &model.Post{
Message: "Let's ask @admin:remote_cluster_B about this",
}
mentionMapWithRemote := model.UserMentionMap{
"admin:remote_cluster_B": "adminB",
}
// When this post is synced to Cluster B, fixMention will transform
// @admin:remote_cluster_B to @admin (removing the remote suffix)
postCopy := &model.Post{Message: postWithRemoteMention.Message}
fixMention(postCopy, mentionMapWithRemote, adminUserB)
// fixMention should replace @username:remote with @realusername (without the remote suffix)
// when syncing to the user's home cluster
expected := "Let's ask @admin about this"
// The part we're testing is that mentions are simplified when sent to the user's home cluster
require.Equal(t, expected, postCopy.Message,
"Should remove remote cluster suffix when message is viewed on user's home server")
})
}