Skip to content

Commit 33fe152

Browse files
committed
test: cover hardware connect flow
1 parent 2786f24 commit 33fe152

2 files changed

Lines changed: 218 additions & 0 deletions

File tree

app/src/test/java/to/bitkit/repositories/HwWalletRepoTest.kt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.synonym.bitkitcore.AccountType
44
import com.synonym.bitkitcore.Activity
55
import com.synonym.bitkitcore.HistoryTransaction
66
import com.synonym.bitkitcore.PaymentType
7+
import com.synonym.bitkitcore.TrezorFeatures
78
import com.synonym.bitkitcore.TxDirection
89
import com.synonym.bitkitcore.WalletBalance
910
import com.synonym.bitkitcore.WatcherEvent
@@ -34,12 +35,14 @@ import to.bitkit.models.toCoreNetwork
3435
import to.bitkit.test.BaseUnitTest
3536
import to.bitkit.utils.AppError
3637
import kotlin.test.assertEquals
38+
import kotlin.test.assertTrue
3739
import kotlin.time.Clock
3840
import kotlin.time.Duration.Companion.seconds
3941
import kotlin.time.ExperimentalTime
4042
import kotlin.time.Instant
4143

4244
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
45+
@Suppress("LargeClass")
4346
class HwWalletRepoTest : BaseUnitTest() {
4447

4548
private val trezorRepo = mock<TrezorRepo>()
@@ -752,6 +755,72 @@ class HwWalletRepoTest : BaseUnitTest() {
752755
confirmations = 3u,
753756
)
754757

758+
@Test
759+
fun `scan delegates to trezorRepo`() = test {
760+
whenever(trezorRepo.scan()).thenReturn(Result.success(emptyList()))
761+
val sut = createRepo()
762+
763+
sut.scan()
764+
765+
verify(trezorRepo).scan()
766+
}
767+
768+
@Test
769+
fun `connect delegates to trezorRepo`() = test {
770+
val features = mock<TrezorFeatures>()
771+
whenever(trezorRepo.connect("dev1")).thenReturn(Result.success(features))
772+
val sut = createRepo()
773+
774+
sut.connect("dev1")
775+
776+
verify(trezorRepo).connect("dev1")
777+
}
778+
779+
@Test
780+
fun `setDeviceLabel persists the trimmed custom label on the matching device`() = test {
781+
whenever(hwWalletStore.loadKnownDevices()).thenReturn(listOf(device))
782+
val sut = createRepo()
783+
784+
val result = sut.setDeviceLabel("dev1", " My Cold Wallet ")
785+
786+
assertTrue(result.isSuccess)
787+
verify(hwWalletStore).saveKnownDevices(listOf(device.copy(customLabel = "My Cold Wallet")))
788+
}
789+
790+
@Test
791+
fun `setDeviceLabel clears the custom label when blank`() = test {
792+
val labelled = device.copy(customLabel = "Old")
793+
whenever(hwWalletStore.loadKnownDevices()).thenReturn(listOf(labelled))
794+
val sut = createRepo()
795+
796+
sut.setDeviceLabel("dev1", " ")
797+
798+
verify(hwWalletStore).saveKnownDevices(listOf(labelled.copy(customLabel = null)))
799+
}
800+
801+
@Test
802+
fun `setDeviceLabel applies to every entry sharing the wallet identity`() = test {
803+
val sharedXpubs = mapOf("nativeSegwit" to "zpubShared")
804+
val ble = device.copy(id = "ble1", xpubs = sharedXpubs)
805+
val usb = device.copy(id = "usb1", transportType = TransportType.USB, xpubs = sharedXpubs)
806+
whenever(hwWalletStore.loadKnownDevices()).thenReturn(listOf(ble, usb))
807+
val sut = createRepo()
808+
809+
sut.setDeviceLabel("usb1", "Shared")
810+
811+
verify(hwWalletStore).saveKnownDevices(
812+
listOf(ble.copy(customLabel = "Shared"), usb.copy(customLabel = "Shared")),
813+
)
814+
}
815+
816+
@Test
817+
fun `wallet name prefers the custom label over the device label`() = test {
818+
storeData.value = HwWalletData(knownDevices = listOf(device.copy(customLabel = "My Cold Wallet")))
819+
val sut = createRepo()
820+
821+
assertEquals("My Cold Wallet", sut.wallets.value.single().name)
822+
}
823+
755824
private suspend fun wheneverStartWatcher() = whenever(
756825
trezorRepo.startWatcher(
757826
any(),
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package to.bitkit.ui.sheets.hardware
2+
3+
import app.cash.turbine.test
4+
import com.synonym.bitkitcore.TrezorDeviceInfo
5+
import com.synonym.bitkitcore.TrezorFeatures
6+
import com.synonym.bitkitcore.TrezorTransportType
7+
import kotlinx.collections.immutable.ImmutableList
8+
import kotlinx.collections.immutable.persistentListOf
9+
import kotlinx.collections.immutable.persistentSetOf
10+
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.flow.MutableStateFlow
12+
import org.junit.Before
13+
import org.junit.Test
14+
import org.mockito.kotlin.mock
15+
import org.mockito.kotlin.verify
16+
import org.mockito.kotlin.whenever
17+
import to.bitkit.models.HwWallet
18+
import to.bitkit.models.TransportType
19+
import to.bitkit.repositories.HwWalletRepo
20+
import to.bitkit.repositories.TrezorState
21+
import to.bitkit.test.BaseUnitTest
22+
import kotlin.test.assertEquals
23+
24+
@OptIn(ExperimentalCoroutinesApi::class)
25+
class HwConnectViewModelTest : BaseUnitTest() {
26+
27+
private val hwWalletRepo = mock<HwWalletRepo>()
28+
private val needsPairingCode = MutableStateFlow(false)
29+
private val wallets = MutableStateFlow<ImmutableList<HwWallet>>(persistentListOf())
30+
private val deviceState = MutableStateFlow(TrezorState())
31+
32+
private lateinit var sut: HwConnectViewModel
33+
34+
@Before
35+
fun setUp() {
36+
whenever(hwWalletRepo.needsPairingCode).thenReturn(needsPairingCode)
37+
whenever(hwWalletRepo.wallets).thenReturn(wallets)
38+
whenever(hwWalletRepo.deviceState).thenReturn(deviceState)
39+
sut = HwConnectViewModel(hwWalletRepo)
40+
}
41+
42+
@Test
43+
fun `onIntroContinue searches then advances to found with the first discovered device`() = test {
44+
deviceState.value = TrezorState(nearbyDevices = persistentListOf(deviceInfo("dev1", model = "Safe 3")))
45+
whenever(hwWalletRepo.scan()).thenReturn(Result.success(emptyList()))
46+
47+
sut.effects.test {
48+
sut.onIntroContinue()
49+
assertEquals(HwConnectEffect.NavigateToSearching, awaitItem())
50+
assertEquals(HwConnectEffect.NavigateToFound, awaitItem())
51+
cancelAndIgnoreRemainingEvents()
52+
}
53+
54+
verify(hwWalletRepo).scan()
55+
assertEquals("dev1", sut.uiState.value.foundDeviceId)
56+
assertEquals("Trezor Safe 3", sut.uiState.value.deviceModel)
57+
}
58+
59+
@Test
60+
fun `onConnectClick connects the found device and advances to paired`() = test {
61+
givenDeviceFound()
62+
val connectedFeatures = features(model = "Safe 3")
63+
whenever(hwWalletRepo.connect("dev1")).thenReturn(Result.success(connectedFeatures))
64+
65+
sut.effects.test {
66+
sut.onConnectClick()
67+
assertEquals(HwConnectEffect.NavigateToPaired, awaitItem())
68+
cancelAndIgnoreRemainingEvents()
69+
}
70+
71+
verify(hwWalletRepo).connect("dev1")
72+
assertEquals("dev1", sut.uiState.value.pairedDeviceId)
73+
assertEquals("Trezor Safe 3", sut.uiState.value.labelInput)
74+
}
75+
76+
@Test
77+
fun `pairing code request surfaces the inline pair code step`() = test {
78+
sut.effects.test {
79+
needsPairingCode.value = true
80+
assertEquals(HwConnectEffect.NavigateToPairCode, awaitItem())
81+
cancelAndIgnoreRemainingEvents()
82+
}
83+
}
84+
85+
@Test
86+
fun `connected wallet updates the balance shown on the paired step`() = test {
87+
givenDeviceFound()
88+
val connectedFeatures = features(model = "Safe 3")
89+
whenever(hwWalletRepo.connect("dev1")).thenReturn(Result.success(connectedFeatures))
90+
sut.onConnectClick()
91+
92+
wallets.value = persistentListOf(hwWallet("dev1", name = "Trezor Safe 3", balance = 10_562_411uL))
93+
94+
assertEquals(10_562_411uL, sut.uiState.value.balanceSats)
95+
assertEquals("Trezor Safe 3", sut.uiState.value.deviceName)
96+
}
97+
98+
@Test
99+
fun `onFinishClick persists the edited label and dismisses`() = test {
100+
givenDeviceFound()
101+
val connectedFeatures = features(model = "Safe 3")
102+
whenever(hwWalletRepo.connect("dev1")).thenReturn(Result.success(connectedFeatures))
103+
sut.onConnectClick()
104+
sut.onLabelChange("My Cold Wallet")
105+
whenever(hwWalletRepo.setDeviceLabel("dev1", "My Cold Wallet")).thenReturn(Result.success(Unit))
106+
107+
sut.effects.test {
108+
sut.onFinishClick()
109+
assertEquals(HwConnectEffect.Dismiss, awaitItem())
110+
cancelAndIgnoreRemainingEvents()
111+
}
112+
113+
verify(hwWalletRepo).setDeviceLabel("dev1", "My Cold Wallet")
114+
}
115+
116+
private suspend fun givenDeviceFound() {
117+
deviceState.value = TrezorState(nearbyDevices = persistentListOf(deviceInfo("dev1", model = "Safe 3")))
118+
whenever(hwWalletRepo.scan()).thenReturn(Result.success(emptyList()))
119+
sut.onIntroContinue()
120+
}
121+
122+
private fun deviceInfo(id: String, model: String?) = TrezorDeviceInfo(
123+
id = id,
124+
transportType = TrezorTransportType.BLUETOOTH,
125+
name = null,
126+
path = "ble:$id",
127+
label = null,
128+
model = model,
129+
isBootloader = false,
130+
)
131+
132+
private fun features(model: String?): TrezorFeatures {
133+
val features = mock<TrezorFeatures>()
134+
whenever(features.label).thenReturn(null)
135+
whenever(features.model).thenReturn(model)
136+
return features
137+
}
138+
139+
private fun hwWallet(id: String, name: String, balance: ULong) = HwWallet(
140+
id = id,
141+
name = name,
142+
model = null,
143+
transportType = TransportType.BLUETOOTH,
144+
isConnected = true,
145+
balanceSats = balance,
146+
activities = persistentListOf(),
147+
deviceIds = persistentSetOf(id),
148+
)
149+
}

0 commit comments

Comments
 (0)