Updated on 2026-08-14
This commit is contained in:
parent
066ab573b3
commit
3fefb7bf52
21 changed files with 944 additions and 56 deletions
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.model
|
|||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.domain.addressbook.model.ContactName.Companion.invoke
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
|
|
@ -31,11 +32,24 @@ data class ContactName private constructor(val value: String) {
|
|||
|
||||
companion object {
|
||||
|
||||
const val MIN_LENGTH = 1
|
||||
private const val MIN_LENGTH = 1
|
||||
const val MAX_LENGTH = 50
|
||||
|
||||
/** Letters, numbers and spaces only — forbids emoji, new lines, tabs, special symbols and html/scripts. */
|
||||
private val allowedPattern = Regex("^[\\p{L}\\p{N} ]+$")
|
||||
/**
|
||||
* Allows letters of any locale (`\p{L}`) and their combining marks (`\p{M}`, which also covers emoji
|
||||
* variation selectors and keycap marks), digits (`\p{N}`), a regular space, and emoji — symbols (`\p{So}`,
|
||||
* including flags / regional indicators), emoji skin-tone modifiers (`\p{Sk}`) and the zero-width joiner
|
||||
* (U+200D) used in emoji sequences.
|
||||
*
|
||||
* Everything else is rejected, which covers the forbidden set: line breaks, tabs and other control
|
||||
* characters, invisible/format unicode (zero-width spaces, BOM, …), exotic spaces, and HTML/script symbols.
|
||||
*
|
||||
* The leading lookahead requires at least one visible "base" character (letter / digit / emoji symbol), so a
|
||||
* name made up only of zero-width joiners, combining marks, modifiers or spaces (i.e. effectively invisible)
|
||||
* is rejected.
|
||||
*/
|
||||
private val allowedPattern =
|
||||
Regex("^(?=.*[\\p{L}\\p{N}\\p{So}])[\\p{L}\\p{M}\\p{N}\\p{So}\\p{Sk}\\u0020\\u200D]+$")
|
||||
|
||||
operator fun invoke(value: String): Either<Error, ContactName> = either {
|
||||
val trimmed = value.trim()
|
||||
|
|
|
|||
|
|
@ -39,8 +39,33 @@ class ContactNameTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `emoji is rejected`() {
|
||||
assertThat(ContactName("Alice 😀").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
|
||||
fun `simple emoji is accepted`() {
|
||||
assertThat(ContactName("Alice 😀").isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emoji-only name is accepted`() {
|
||||
assertThat(ContactName("😀").isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flag emoji is accepted`() {
|
||||
assertThat(ContactName("Team 🇺🇸").isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zwj emoji sequence is accepted`() {
|
||||
assertThat(ContactName("Family 👨👩👧").isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emoji with variation selector is accepted`() {
|
||||
assertThat(ContactName("Love ❤️").isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-latin letters are accepted`() {
|
||||
assertThat(ContactName("Алёша 大阪").isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -53,6 +78,16 @@ class ContactNameTest {
|
|||
assertThat(ContactName("Ali\tce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zero-width space is rejected`() {
|
||||
assertThat(ContactName("Ali\u200Bce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-breaking space is rejected`() {
|
||||
assertThat(ContactName("Ali\u00A0ce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `html script is rejected`() {
|
||||
assertThat(ContactName("<script>alert(1)</script>").leftOrNull())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue