github.com/status-im/status-go@v1.1.0/protocol/messenger_mention_test.go (about)

     1  package protocol
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/status-im/status-go/eth-node/crypto"
    12  	"github.com/status-im/status-go/logutils"
    13  )
    14  
    15  func TestRePosRegex(t *testing.T) {
    16  	testCases := []struct {
    17  		input    string
    18  		expected bool
    19  	}{
    20  		{"@", true},
    21  		{"~", true},
    22  		{"\\", true},
    23  		{"*", true},
    24  		{"_", true},
    25  		{"\n", true},
    26  		{"`", true},
    27  		{"a", false},
    28  		{"#", false},
    29  	}
    30  
    31  	for _, tc := range testCases {
    32  		actual := specialCharsRegex.MatchString(tc.input)
    33  		if actual != tc.expected {
    34  			t.Errorf("Unexpected match result for input '%s': expected=%v, actual=%v", tc.input, tc.expected, actual)
    35  		}
    36  	}
    37  }
    38  
    39  func TestRePos(t *testing.T) {
    40  	// Test case 1: Empty string
    41  	s1 := ""
    42  	var want1 []specialCharLocation
    43  	if got1 := rePos(s1); !reflect.DeepEqual(got1, want1) {
    44  		t.Errorf("rePos(%q) = %v, want %v", s1, got1, want1)
    45  	}
    46  
    47  	// Test case 2: Single match
    48  	s2 := "test@string"
    49  	want2 := []specialCharLocation{{4, "@"}}
    50  	if got2 := rePos(s2); !reflect.DeepEqual(got2, want2) {
    51  		t.Errorf("rePos(%q) = %v, want %v", s2, got2, want2)
    52  	}
    53  
    54  	// Test case 3: Multiple matches
    55  	s3 := "this is a test string@with multiple@matches"
    56  	want3 := []specialCharLocation{{21, "@"}, {35, "@"}}
    57  	if got3 := rePos(s3); !reflect.DeepEqual(got3, want3) {
    58  		t.Errorf("rePos(%q) = %v, want %v", s3, got3, want3)
    59  	}
    60  
    61  	// Test case 4: No matches
    62  	s4 := "this is a test string with no matches"
    63  	var want4 []specialCharLocation
    64  	if got4 := rePos(s4); !reflect.DeepEqual(got4, want4) {
    65  		t.Errorf("rePos(%q) = %v, want %v", s4, got4, want4)
    66  	}
    67  
    68  	// Test case 5: Matches at the beginning and end
    69  	s5 := "@this is a test string@"
    70  	want5 := []specialCharLocation{{0, "@"}, {22, "@"}}
    71  	if got5 := rePos(s5); !reflect.DeepEqual(got5, want5) {
    72  		t.Errorf("rePos(%q) = %v, want %v", s5, got5, want5)
    73  	}
    74  
    75  	// Test case 6: special characters
    76  	s6 := "Привет @testm1 "
    77  	want6 := []specialCharLocation{{7, "@"}}
    78  	if got6 := rePos(s6); !reflect.DeepEqual(got6, want6) {
    79  		t.Errorf("rePos(%q) = %v, want %v", s6, got6, want6)
    80  	}
    81  
    82  }
    83  
    84  func TestReplaceMentions(t *testing.T) {
    85  	users := map[string]*MentionableUser{
    86  		"0xpk1": {
    87  			Contact: &Contact{
    88  				ID:            "0xpk1",
    89  				LocalNickname: "User Number One",
    90  			},
    91  		},
    92  		"0xpk2": {
    93  			Contact: &Contact{
    94  				ID:            "0xpk2",
    95  				LocalNickname: "user2",
    96  				ENSVerified:   true,
    97  				EnsName:       "User Number Two",
    98  			},
    99  		},
   100  		"0xpk3": {
   101  			Contact: &Contact{
   102  				ID:            "0xpk3",
   103  				LocalNickname: "user3",
   104  				ENSVerified:   true,
   105  				EnsName:       "User Number Three",
   106  			},
   107  		},
   108  		"0xpk4": {
   109  			Contact: &Contact{
   110  				ID:            "0xpk4",
   111  				EnsName:       "ens-user-4.eth",
   112  				ENSVerified:   true,
   113  				DisplayName:   "display-name-user-4",
   114  				LocalNickname: "primary-name-user-4",
   115  			},
   116  		},
   117  		"0xpk5": {
   118  			Contact: &Contact{
   119  				ID:            "0xpk5",
   120  				LocalNickname: "User Number",
   121  			},
   122  		},
   123  		"0xpk6": {
   124  			Contact: &Contact{
   125  				ID:            "0xpk6",
   126  				LocalNickname: "特别字符",
   127  				DisplayName:   "特别 字符",
   128  			},
   129  		},
   130  	}
   131  
   132  	tests := []struct {
   133  		name     string
   134  		text     string
   135  		expected string
   136  	}{
   137  		{"empty string", "", ""},
   138  		{"no text", "", ""},
   139  		{"incomlepte mention 1", "@", "@"},
   140  		{"incomplete mention 2", "@r", "@r"},
   141  		{"no mentions", "foo bar @buzz kek @foo", "foo bar @buzz kek @foo"},
   142  		{"starts with mention", "@User Number One", "@0xpk1"},
   143  		{"starts with mention, comma after mention", "@User Number One,", "@0xpk1,"},
   144  		{"starts with mention but no space after", "@User NumberOnefoo", "@User NumberOnefoo"},
   145  		{"starts with mention, some text after mention", "@User Number One foo", "@0xpk1 foo"},
   146  		{"starts with some text, then mention", "text @User Number One", "text @0xpk1"},
   147  		{"starts with some text, then mention, then more text", "text @User Number One foo", "text @0xpk1 foo"},
   148  		{"no space before mention", "text@User Number One", "text@0xpk1"},
   149  		{"two different mentions", "@User Number One @User Number two", "@0xpk1 @0xpk2"},
   150  		{"two different mentions, separated with comma", "@User Number One,@User Number two", "@0xpk1,@0xpk2"},
   151  		{"two different mentions inside text", "foo@User Number One bar @User Number two baz", "foo@0xpk1 bar @0xpk2 baz"},
   152  		{"ens mention", "@user2", "@0xpk2"},
   153  		{"multiple mentions", strings.Repeat("@User Number One @User Number two ", 1000), strings.Repeat("@0xpk1 @0xpk2 ", 1000)},
   154  
   155  		{"single * case 1", "*@user2*", "*@user2*"},
   156  		{"single * case 2", "*@user2 *", "*@0xpk2 *"},
   157  		{"single * case 3", "a*@user2*", "a*@user2*"},
   158  		{"single * case 4", "*@user2 foo*foo", "*@0xpk2 foo*foo"},
   159  		{"single * case 5", "a *@user2*", "a *@user2*"},
   160  		{"single * case 6", "*@user2 foo*", "*@user2 foo*"},
   161  		{"single * case 7", "@user2 *@user2 foo* @user2", "@0xpk2 *@user2 foo* @0xpk2"},
   162  		{"single * case 8", "*@user2 foo**@user2 foo*", "*@user2 foo**@user2 foo*"},
   163  		{"single * case 9", "*@user2 foo***@user2 foo* @user2", "*@user2 foo***@user2 foo* @0xpk2"},
   164  
   165  		{"double * case 1", "**@user2**", "**@user2**"},
   166  		{"double * case 2", "**@user2 **", "**@0xpk2 **"},
   167  		{"double * case 3", "a**@user2**", "a**@user2**"},
   168  		{"double * case 4", "**@user2 foo**foo", "**@user2 foo**foo"},
   169  		{"double * case 5", "a **@user2**", "a **@user2**"},
   170  		{"double * case 6", "**@user2 foo**", "**@user2 foo**"},
   171  		{"double * case 7", "@user2 **@user2 foo** @user2", "@0xpk2 **@user2 foo** @0xpk2"},
   172  		{"double * case 8", "**@user2 foo****@user2 foo**", "**@user2 foo****@user2 foo**"},
   173  		{"double * case 9", "**@user2 foo*****@user2 foo** @user2", "**@user2 foo*****@user2 foo** @0xpk2"},
   174  
   175  		{"tripple * case 1", "***@user2 foo***@user2 foo*", "***@user2 foo***@0xpk2 foo*"},
   176  		{"tripple ~ case 1", "~~~@user2 foo~~~@user2 foo~", "~~~@user2 foo~~~@user2 foo~"},
   177  
   178  		{"quote case 1", ">@user2", ">@user2"},
   179  		{"quote case 2", "\n>@user2", "\n>@user2"},
   180  		{"quote case 3", "\n> @user2 \n   \n @user2", "\n> @user2 \n   \n @0xpk2"},
   181  		{"quote case 4", ">@user2\n\n>@user2", ">@user2\n\n>@user2"},
   182  		{"quote case 5", "***hey\n\n>@user2\n\n@user2 foo***", "***hey\n\n>@user2\n\n@0xpk2 foo***"},
   183  
   184  		{"code case 1", "` @user2 `", "` @user2 `"},
   185  		{"code case 2", "` @user2 `", "` @user2 `"},
   186  		{"code case 3", "``` @user2 ```", "``` @user2 ```"},
   187  		{"code case 4", "` ` @user2 ``", "` ` @0xpk2 ``"},
   188  
   189  		{"double @", "@ @user2", "@ @0xpk2"},
   190  
   191  		{"user name contains dash", "@display-name-user-4 ", "@0xpk4 "},
   192  
   193  		{"username or nickname of one is a substring of another case 1", "@User Number One @User Number", "@0xpk1 @0xpk5"},
   194  		{"username or nickname of one is a substring of another case 2", "@User Number @User Number One ", "@0xpk5 @0xpk1 "},
   195  
   196  		{"special chars in username case1", "@特别字符", "@0xpk6"},
   197  		{"special chars in username case2", "@特别字符 ", "@0xpk6 "},
   198  		{"special chars in username case3", " @特别 字符 ", " @0xpk6 "},
   199  	}
   200  
   201  	for _, tt := range tests {
   202  		t.Run(tt.name, func(t *testing.T) {
   203  			got := ReplaceMentions(tt.text, users)
   204  			if got != tt.expected {
   205  				t.Errorf("testing %q, ReplaceMentions(%q) got %q, expected %q", tt.name, tt.text, got, tt.expected)
   206  			}
   207  		})
   208  	}
   209  }
   210  
   211  func TestGetAtSignIdxs(t *testing.T) {
   212  	tests := []struct {
   213  		name  string
   214  		text  string
   215  		start int
   216  		want  []int
   217  	}{
   218  		{
   219  			name:  "no @ sign",
   220  			text:  "hello world",
   221  			start: 0,
   222  			want:  []int{},
   223  		},
   224  		{
   225  			name:  "single @ sign",
   226  			text:  "hello @world",
   227  			start: 0,
   228  			want:  []int{6},
   229  		},
   230  		{
   231  			name:  "multiple @ signs",
   232  			text:  "@hello @world @again",
   233  			start: 0,
   234  			want:  []int{0, 7, 14},
   235  		},
   236  		{
   237  			name:  "start after first @ sign",
   238  			text:  "hello @world",
   239  			start: 6,
   240  			want:  []int{12},
   241  		},
   242  		{
   243  			name:  "start after second @ sign",
   244  			text:  "hello @world @again",
   245  			start: 8,
   246  			want:  []int{14, 21},
   247  		},
   248  		{
   249  			name:  "start after last @ sign",
   250  			text:  "hello @world @again",
   251  			start: 15,
   252  			want:  []int{21, 28},
   253  		},
   254  	}
   255  	for _, tt := range tests {
   256  		t.Run(tt.name, func(t *testing.T) {
   257  			got := getAtSignIdxs(tt.text, tt.start)
   258  			if !reflect.DeepEqual(got, tt.want) {
   259  				t.Errorf("getAtSignIdxs() = %v, want %v", got, tt.want)
   260  			}
   261  		})
   262  	}
   263  }
   264  
   265  func TestToInfo(t *testing.T) {
   266  	newText := " "
   267  	t.Run("toInfo base case", func(t *testing.T) {
   268  		expected := &MentionState{
   269  			AtSignIdx: 2,
   270  			AtIdxs: []*AtIndexEntry{
   271  				{
   272  					Checked:   true,
   273  					Mentioned: true,
   274  					From:      2,
   275  					To:        17,
   276  				},
   277  			},
   278  			MentionEnd:   19,
   279  			PreviousText: "",
   280  			NewText:      newText,
   281  			Start:        18,
   282  			End:          18,
   283  		}
   284  
   285  		inputSegments := []InputSegment{
   286  			{Type: Text, Value: "H."},
   287  			{Type: Mention, Value: "@helpinghand.eth"},
   288  			{Type: Text, Value: " "},
   289  		}
   290  
   291  		actual := toInfo(inputSegments)
   292  
   293  		if !reflect.DeepEqual(expected.AtIdxs, actual.AtIdxs) {
   294  			t.Errorf("Expected AtIdxs: %#v, but got: %#v", expected.AtIdxs, actual.AtIdxs)
   295  		}
   296  
   297  		expected.AtIdxs = nil
   298  		actual.AtIdxs = nil
   299  
   300  		if !reflect.DeepEqual(expected, actual) {
   301  			t.Errorf("Expected %#v, but got %#v", expected, actual)
   302  		}
   303  	})
   304  }
   305  
   306  func TestToInputField(t *testing.T) {
   307  	testCases := []struct {
   308  		name     string
   309  		input    string
   310  		expected []InputSegment
   311  	}{
   312  		{
   313  			"only text",
   314  			"parse-text",
   315  			[]InputSegment{{Type: Text, Value: "parse-text"}},
   316  		},
   317  		{
   318  			"in the middle",
   319  			"hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he",
   320  			[]InputSegment{
   321  				{Type: Text, Value: "hey "},
   322  				{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
   323  				{Type: Text, Value: " he"},
   324  			},
   325  		},
   326  		{
   327  			"at the beginning",
   328  			"@0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he",
   329  			[]InputSegment{
   330  				{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
   331  				{Type: Text, Value: " he"},
   332  			},
   333  		},
   334  		{
   335  			"at the end",
   336  			"hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073",
   337  			[]InputSegment{
   338  				{Type: Text, Value: "hey "},
   339  				{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
   340  			},
   341  		},
   342  		{
   343  			"invalid",
   344  			"invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073",
   345  			[]InputSegment{
   346  				{Type: Text, Value: "invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
   347  			},
   348  		},
   349  	}
   350  
   351  	for _, tc := range testCases {
   352  		t.Run(tc.name, func(t *testing.T) {
   353  			result := toInputField(tc.input)
   354  			if !reflect.DeepEqual(result, tc.expected) {
   355  				t.Errorf("Expected: %v, got: %v", tc.expected, result)
   356  			}
   357  		})
   358  	}
   359  }
   360  
   361  func TestSubs(t *testing.T) {
   362  	testCases := []struct {
   363  		name     string
   364  		input    string
   365  		start    int
   366  		end      int
   367  		expected string
   368  	}{
   369  		{
   370  			name:     "Normal case",
   371  			input:    "Hello, world!",
   372  			start:    0,
   373  			end:      5,
   374  			expected: "Hello",
   375  		},
   376  		{
   377  			name:     "Start index out of range (negative)",
   378  			input:    "Hello, world!",
   379  			start:    -5,
   380  			end:      5,
   381  			expected: "Hello",
   382  		},
   383  		{
   384  			name:     "End index out of range",
   385  			input:    "Hello, world!",
   386  			start:    7,
   387  			end:      50,
   388  			expected: "world!",
   389  		},
   390  		{
   391  			name:     "Start index greater than end index",
   392  			input:    "Hello, world!",
   393  			start:    10,
   394  			end:      5,
   395  			expected: ", wor",
   396  		},
   397  		{
   398  			name:     "Both indices out of range",
   399  			input:    "Hello, world!",
   400  			start:    -5,
   401  			end:      50,
   402  			expected: "Hello, world!",
   403  		},
   404  		{
   405  			name:     "Start index negative, end index out of range",
   406  			input:    "Hello, world!",
   407  			start:    -10,
   408  			end:      15,
   409  			expected: "Hello, world!",
   410  		},
   411  		{
   412  			name:     "Start index negative, end index within range",
   413  			input:    "Hello, world!",
   414  			start:    -10,
   415  			end:      5,
   416  			expected: "Hello",
   417  		},
   418  		{
   419  			name:     "Start index negative, end index negative",
   420  			input:    "Hello, world!",
   421  			start:    -10,
   422  			end:      -5,
   423  			expected: "",
   424  		},
   425  
   426  		{
   427  			name:     "Start index zero, end index zero",
   428  			input:    "Hello, world!",
   429  			start:    0,
   430  			end:      0,
   431  			expected: "",
   432  		},
   433  		{
   434  			name:     "Start index positive, end index zero",
   435  			input:    "Hello, world!",
   436  			start:    3,
   437  			end:      0,
   438  			expected: "Hel",
   439  		},
   440  		{
   441  			name:     "Start index equal to input length",
   442  			input:    "Hello, world!",
   443  			start:    13,
   444  			end:      15,
   445  			expected: "",
   446  		},
   447  		{
   448  			name:     "End index negative",
   449  			input:    "Hello, world!",
   450  			start:    5,
   451  			end:      -5,
   452  			expected: "Hello",
   453  		},
   454  		{
   455  			name:     "Start and end indices equal and negative",
   456  			input:    "Hello, world!",
   457  			start:    -3,
   458  			end:      -3,
   459  			expected: "",
   460  		},
   461  		{
   462  			name:     "Start index greater than input length",
   463  			input:    "Hello, world!",
   464  			start:    15,
   465  			end:      20,
   466  			expected: "",
   467  		},
   468  		{
   469  			name:     "End index equal to input length",
   470  			input:    "Hello, world!",
   471  			start:    0,
   472  			end:      13,
   473  			expected: "Hello, world!",
   474  		},
   475  	}
   476  
   477  	for _, tc := range testCases {
   478  		t.Run(tc.name, func(t *testing.T) {
   479  			actual := subs(tc.input, tc.start, tc.end)
   480  			if actual != tc.expected {
   481  				t.Errorf("Test case '%s': expected '%s', got '%s'", tc.name, tc.expected, actual)
   482  			}
   483  		})
   484  	}
   485  }
   486  
   487  func TestLastIndexOf(t *testing.T) {
   488  	atSignIdx := lastIndexOfAtSign("@", 0)
   489  	require.Equal(t, 0, atSignIdx)
   490  
   491  	atSignIdx = lastIndexOfAtSign("@@", 1)
   492  	require.Equal(t, 1, atSignIdx)
   493  
   494  	//at-sign-idx 0 text @t searched-text t start 2 end 2 new-text
   495  	atSignIdx = lastIndexOfAtSign("@t", 2)
   496  	require.Equal(t, 0, atSignIdx)
   497  
   498  	atSignIdx = lastIndexOfAtSign("at", 3)
   499  	require.Equal(t, -1, atSignIdx)
   500  }
   501  
   502  func TestDiffText(t *testing.T) {
   503  	testCases := []struct {
   504  		oldText  string
   505  		newText  string
   506  		expected *TextDiff
   507  	}{
   508  		{
   509  			oldText: "",
   510  			newText: "A",
   511  			expected: &TextDiff{
   512  				start:        0,
   513  				end:          0,
   514  				previousText: "",
   515  				newText:      "A",
   516  				operation:    textOperationAdd,
   517  			},
   518  		},
   519  		{
   520  			oldText: "A",
   521  			newText: "Ab",
   522  			expected: &TextDiff{
   523  				start:        1,
   524  				end:          1,
   525  				previousText: "A",
   526  				newText:      "b",
   527  				operation:    textOperationAdd,
   528  			},
   529  		},
   530  		{
   531  			oldText: "Ab",
   532  			newText: "Abc",
   533  			expected: &TextDiff{
   534  				start:        2,
   535  				end:          2,
   536  				previousText: "Ab",
   537  				newText:      "c",
   538  				operation:    textOperationAdd,
   539  			},
   540  		},
   541  		{
   542  			oldText: "Ab",
   543  			newText: "cAb",
   544  			expected: &TextDiff{
   545  				start:        0,
   546  				end:          0,
   547  				previousText: "Ab",
   548  				newText:      "c",
   549  				operation:    textOperationAdd,
   550  			},
   551  		},
   552  		{
   553  			oldText: "Ac",
   554  			newText: "Adc",
   555  			expected: &TextDiff{
   556  				start:        1,
   557  				end:          1,
   558  				previousText: "Ac",
   559  				newText:      "d",
   560  				operation:    textOperationAdd,
   561  			},
   562  		},
   563  		{
   564  			oldText: "Adc",
   565  			newText: "Ad ee c",
   566  			expected: &TextDiff{
   567  				start:        2,
   568  				end:          2,
   569  				previousText: "Adc",
   570  				newText:      " ee ",
   571  				operation:    textOperationAdd,
   572  			},
   573  		},
   574  		{
   575  			oldText: "Ad ee c",
   576  			newText: "A fff d ee c",
   577  			expected: &TextDiff{
   578  				start:        1,
   579  				end:          1,
   580  				previousText: "Ad ee c",
   581  				newText:      " fff ",
   582  				operation:    textOperationAdd,
   583  			},
   584  		},
   585  		{
   586  			oldText: "Abc",
   587  			newText: "Ac",
   588  			expected: &TextDiff{
   589  				start:        1,
   590  				end:          1,
   591  				previousText: "Abc",
   592  				newText:      "",
   593  				operation:    textOperationDelete,
   594  			},
   595  		},
   596  		{
   597  			oldText: "Abcd",
   598  			newText: "Ab",
   599  			expected: &TextDiff{
   600  				start:        2,
   601  				end:          3,
   602  				previousText: "Abcd",
   603  				newText:      "",
   604  				operation:    textOperationDelete,
   605  			},
   606  		},
   607  		{
   608  			oldText: "Abcd",
   609  			newText: "bcd",
   610  			expected: &TextDiff{
   611  				start:        0,
   612  				end:          0,
   613  				previousText: "Abcd",
   614  				newText:      "",
   615  				operation:    textOperationDelete,
   616  			},
   617  		},
   618  		{
   619  			oldText: "Abcd你好",
   620  			newText: "Abcd你",
   621  			expected: &TextDiff{
   622  				start:        5,
   623  				end:          5,
   624  				previousText: "Abcd你好",
   625  				newText:      "",
   626  				operation:    textOperationDelete,
   627  			},
   628  		},
   629  		{
   630  			oldText: "Abcd你好",
   631  			newText: "Abcd",
   632  			expected: &TextDiff{
   633  				start:        4,
   634  				end:          5,
   635  				previousText: "Abcd你好",
   636  				newText:      "",
   637  				operation:    textOperationDelete,
   638  			},
   639  		},
   640  		{
   641  			oldText: "A fff d ee c",
   642  			newText: " fff d ee c",
   643  			expected: &TextDiff{
   644  				start:        0,
   645  				end:          0,
   646  				previousText: "A fff d ee c",
   647  				newText:      "",
   648  				operation:    textOperationDelete,
   649  			},
   650  		},
   651  		{
   652  			oldText: " fff d ee c",
   653  			newText: " fffee c",
   654  			expected: &TextDiff{
   655  				start:        4,
   656  				end:          6,
   657  				previousText: " fff d ee c",
   658  				newText:      "",
   659  				operation:    textOperationDelete,
   660  			},
   661  		},
   662  		{
   663  			oldText:  "abc",
   664  			newText:  "abc",
   665  			expected: nil,
   666  		},
   667  		{
   668  			oldText: "abc",
   669  			newText: "ghij",
   670  			expected: &TextDiff{
   671  				start:        0,
   672  				end:          2,
   673  				previousText: "abc",
   674  				newText:      "ghij",
   675  				operation:    textOperationReplace,
   676  			},
   677  		},
   678  		{
   679  			oldText: "abc",
   680  			newText: "babcd",
   681  			expected: &TextDiff{
   682  				start:        0,
   683  				end:          2,
   684  				previousText: "abc",
   685  				newText:      "babcd",
   686  				operation:    textOperationReplace,
   687  			},
   688  		},
   689  		{
   690  			oldText: "abc",
   691  			newText: "baebcd",
   692  			expected: &TextDiff{
   693  				start:        0,
   694  				end:          2,
   695  				previousText: "abc",
   696  				newText:      "baebcd",
   697  				operation:    textOperationReplace,
   698  			},
   699  		},
   700  		{
   701  			oldText: "abc",
   702  			newText: "aefc",
   703  			expected: &TextDiff{
   704  				start:        1,
   705  				end:          1,
   706  				previousText: "abc",
   707  				newText:      "ef",
   708  				operation:    textOperationReplace,
   709  			},
   710  		},
   711  		{
   712  			oldText: "abc",
   713  			newText: "adc",
   714  			expected: &TextDiff{
   715  				start:        1,
   716  				end:          1,
   717  				previousText: "abc",
   718  				newText:      "d",
   719  				operation:    textOperationReplace,
   720  			},
   721  		},
   722  		{
   723  			oldText: "abc",
   724  			newText: "abd",
   725  			expected: &TextDiff{
   726  				start:        2,
   727  				end:          2,
   728  				previousText: "abc",
   729  				newText:      "d",
   730  				operation:    textOperationReplace,
   731  			},
   732  		},
   733  		{
   734  			oldText: "abc",
   735  			newText: "cbc",
   736  			expected: &TextDiff{
   737  				start:        0,
   738  				end:          0,
   739  				previousText: "abc",
   740  				newText:      "c",
   741  				operation:    textOperationReplace,
   742  			},
   743  		},
   744  		{
   745  			oldText: "abc",
   746  			newText: "ffbc",
   747  			expected: &TextDiff{
   748  				start:        0,
   749  				end:          0,
   750  				previousText: "abc",
   751  				newText:      "ff",
   752  				operation:    textOperationReplace,
   753  			},
   754  		},
   755  	}
   756  	for i, tc := range testCases {
   757  		t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
   758  			diff := diffText(tc.oldText, tc.newText)
   759  			require.Equal(t, tc.expected, diff)
   760  		})
   761  	}
   762  }
   763  
   764  type MockMentionableUserGetter struct {
   765  	mentionableUserMap map[string]*MentionableUser
   766  }
   767  
   768  func (m *MockMentionableUserGetter) getMentionableUsers(chatID string) (map[string]*MentionableUser, error) {
   769  	return m.mentionableUserMap, nil
   770  }
   771  
   772  func (m *MockMentionableUserGetter) getMentionableUser(chatID string, pk string) (*MentionableUser, error) {
   773  	return m.mentionableUserMap[pk], nil
   774  }
   775  
   776  func TestMentionSuggestionCases(t *testing.T) {
   777  	mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
   778  
   779  	testCases := []struct {
   780  		inputText    string
   781  		expectedSize int
   782  	}{
   783  		{"@", len(mentionableUserMap)},
   784  		{"@u", len(mentionableUserMap)},
   785  		{"@u2", 1},
   786  		{"@u23", 0},
   787  		{"@u2", 1},
   788  		{"@u2 abc", 0},
   789  		{"@u2 abc @u3", 1},
   790  		{"@u2 abc@u3", 0},
   791  		{"@u2 abc@u3 ", 0},
   792  		{"@u2 abc @u3", 1},
   793  	}
   794  
   795  	for i, tc := range testCases {
   796  		t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
   797  			ctx, err := mentionManager.OnChangeText(chatID, tc.inputText, uint64(i+1))
   798  			require.NoError(t, err)
   799  			t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
   800  			require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
   801  		})
   802  	}
   803  }
   804  
   805  func TestMentionSuggestionAfterToInputField(t *testing.T) {
   806  	mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
   807  	_, err := mentionManager.ToInputField(chatID, "abc")
   808  	require.NoError(t, err)
   809  	ctx, err := mentionManager.OnChangeText(chatID, "@", 1)
   810  	require.NoError(t, err)
   811  	require.Equal(t, len(mentionableUserMap), len(ctx.MentionSuggestions))
   812  }
   813  
   814  func TestMentionSuggestionSpecialInputModeForAndroid(t *testing.T) {
   815  	mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
   816  
   817  	testCases := []struct {
   818  		inputText    string
   819  		expectedSize int
   820  	}{
   821  		{"A", 0},
   822  		{"As", 0},
   823  		{"Asd", 0},
   824  		{"Asd@", len(mentionableUserMap)},
   825  	}
   826  
   827  	for i, tc := range testCases {
   828  		t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
   829  			ctx, err := mentionManager.OnChangeText(chatID, tc.inputText, uint64(i+1))
   830  			require.NoError(t, err)
   831  			require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
   832  			t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
   833  		})
   834  	}
   835  }
   836  
   837  func TestMentionSuggestionSpecialChars(t *testing.T) {
   838  	mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
   839  
   840  	testCases := []struct {
   841  		inputText    string
   842  		expectedSize int
   843  	}{
   844  		{"'", 0},
   845  		{"‘", 0},
   846  		{"‘@", len(mentionableUserMap)},
   847  		{"‘@自由人", 1},
   848  	}
   849  
   850  	for i, tc := range testCases {
   851  		t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
   852  			ctx, err := mentionManager.OnChangeText(chatID, tc.inputText, uint64(i+1))
   853  			require.NoError(t, err)
   854  			t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
   855  			require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
   856  		})
   857  	}
   858  }
   859  
   860  func TestMentionSuggestionAtSignSpaceCases(t *testing.T) {
   861  	mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest(t, map[string]*MentionableUser{
   862  		"0xpk1": {
   863  			Contact: &Contact{
   864  				ID:            "0xpk1",
   865  				LocalNickname: "User Number One",
   866  			},
   867  		},
   868  	})
   869  
   870  	testCases := []struct {
   871  		inputText    string
   872  		expectedSize int
   873  	}{
   874  		{"@", len(mentionableUserMap)},
   875  		{"@ ", 0},
   876  		{"@ @", len(mentionableUserMap)},
   877  	}
   878  
   879  	var ctx *ChatMentionContext
   880  	var err error
   881  	for i, tc := range testCases {
   882  		ctx, err = mentionManager.OnChangeText(chatID, tc.inputText, uint64(i+1))
   883  		require.NoError(t, err)
   884  		t.Logf("After OnChangeText, Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
   885  		require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
   886  	}
   887  	require.Len(t, ctx.InputSegments, 2)
   888  	require.Equal(t, Text, ctx.InputSegments[0].Type)
   889  	require.Equal(t, "@ ", ctx.InputSegments[0].Value)
   890  	require.Equal(t, Text, ctx.InputSegments[1].Type)
   891  	require.Equal(t, "@", ctx.InputSegments[1].Value)
   892  }
   893  
   894  func TestSelectMention(t *testing.T) {
   895  	mentionableUsers, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
   896  
   897  	var callID uint64 = 1
   898  	text := "@u2 abc"
   899  	ctx, err := mentionManager.OnChangeText(chatID, text, callID)
   900  	require.NoError(t, err)
   901  	require.Equal(t, 0, len(ctx.MentionSuggestions))
   902  
   903  	callID++
   904  	ctx, err = mentionManager.OnChangeText(chatID, "@u abc", callID)
   905  	require.NoError(t, err)
   906  	require.Equal(t, len(mentionableUsers), len(ctx.MentionSuggestions))
   907  
   908  	ctx, err = mentionManager.SelectMention(chatID, "@u abc", "u2", "0xpk2")
   909  	require.NoError(t, err)
   910  	require.Equal(t, 0, len(ctx.MentionSuggestions))
   911  	require.Equal(t, text, ctx.NewText)
   912  	require.Equal(t, text, ctx.PreviousText)
   913  
   914  	callID++
   915  	ctx, err = mentionManager.OnChangeText(chatID, text, callID)
   916  	require.NoError(t, err)
   917  	require.Equal(t, 0, len(ctx.MentionSuggestions))
   918  }
   919  
   920  func TestInputSegments(t *testing.T) {
   921  	_, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
   922  	var callID uint64 = 1
   923  	ctx, err := mentionManager.OnChangeText(chatID, "@u1", callID)
   924  	require.NoError(t, err)
   925  	require.Equal(t, 1, len(ctx.InputSegments))
   926  	require.Equal(t, Text, ctx.InputSegments[0].Type)
   927  	require.Equal(t, "@u1", ctx.InputSegments[0].Value)
   928  
   929  	callID++
   930  	ctx, err = mentionManager.OnChangeText(chatID, "@u1 @User Number One", callID)
   931  	require.NoError(t, err)
   932  	require.Equal(t, 2, len(ctx.InputSegments))
   933  	require.Equal(t, Text, ctx.InputSegments[0].Type)
   934  	require.Equal(t, "@u1 ", ctx.InputSegments[0].Value)
   935  	require.Equal(t, Mention, ctx.InputSegments[1].Type)
   936  	require.Equal(t, "@User Number One", ctx.InputSegments[1].Value)
   937  
   938  	callID++
   939  	ctx, err = mentionManager.OnChangeText(chatID, "@u1 @User Number O", callID)
   940  	require.NoError(t, err)
   941  	require.Equal(t, 2, len(ctx.InputSegments))
   942  	require.Equal(t, Text, ctx.InputSegments[1].Type)
   943  	require.Equal(t, "@User Number O", ctx.InputSegments[1].Value)
   944  
   945  	callID++
   946  	ctx, err = mentionManager.OnChangeText(chatID, "@u2 @User Number One", callID)
   947  	require.NoError(t, err)
   948  	require.Equal(t, 3, len(ctx.InputSegments))
   949  	require.Equal(t, Mention, ctx.InputSegments[0].Type)
   950  	require.Equal(t, "@u2", ctx.InputSegments[0].Value)
   951  	require.Equal(t, Text, ctx.InputSegments[1].Type)
   952  	require.Equal(t, " ", ctx.InputSegments[1].Value)
   953  	require.Equal(t, Mention, ctx.InputSegments[2].Type)
   954  	require.Equal(t, "@User Number One", ctx.InputSegments[2].Value)
   955  
   956  	callID++
   957  	ctx, err = mentionManager.OnChangeText(chatID, "@u2 @User Number One a ", callID)
   958  	require.NoError(t, err)
   959  	require.Equal(t, 4, len(ctx.InputSegments))
   960  	require.Equal(t, Mention, ctx.InputSegments[2].Type)
   961  	require.Equal(t, "@User Number One", ctx.InputSegments[2].Value)
   962  	require.Equal(t, Text, ctx.InputSegments[3].Type)
   963  	require.Equal(t, " a ", ctx.InputSegments[3].Value)
   964  
   965  	callID++
   966  	ctx, err = mentionManager.OnChangeText(chatID, "@u2 @User Numbed One a ", callID)
   967  	require.NoError(t, err)
   968  	require.Equal(t, 3, len(ctx.InputSegments))
   969  	require.Equal(t, Mention, ctx.InputSegments[0].Type)
   970  	require.Equal(t, "@u2", ctx.InputSegments[0].Value)
   971  	require.Equal(t, Text, ctx.InputSegments[2].Type)
   972  	require.Equal(t, "@User Numbed One a ", ctx.InputSegments[2].Value)
   973  
   974  	callID++
   975  	ctx, err = mentionManager.OnChangeText(chatID, "@ @ ", callID)
   976  	require.NoError(t, err)
   977  	require.Equal(t, 2, len(ctx.InputSegments))
   978  	require.Equal(t, Text, ctx.InputSegments[0].Type)
   979  	require.Equal(t, "@ ", ctx.InputSegments[0].Value)
   980  	require.Equal(t, Text, ctx.InputSegments[1].Type)
   981  	require.Equal(t, "@ ", ctx.InputSegments[1].Value)
   982  
   983  	callID++
   984  	ctx, err = mentionManager.OnChangeText(chatID, "@u3 @ ", callID)
   985  	require.NoError(t, err)
   986  	require.Equal(t, 3, len(ctx.InputSegments))
   987  	require.Equal(t, Mention, ctx.InputSegments[0].Type)
   988  	require.Equal(t, "@u3", ctx.InputSegments[0].Value)
   989  	require.Equal(t, Text, ctx.InputSegments[1].Type)
   990  	require.Equal(t, " ", ctx.InputSegments[1].Value)
   991  	require.Equal(t, Text, ctx.InputSegments[2].Type)
   992  	require.Equal(t, "@ ", ctx.InputSegments[2].Value)
   993  
   994  	callID++
   995  	_, err = mentionManager.OnChangeText(chatID, " @ @User Number Three ", callID)
   996  	require.NoError(t, err)
   997  	callID++
   998  	_, err = mentionManager.OnChangeText(chatID, "@U @ @User Number Three ", callID)
   999  	require.NoError(t, err)
  1000  	ctx, err = mentionManager.SelectMention(chatID, "@U @ @User Number Three ", "User Number Three", "0xpk3")
  1001  	require.NoError(t, err)
  1002  	require.Equal(t, 2, mentionTypeNum(ctx.InputSegments))
  1003  
  1004  	callID++
  1005  	ctx, _ = mentionManager.OnChangeText(chatID, "@User Number Threea", callID)
  1006  	require.Equal(t, 0, mentionTypeNum(ctx.InputSegments))
  1007  
  1008  	callID++
  1009  	ctx, _ = mentionManager.OnChangeText(chatID, "@User Number Threea\n@u2\nabc@u3 asa", callID)
  1010  	require.Equal(t, 2, mentionTypeNum(ctx.InputSegments))
  1011  	callID++
  1012  	ctx, _ = mentionManager.OnChangeText(chatID, "@User Number Thre\n@u2\nabc@u3 asa", callID)
  1013  	require.Equal(t, 2, mentionTypeNum(ctx.InputSegments))
  1014  	require.Equal(t, "@u2", ctx.InputSegments[1].Value)
  1015  	require.Equal(t, "@u3", ctx.InputSegments[3].Value)
  1016  }
  1017  
  1018  func mentionTypeNum(inputSegments []InputSegment) int {
  1019  	var num int
  1020  	for _, s := range inputSegments {
  1021  		if s.Type == Mention {
  1022  			num++
  1023  		}
  1024  	}
  1025  	return num
  1026  }
  1027  
  1028  func setupMentionSuggestionTest(t *testing.T, mentionableUserMapInput map[string]*MentionableUser) (map[string]*MentionableUser, string, *MentionManager) {
  1029  	mentionableUserMap := mentionableUserMapInput
  1030  	if mentionableUserMap == nil {
  1031  		mentionableUserMap = getDefaultMentionableUserMap()
  1032  	}
  1033  
  1034  	for _, u := range mentionableUserMap {
  1035  		addSearchablePhrases(u)
  1036  	}
  1037  
  1038  	mockMentionableUserGetter := &MockMentionableUserGetter{
  1039  		mentionableUserMap: mentionableUserMap,
  1040  	}
  1041  
  1042  	chatID := "0xchatID"
  1043  	allChats := new(chatMap)
  1044  	allChats.Store(chatID, &Chat{})
  1045  
  1046  	key, err := crypto.GenerateKey()
  1047  	require.NoError(t, err)
  1048  	mentionManager := &MentionManager{
  1049  		mentionableUserGetter: mockMentionableUserGetter,
  1050  		mentionContexts:       make(map[string]*ChatMentionContext),
  1051  		Messenger: &Messenger{
  1052  			allChats: allChats,
  1053  			identity: key,
  1054  		},
  1055  		logger: logutils.ZapLogger().Named("MentionManager"),
  1056  	}
  1057  
  1058  	return mentionableUserMap, chatID, mentionManager
  1059  }
  1060  
  1061  func getDefaultMentionableUserMap() map[string]*MentionableUser {
  1062  	return map[string]*MentionableUser{
  1063  		"0xpk1": {
  1064  			Contact: &Contact{
  1065  				ID:            "0xpk1",
  1066  				LocalNickname: "User Number One",
  1067  			},
  1068  		},
  1069  		"0xpk2": {
  1070  			Contact: &Contact{
  1071  				ID:            "0xpk2",
  1072  				LocalNickname: "u2",
  1073  				ENSVerified:   true,
  1074  				EnsName:       "User Number Two",
  1075  			},
  1076  		},
  1077  		"0xpk3": {
  1078  			Contact: &Contact{
  1079  				ID:            "0xpk3",
  1080  				LocalNickname: "u3",
  1081  				ENSVerified:   true,
  1082  				EnsName:       "User Number Three",
  1083  			},
  1084  		},
  1085  		"0xpk4": {
  1086  			Contact: &Contact{
  1087  				ID:            "0xpk4",
  1088  				LocalNickname: "自由人",
  1089  				ENSVerified:   true,
  1090  				EnsName:       "User Number Four",
  1091  			},
  1092  		},
  1093  	}
  1094  }