Skip to content

Commit d9f4821

Browse files
committed
Add unit tests for FindByViewModel.
1 parent f8d7c27 commit d9f4821

File tree

3 files changed

+222
-8
lines changed

3 files changed

+222
-8
lines changed

app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,13 @@ class FindByActivity : PassphraseRequiredActivity() {
134134
onNavigationClick = { finishAfterTransition() },
135135
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24)
136136
) {
137-
val context = LocalContext.current
138-
139137
Content(
140138
paddingValues = it,
141139
state = state,
142140
onUserEntryChanged = viewModel::onUserEntryChanged,
143141
onNextClick = {
144142
lifecycleScope.launch {
145-
when (val result = viewModel.onNextClicked(context)) {
143+
when (val result = viewModel.onNextClicked()) {
146144
is FindByResult.Success -> {
147145
setResult(RESULT_OK, Intent().putExtra(RECIPIENT_ID, result.recipientId))
148146
finishAfterTransition()
@@ -195,7 +193,7 @@ class FindByActivity : PassphraseRequiredActivity() {
195193
val cleansed = state.userEntry.removePrefix(state.selectedCountry.countryCode.toString())
196194
E164Util.formatAsE164WithCountryCodeForDisplay(state.selectedCountry.countryCode.toString(), cleansed)
197195
}
198-
stringResource(id = R.string.FindByActivity__s_is_not_a_valid_phone_number, state.userEntry)
196+
stringResource(id = R.string.FindByActivity__s_is_not_a_valid_phone_number, formattedNumber)
199197
}
200198

201199
Dialogs.SimpleAlertDialog(

app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
package org.thoughtcrime.securesms.recipients.ui.findby
77

8-
import android.content.Context
98
import androidx.annotation.WorkerThread
109
import androidx.compose.runtime.State
1110
import androidx.compose.runtime.mutableStateOf
@@ -43,13 +42,13 @@ class FindByViewModel(
4342
internalState.value = state.value.copy(selectedCountry = country)
4443
}
4544

46-
suspend fun onNextClicked(context: Context): FindByResult {
45+
suspend fun onNextClicked(): FindByResult {
4746
internalState.value = state.value.copy(isLookupInProgress = true)
4847
val findByResult = viewModelScope.async(context = Dispatchers.IO) {
4948
if (state.value.mode == FindByMode.USERNAME) {
5049
performUsernameLookup()
5150
} else {
52-
performPhoneLookup(context)
51+
performPhoneLookup()
5352
}
5453
}.await()
5554

@@ -73,7 +72,7 @@ class FindByViewModel(
7372
}
7473

7574
@WorkerThread
76-
private fun performPhoneLookup(context: Context): FindByResult {
75+
private fun performPhoneLookup(): FindByResult {
7776
val stateSnapshot = state.value
7877
val countryCode = stateSnapshot.selectedCountry.countryCode
7978
val nationalNumber = stateSnapshot.userEntry.removePrefix(countryCode.toString())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright 2025 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.thoughtcrime.securesms.recipients.ui.findby
7+
8+
import io.mockk.every
9+
import io.mockk.mockk
10+
import io.mockk.mockkObject
11+
import io.mockk.mockkStatic
12+
import io.mockk.unmockkObject
13+
import io.mockk.unmockkStatic
14+
import kotlinx.coroutines.test.runTest
15+
import org.junit.After
16+
import org.junit.Assert.assertEquals
17+
import org.junit.Assert.assertTrue
18+
import org.junit.Before
19+
import org.junit.Test
20+
import org.thoughtcrime.securesms.dependencies.AppDependencies
21+
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
22+
import org.thoughtcrime.securesms.recipients.Recipient
23+
import org.thoughtcrime.securesms.recipients.RecipientId
24+
import org.thoughtcrime.securesms.recipients.RecipientRepository
25+
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
26+
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
27+
import java.util.Optional
28+
29+
class FindByViewModelTest {
30+
31+
private val liveRecipientCache = mockk<LiveRecipientCache>(relaxed = true)
32+
private lateinit var viewModel: FindByViewModel
33+
34+
@Before
35+
fun setup() {
36+
mockkStatic(AppDependencies::class)
37+
every { AppDependencies.recipientCache } returns liveRecipientCache
38+
every { Recipient.self() } returns mockk {
39+
every { e164 } returns Optional.of("+15551234")
40+
}
41+
}
42+
43+
@After
44+
fun tearDown() {
45+
unmockkStatic(AppDependencies::class)
46+
}
47+
48+
@Test
49+
fun `Given phone number mode, when I change user entry, then I expect digits only`() {
50+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
51+
52+
viewModel.onUserEntryChanged("123abc456")
53+
val result = viewModel.state.value.userEntry
54+
55+
assertEquals("123456", result)
56+
}
57+
58+
@Test
59+
fun `Given username mode, when I change user entry, then I expect unaltered value`() {
60+
viewModel = FindByViewModel(FindByMode.USERNAME)
61+
62+
viewModel.onUserEntryChanged("username123")
63+
val result = viewModel.state.value.userEntry
64+
65+
assertEquals("username123", result)
66+
}
67+
68+
@Test
69+
fun `Given a selected country, when I update it, then I expect the state to reflect it`() {
70+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
71+
val country = Country(emoji = "", name = "United States", countryCode = 1, regionCode = "US")
72+
73+
viewModel.onCountrySelected(country)
74+
val result = viewModel.state.value.selectedCountry
75+
76+
assertEquals(country, result)
77+
}
78+
79+
@Test
80+
fun `Given invalid username, when I click next, then I expect InvalidEntry`() = runTest {
81+
viewModel = FindByViewModel(FindByMode.USERNAME)
82+
83+
viewModel.onUserEntryChanged("invalid username")
84+
val result = viewModel.onNextClicked()
85+
86+
assertTrue(result is FindByResult.InvalidEntry)
87+
}
88+
89+
@Test
90+
fun `Given empty phone number, when I click next, then I expect InvalidEntry`() = runTest {
91+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
92+
93+
viewModel.onUserEntryChanged("")
94+
mockkStatic(RecipientRepository::class).apply {
95+
every { RecipientRepository.lookupNewE164(any()) } returns RecipientRepository.LookupResult.InvalidEntry
96+
}
97+
98+
val result = viewModel.onNextClicked()
99+
100+
assertTrue(result is FindByResult.InvalidEntry)
101+
102+
unmockkObject(RecipientRepository)
103+
}
104+
105+
@Test
106+
fun `Given valid phone lookup, when I click next, then I expect Success`() = runTest {
107+
val recipientId = RecipientId.from(123L)
108+
109+
mockkStatic(RecipientRepository::class).apply {
110+
every { RecipientRepository.lookupNewE164("+15551234") } returns
111+
RecipientRepository.LookupResult.Success(recipientId)
112+
}
113+
114+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
115+
val country = Country(emoji = "🇺🇸", name = "United States", countryCode = 1, regionCode = "US")
116+
117+
viewModel.onCountrySelected(country)
118+
viewModel.onUserEntryChanged("5551234")
119+
120+
val result = viewModel.onNextClicked()
121+
122+
assertTrue(result is FindByResult.Success)
123+
assertEquals((result as FindByResult.Success).recipientId, recipientId)
124+
125+
unmockkObject(RecipientRepository)
126+
}
127+
128+
@Test
129+
fun `Given unknown phone lookup, when I click next, then I expect NotFound`() = runTest {
130+
val recipientId = RecipientId.from(123L)
131+
132+
mockkStatic(RecipientRepository::class).apply {
133+
every { RecipientRepository.lookupNewE164(any()) } returns RecipientRepository.LookupResult.NotFound(recipientId)
134+
}
135+
136+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
137+
viewModel.onUserEntryChanged("0000000000")
138+
139+
val result = viewModel.onNextClicked()
140+
141+
assertTrue(result is FindByResult.NotFound)
142+
assertEquals((result as FindByResult.NotFound).recipientId, recipientId)
143+
144+
unmockkObject(RecipientRepository)
145+
}
146+
147+
@Test
148+
fun `Given matching country name, when I filter countries, then I expect matched countries`() {
149+
val countries = listOf(
150+
Country(emoji = "🇺🇸", name = "United States", countryCode = 1, regionCode = "US"),
151+
Country(emoji = "🇨🇦", name = "Canada", countryCode = 1, regionCode = "CA"),
152+
Country(emoji = "🇬🇧", name = "United Kingdom", countryCode = 44, regionCode = "GB")
153+
)
154+
155+
mockkObject(CountryUtils).apply {
156+
every { CountryUtils.getCountries() } returns countries
157+
}
158+
159+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
160+
161+
viewModel.filterCountries("United")
162+
val result = viewModel.state.value.filteredCountries
163+
164+
assertEquals(2, result.size)
165+
assertTrue(result.any { it.name == "United States" })
166+
assertTrue(result.any { it.name == "United Kingdom" })
167+
168+
unmockkObject(CountryUtils)
169+
}
170+
171+
@Test
172+
fun `Given matching country code, when I filter countries, then I expect matched countries`() {
173+
val countries = listOf(
174+
Country(emoji = "🇺🇸", name = "United States", countryCode = 1, regionCode = "US"),
175+
Country(emoji = "🇨🇦", name = "Canada", countryCode = 1, regionCode = "CA"),
176+
Country(emoji = "🇬🇧", name = "United Kingdom", countryCode = 44, regionCode = "GB")
177+
)
178+
179+
mockkObject(CountryUtils).apply {
180+
every { CountryUtils.getCountries() } returns countries
181+
}
182+
183+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
184+
185+
viewModel.filterCountries("1")
186+
val result = viewModel.state.value.filteredCountries
187+
188+
assertEquals(2, result.size)
189+
assertTrue(result.any { it.name == "United States" })
190+
assertTrue(result.any { it.name == "Canada" })
191+
192+
unmockkObject(CountryUtils)
193+
}
194+
195+
@Test
196+
fun `Given empty country filter, when I filter countries, then I expect empty countries list`() {
197+
val countries = listOf(
198+
Country(emoji = "🇺🇸", name = "United States", countryCode = 1, regionCode = "US"),
199+
Country(emoji = "🇨🇦", name = "Canada", countryCode = 1, regionCode = "CA"),
200+
Country(emoji = "🇬🇧", name = "United Kingdom", countryCode = 44, regionCode = "GB")
201+
)
202+
203+
mockkObject(CountryUtils).apply {
204+
every { CountryUtils.getCountries() } returns countries
205+
}
206+
207+
viewModel = FindByViewModel(FindByMode.PHONE_NUMBER)
208+
209+
viewModel.filterCountries("")
210+
val result = viewModel.state.value.filteredCountries
211+
212+
assertEquals(0, result.size)
213+
214+
unmockkObject(CountryUtils)
215+
}
216+
}
217+

0 commit comments

Comments
 (0)