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 }