status-go/protocol/messenger_mention_test.go

792 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package protocol
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/logutils"
)
func TestRePosRegex(t *testing.T) {
testCases := []struct {
input string
expected bool
}{
{"@", true},
{"~", true},
{"\\", true},
{"*", true},
{"_", true},
{"\n", true},
{"`", true},
{"a", false},
{"#", false},
}
for _, tc := range testCases {
actual := specialCharsRegex.MatchString(tc.input)
if actual != tc.expected {
t.Errorf("Unexpected match result for input '%s': expected=%v, actual=%v", tc.input, tc.expected, actual)
}
}
}
func TestRePos(t *testing.T) {
// Test case 1: Empty string
s1 := ""
var want1 []specialCharLocation
if got1 := rePos(s1); !reflect.DeepEqual(got1, want1) {
t.Errorf("rePos(%q) = %v, want %v", s1, got1, want1)
}
// Test case 2: Single match
s2 := "test@string"
want2 := []specialCharLocation{{4, "@"}}
if got2 := rePos(s2); !reflect.DeepEqual(got2, want2) {
t.Errorf("rePos(%q) = %v, want %v", s2, got2, want2)
}
// Test case 3: Multiple matches
s3 := "this is a test string@with multiple@matches"
want3 := []specialCharLocation{{21, "@"}, {35, "@"}}
if got3 := rePos(s3); !reflect.DeepEqual(got3, want3) {
t.Errorf("rePos(%q) = %v, want %v", s3, got3, want3)
}
// Test case 4: No matches
s4 := "this is a test string with no matches"
var want4 []specialCharLocation
if got4 := rePos(s4); !reflect.DeepEqual(got4, want4) {
t.Errorf("rePos(%q) = %v, want %v", s4, got4, want4)
}
// Test case 5: Matches at the beginning and end
s5 := "@this is a test string@"
want5 := []specialCharLocation{{0, "@"}, {22, "@"}}
if got5 := rePos(s5); !reflect.DeepEqual(got5, want5) {
t.Errorf("rePos(%q) = %v, want %v", s5, got5, want5)
}
// Test case 6: special characters
s6 := "Привет @testm1 "
want6 := []specialCharLocation{{7, "@"}}
if got6 := rePos(s6); !reflect.DeepEqual(got6, want6) {
t.Errorf("rePos(%q) = %v, want %v", s6, got6, want6)
}
}
func TestReplaceMentions(t *testing.T) {
users := map[string]*MentionableUser{
"0xpk1": {
primaryName: "User Number One",
Contact: &Contact{
ID: "0xpk1",
},
},
"0xpk2": {
primaryName: "user2",
secondaryName: "User Number Two",
Contact: &Contact{
ID: "0xpk2",
},
},
"0xpk3": {
primaryName: "user3",
secondaryName: "User Number Three",
Contact: &Contact{
ID: "0xpk3",
},
},
}
tests := []struct {
name string
text string
expected string
}{
{"empty string", "", ""},
{"no text", "", ""},
{"incomlepte mention 1", "@", "@"},
{"incomplete mention 2", "@r", "@r"},
{"no mentions", "foo bar @buzz kek @foo", "foo bar @buzz kek @foo"},
{"starts with mention", "@User Number One", "@0xpk1"},
{"starts with mention, comma after mention", "@User Number One,", "@0xpk1,"},
{"starts with mention but no space after", "@User Number Onefoo", "@User Number Onefoo"},
{"starts with mention, some text after mention", "@User Number One foo", "@0xpk1 foo"},
{"starts with some text, then mention", "text @User Number One", "text @0xpk1"},
{"starts with some text, then mention, then more text", "text @User Number One foo", "text @0xpk1 foo"},
{"no space before mention", "text@User Number One", "text@0xpk1"},
{"two different mentions", "@User Number One @User Number two", "@0xpk1 @0xpk2"},
{"two different mentions, separated with comma", "@User Number One,@User Number two", "@0xpk1,@0xpk2"},
{"two different mentions inside text", "foo@User Number One bar @User Number two baz", "foo@0xpk1 bar @0xpk2 baz"},
{"ens mention", "@user2", "@0xpk2"},
{"multiple mentions", strings.Repeat("@User Number One @User Number two ", 1000), strings.Repeat("@0xpk1 @0xpk2 ", 1000)},
{"single * case 1", "*@user2*", "*@user2*"},
{"single * case 2", "*@user2 *", "*@0xpk2 *"},
{"single * case 3", "a*@user2*", "a*@user2*"},
{"single * case 4", "*@user2 foo*foo", "*@0xpk2 foo*foo"},
{"single * case 5", "a *@user2*", "a *@user2*"},
{"single * case 6", "*@user2 foo*", "*@user2 foo*"},
{"single * case 7", "@user2 *@user2 foo* @user2", "@0xpk2 *@user2 foo* @0xpk2"},
{"single * case 8", "*@user2 foo**@user2 foo*", "*@user2 foo**@user2 foo*"},
{"single * case 9", "*@user2 foo***@user2 foo* @user2", "*@user2 foo***@user2 foo* @0xpk2"},
{"double * case 1", "**@user2**", "**@user2**"},
{"double * case 2", "**@user2 **", "**@0xpk2 **"},
{"double * case 3", "a**@user2**", "a**@user2**"},
{"double * case 4", "**@user2 foo**foo", "**@user2 foo**foo"},
{"double * case 5", "a **@user2**", "a **@user2**"},
{"double * case 6", "**@user2 foo**", "**@user2 foo**"},
{"double * case 7", "@user2 **@user2 foo** @user2", "@0xpk2 **@user2 foo** @0xpk2"},
{"double * case 8", "**@user2 foo****@user2 foo**", "**@user2 foo****@user2 foo**"},
{"double * case 9", "**@user2 foo*****@user2 foo** @user2", "**@user2 foo*****@user2 foo** @0xpk2"},
{"tripple * case 1", "***@user2 foo***@user2 foo*", "***@user2 foo***@0xpk2 foo*"},
{"tripple ~ case 1", "~~~@user2 foo~~~@user2 foo~", "~~~@user2 foo~~~@user2 foo~"},
{"quote case 1", ">@user2", ">@user2"},
{"quote case 2", "\n>@user2", "\n>@user2"},
{"quote case 3", "\n> @user2 \n \n @user2", "\n> @user2 \n \n @0xpk2"},
{"quote case 4", ">@user2\n\n>@user2", ">@user2\n\n>@user2"},
{"quote case 5", "***hey\n\n>@user2\n\n@user2 foo***", "***hey\n\n>@user2\n\n@0xpk2 foo***"},
{"code case 1", "` @user2 `", "` @user2 `"},
{"code case 2", "` @user2 `", "` @user2 `"},
{"code case 3", "``` @user2 ```", "``` @user2 ```"},
{"code case 4", "` ` @user2 ``", "` ` @0xpk2 ``"},
{"double @", "@ @user2", "@ @0xpk2"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ReplaceMentions(tt.text, users)
if got != tt.expected {
t.Errorf("testing %q, ReplaceMentions(%q) got %q, expected %q", tt.name, tt.text, got, tt.expected)
}
})
}
}
func TestGetAtSignIdxs(t *testing.T) {
tests := []struct {
name string
text string
start int
want []int
}{
{
name: "no @ sign",
text: "hello world",
start: 0,
want: []int{},
},
{
name: "single @ sign",
text: "hello @world",
start: 0,
want: []int{6},
},
{
name: "multiple @ signs",
text: "@hello @world @again",
start: 0,
want: []int{0, 7, 14},
},
{
name: "start after first @ sign",
text: "hello @world",
start: 6,
want: []int{12},
},
{
name: "start after second @ sign",
text: "hello @world @again",
start: 8,
want: []int{14, 21},
},
{
name: "start after last @ sign",
text: "hello @world @again",
start: 15,
want: []int{21, 28},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getAtSignIdxs(tt.text, tt.start)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAtSignIdxs() = %v, want %v", got, tt.want)
}
})
}
}
func TestCalcAtIdxs(t *testing.T) {
newText := "@abc"
state := MentionState{
AtIdxs: []*AtIndexEntry{
{From: 0, To: 3, Checked: false},
},
NewText: &newText,
PreviousText: "",
Start: 0,
}
want := []*AtIndexEntry{
{From: 0, To: 0, Checked: false},
{From: 4, To: 7, Checked: false},
}
got := calcAtIdxs(&state)
if !reflect.DeepEqual(got, want) {
t.Errorf("calcAtIdxs() = %v, want %v", got, want)
}
}
func TestToInfo(t *testing.T) {
newText := " "
t.Run("toInfo base case", func(t *testing.T) {
expected := &MentionState{
AtSignIdx: 2,
AtIdxs: []*AtIndexEntry{
{
Checked: true,
Mentioned: true,
From: 2,
To: 17,
},
},
MentionEnd: 19,
PreviousText: "",
NewText: &newText,
Start: 18,
End: 18,
}
inputSegments := []InputSegment{
{Type: Text, Value: "H."},
{Type: Mention, Value: "@helpinghand.eth"},
{Type: Text, Value: " "},
}
actual := toInfo(inputSegments)
if !reflect.DeepEqual(expected.AtIdxs, actual.AtIdxs) {
t.Errorf("Expected AtIdxs: %#v, but got: %#v", expected.AtIdxs, actual.AtIdxs)
}
expected.AtIdxs = nil
actual.AtIdxs = nil
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Expected %#v, but got %#v", expected, actual)
}
})
}
func TestToInputField(t *testing.T) {
testCases := []struct {
name string
input string
expected []InputSegment
}{
{
"only text",
"parse-text",
[]InputSegment{{Type: Text, Value: "parse-text"}},
},
{
"in the middle",
"hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he",
[]InputSegment{
{Type: Text, Value: "hey "},
{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
{Type: Text, Value: " he"},
},
},
{
"at the beginning",
"@0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he",
[]InputSegment{
{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
{Type: Text, Value: " he"},
},
},
{
"at the end",
"hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073",
[]InputSegment{
{Type: Text, Value: "hey "},
{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
},
},
{
"invalid",
"invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073",
[]InputSegment{
{Type: Text, Value: "invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := toInputField(tc.input)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Expected: %v, got: %v", tc.expected, result)
}
})
}
}
func TestSubs(t *testing.T) {
testCases := []struct {
name string
input string
start int
end int
expected string
}{
{
name: "Normal case",
input: "Hello, world!",
start: 0,
end: 5,
expected: "Hello",
},
{
name: "Start index out of range (negative)",
input: "Hello, world!",
start: -5,
end: 5,
expected: "Hello",
},
{
name: "End index out of range",
input: "Hello, world!",
start: 7,
end: 50,
expected: "world!",
},
{
name: "Start index greater than end index",
input: "Hello, world!",
start: 10,
end: 5,
expected: ", wor",
},
{
name: "Both indices out of range",
input: "Hello, world!",
start: -5,
end: 50,
expected: "Hello, world!",
},
{
name: "Start index negative, end index out of range",
input: "Hello, world!",
start: -10,
end: 15,
expected: "Hello, world!",
},
{
name: "Start index negative, end index within range",
input: "Hello, world!",
start: -10,
end: 5,
expected: "Hello",
},
{
name: "Start index negative, end index negative",
input: "Hello, world!",
start: -10,
end: -5,
expected: "",
},
{
name: "Start index zero, end index zero",
input: "Hello, world!",
start: 0,
end: 0,
expected: "",
},
{
name: "Start index positive, end index zero",
input: "Hello, world!",
start: 3,
end: 0,
expected: "Hel",
},
{
name: "Start index equal to input length",
input: "Hello, world!",
start: 13,
end: 15,
expected: "",
},
{
name: "End index negative",
input: "Hello, world!",
start: 5,
end: -5,
expected: "Hello",
},
{
name: "Start and end indices equal and negative",
input: "Hello, world!",
start: -3,
end: -3,
expected: "",
},
{
name: "Start index greater than input length",
input: "Hello, world!",
start: 15,
end: 20,
expected: "",
},
{
name: "End index equal to input length",
input: "Hello, world!",
start: 0,
end: 13,
expected: "Hello, world!",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := subs(tc.input, tc.start, tc.end)
if actual != tc.expected {
t.Errorf("Test case '%s': expected '%s', got '%s'", tc.name, tc.expected, actual)
}
})
}
}
func TestLastIndexOf(t *testing.T) {
atSignIdx := lastIndexOf("@", charAtSign, 0)
require.Equal(t, 0, atSignIdx)
atSignIdx = lastIndexOf("@@", charAtSign, 1)
require.Equal(t, 1, atSignIdx)
//at-sign-idx 0 text @t searched-text t start 2 end 2 new-text
atSignIdx = lastIndexOf("@t", charAtSign, 2)
require.Equal(t, 0, atSignIdx)
}
func TestDiffText(t *testing.T) {
testCases := []struct {
oldText string
newText string
expected *TextDiff
}{
{
oldText: "",
newText: "A",
expected: &TextDiff{
start: 0,
end: 0,
previousText: "",
newText: "A",
},
},
{
oldText: "A",
newText: "Ab",
expected: &TextDiff{
start: 1,
end: 1,
previousText: "A",
newText: "b",
},
},
{
oldText: "Ab",
newText: "Abc",
expected: &TextDiff{
start: 2,
end: 2,
previousText: "Ab",
newText: "c",
},
},
{
oldText: "Abc",
newText: "Ac",
expected: &TextDiff{
start: 1,
end: 2,
previousText: "Abc",
newText: "",
},
},
{
oldText: "Ac",
newText: "Adc",
expected: &TextDiff{
start: 1,
end: 1,
previousText: "Ac",
newText: "d",
},
},
{
oldText: "Adc",
newText: "Ad ee c",
expected: &TextDiff{
start: 2,
end: 2,
previousText: "Adc",
newText: " ee ",
},
},
{
oldText: "Ad ee c",
newText: "A fff d ee c",
expected: &TextDiff{
start: 1,
end: 1,
previousText: "Ad ee c",
newText: " fff ",
},
},
{
oldText: "A fff d ee c",
newText: " fff d ee c",
expected: &TextDiff{
start: 0,
end: 1,
previousText: "A fff d ee c",
newText: "",
},
},
{
oldText: " fff d ee c",
newText: " fffee c",
expected: &TextDiff{
start: 4,
end: 7,
previousText: " fff d ee c",
newText: "",
},
},
{
oldText: "abc",
newText: "abc",
expected: nil,
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
diff := diffText(tc.oldText, tc.newText)
require.Equal(t, tc.expected, diff)
})
}
}
type MockMentionableUserGetter struct {
mentionableUserMap map[string]*MentionableUser
}
func (m *MockMentionableUserGetter) getMentionableUsers(chatID string) (map[string]*MentionableUser, error) {
return m.mentionableUserMap, nil
}
func (m *MockMentionableUserGetter) getMentionableUser(chatID string, pk string) (*MentionableUser, error) {
return m.mentionableUserMap[pk], nil
}
func TestMentionSuggestionCases(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
testCases := []struct {
inputText string
expectedSize int
}{
{"@", len(mentionableUserMap)},
{"@u", len(mentionableUserMap)},
{"@u2", 1},
{"@u23", 0},
{"@u2", 1},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
_, err := mentionManager.OnChangeText(chatID, tc.inputText)
require.NoError(t, err)
ctx, err := mentionManager.CalculateSuggestions(chatID, tc.inputText)
require.NoError(t, err)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
})
}
}
func TestMentionSuggestionAfterToInputField(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
_, err := mentionManager.ToInputField(chatID, "abc")
require.NoError(t, err)
ctx, err := mentionManager.OnChangeText(chatID, "@")
require.NoError(t, err)
require.Equal(t, len(mentionableUserMap), len(ctx.MentionSuggestions))
}
func TestMentionSuggestionSpecialInputModeForAndroid(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
testCases := []struct {
inputText string
expectedSize int
}{
{"A", 0},
{"As", 0},
{"Asd", 0},
{"Asd@", len(mentionableUserMap)},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
ctx, err := mentionManager.OnChangeText(chatID, tc.inputText)
require.NoError(t, err)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
})
}
}
func TestMentionSuggestionSpecialChars(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
testCases := []struct {
inputText string
expectedSize int
calculateSuggestion bool
}{
{"'", 0, false},
{"", 0, true},
{"@", len(mentionableUserMap), true},
}
for _, tc := range testCases {
ctx, err := mentionManager.OnChangeText(chatID, tc.inputText)
require.NoError(t, err)
if tc.calculateSuggestion {
ctx, err = mentionManager.CalculateSuggestions(chatID, tc.inputText)
require.NoError(t, err)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
}
t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
}
}
func TestMentionSuggestionAtSignSpaceCases(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, map[string]*MentionableUser{
"0xpk1": {
primaryName: "User Number One",
Contact: &Contact{
ID: "0xpk1",
},
},
})
testCases := []struct {
inputText string
expectedSize int
calculateSuggestion bool
}{
{"@", len(mentionableUserMap), true},
{"@ ", 0, true},
{"@ @", len(mentionableUserMap), true},
}
var ctx *ChatMentionContext
var err error
for _, tc := range testCases {
ctx, err = mentionManager.OnChangeText(chatID, tc.inputText)
require.NoError(t, err)
t.Logf("After OnChangeText, Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
if tc.calculateSuggestion {
ctx, err = mentionManager.CalculateSuggestions(chatID, tc.inputText)
require.NoError(t, err)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
t.Logf("After CalculateSuggestions, Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
}
}
require.Len(t, ctx.InputSegments, 3)
require.Equal(t, Mention, ctx.InputSegments[0].Type)
require.Equal(t, "@ @", ctx.InputSegments[0].Value)
require.Equal(t, Text, ctx.InputSegments[1].Type)
require.Equal(t, "@", ctx.InputSegments[1].Value)
require.Equal(t, Text, ctx.InputSegments[2].Type)
require.Equal(t, "@", ctx.InputSegments[2].Value)
}
func setupMentionSuggestionTest(t *testing.T, mentionableUserMapInput map[string]*MentionableUser) (map[string]*MentionableUser, string, *MentionManager) {
mentionableUserMap := mentionableUserMapInput
if mentionableUserMap == nil {
mentionableUserMap = getDefaultMentionableUserMap()
}
for _, u := range mentionableUserMap {
addSearchablePhrases(u)
}
mockMentionableUserGetter := &MockMentionableUserGetter{
mentionableUserMap: mentionableUserMap,
}
chatID := "0xchatID"
allChats := new(chatMap)
allChats.Store(chatID, &Chat{})
key, err := crypto.GenerateKey()
require.NoError(t, err)
mentionManager := &MentionManager{
mentionableUserGetter: mockMentionableUserGetter,
mentionContexts: make(map[string]*ChatMentionContext),
Messenger: &Messenger{
allChats: allChats,
identity: key,
},
logger: logutils.ZapLogger().Named("MentionManager"),
}
return mentionableUserMap, chatID, mentionManager
}
func getDefaultMentionableUserMap() map[string]*MentionableUser {
return map[string]*MentionableUser{
"0xpk1": {
primaryName: "User Number One",
Contact: &Contact{
ID: "0xpk1",
},
},
"0xpk2": {
primaryName: "u2",
secondaryName: "User Number Two",
Contact: &Contact{
ID: "0xpk2",
},
},
"0xpk3": {
primaryName: "u3",
secondaryName: "User Number Three",
Contact: &Contact{
ID: "0xpk3",
},
},
}
}