11package cash.p.terminal.core.adapters.zcash
22
3- import android.content.Context
43import android.database.sqlite.SQLiteDatabaseCorruptException
5- import cash.p.terminal.core.ILocalStorage
64import cash.p.terminal.core.TestDispatcherProvider
7- import cash.p.terminal.core.managers.BackgroundKeepAliveManager
85import cash.p.terminal.core.managers.RestoreSettings
9- import cash.p.terminal.domain.usecase.ClearZCashWalletDataUseCase
10- import cash.p.terminal.wallet.Account
11- import cash.p.terminal.wallet.AccountOrigin
12- import cash.p.terminal.wallet.AccountType
136import cash.p.terminal.wallet.AdapterState
147import cash.p.terminal.wallet.Wallet
158import cash.z.ecc.android.sdk.SdkSynchronizer
169import cash.z.ecc.android.sdk.Synchronizer
1710import cash.z.ecc.android.sdk.WalletInitMode
1811import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
1912import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
20- import cash.z.ecc.android.sdk.model.AccountBalance
21- import cash.z.ecc.android.sdk.model.AccountUuid
2213import cash.z.ecc.android.sdk.model.BlockHeight
2314import cash.z.ecc.android.sdk.model.PercentDecimal
2415import cash.z.ecc.android.sdk.model.TransactionOverview
2516import cash.z.ecc.android.sdk.model.ZcashNetwork
2617import io.horizontalsystems.core.BackgroundManager
2718import io.horizontalsystems.core.BackgroundManagerState
28- import io.horizontalsystems.core.CoreApp
2919import io.horizontalsystems.core.entities.BlockchainType
3020import io.mockk.coEvery
3121import io.mockk.coVerify
3222import io.mockk.every
3323import io.mockk.mockk
34- import io.mockk.mockkObject
3524import io.mockk.slot
36- import io.mockk.unmockkAll
3725import io.mockk.verify
3826import junit.framework.TestCase.assertEquals
3927import junit.framework.TestCase.assertFalse
4028import junit.framework.TestCase.assertTrue
4129import kotlinx.coroutines.CancellationException
4230import kotlinx.coroutines.CompletableDeferred
43- import kotlinx.coroutines.CoroutineScope
44- import kotlinx.coroutines.Dispatchers
4531import kotlinx.coroutines.ExperimentalCoroutinesApi
46- import kotlinx.coroutines.SupervisorJob
4732import kotlinx.coroutines.cancel
4833import kotlinx.coroutines.flow.MutableStateFlow
4934import kotlinx.coroutines.flow.flow
5035import kotlinx.coroutines.suspendCancellableCoroutine
51- import kotlinx.coroutines.test.StandardTestDispatcher
5236import kotlinx.coroutines.test.advanceTimeBy
5337import kotlinx.coroutines.test.advanceUntilIdle
54- import kotlinx.coroutines.test.resetMain
5538import kotlinx.coroutines.test.runCurrent
5639import kotlinx.coroutines.test.runTest
57- import kotlinx.coroutines.test.setMain
58- import org.junit.After
59- import org.junit.Before
6040import org.junit.Test
61- import org.koin.core.context.startKoin
62- import org.koin.core.context.stopKoin
63- import org.koin.dsl.module
6441
6542/* *
6643 * Tests for ZcashAdapter database corruption detection and recovery.
6744 *
68- * All adapter coroutines (recovery, status/start/restart jobs, the subscriber scope) run on
69- * [dispatcher], a single [StandardTestDispatcher] shared with `runTest`, so every wait below is
70- * driven deterministically via the virtual-time scheduler instead of real timeouts.
45+ * The shared harness lives in [ZcashAdapterTestFixture]: all adapter coroutines (recovery,
46+ * status/start/restart jobs, the subscriber scope) run on its single `StandardTestDispatcher`
47+ * shared with `runTest`, so every wait below is driven deterministically via the virtual-time
48+ * scheduler instead of real timeouts.
7149 */
7250@OptIn(ExperimentalCoroutinesApi ::class )
73- class ZcashAdapterCorruptionRecoveryTest {
74-
75- private val dispatcher = StandardTestDispatcher ()
76-
77- // Separate from the `runTest` scope on purpose: the adapter's subscriber collectors are
78- // parented to synchronizer.coroutineScope (ZcashAdapter.subscribe()) and never complete on
79- // their own. If they were children of the `runTest` scope, `runTest` would hang waiting for
80- // them. They live in `appScope` instead, cancelled explicitly in tearDown().
81- private val appScope = CoroutineScope (SupervisorJob () + dispatcher)
82-
83- private val context = mockk<Context >(relaxed = true )
84- private val wallet = mockk<Wallet >(relaxed = true )
85- private val localStorage = mockk<ILocalStorage >(relaxed = true )
86- private val backgroundManager = mockk<BackgroundManager >(relaxed = true )
87- private val singleUseAddressManager = mockk<ZcashSingleUseAddressManager >(relaxed = true )
88- private val clearZCashWalletDataUseCase = mockk<ClearZCashWalletDataUseCase >(relaxed = true )
89- private val backgroundKeepAliveManager = mockk<BackgroundKeepAliveManager >(relaxed = true )
90- private val restoreSettings = RestoreSettings ().apply { birthdayHeight = 2000000L }
91-
92- private lateinit var mockSynchronizer: SdkSynchronizer
93-
94- private val statusFlow = MutableStateFlow (Synchronizer .Status .SYNCING )
95- private val progressFlow = MutableStateFlow (PercentDecimal .ZERO_PERCENT )
96- private val walletBalancesFlow = MutableStateFlow <Map <AccountUuid , AccountBalance >? > (null )
97- private val processorInfoFlow = MutableStateFlow (
98- CompactBlockProcessor .ProcessorInfo (null , null , null )
99- )
100- private val allTransactionsFlow = MutableStateFlow <List <TransactionOverview >>(emptyList())
51+ class ZcashAdapterCorruptionRecoveryTest : ZcashAdapterTestFixture () {
10152
10253 private var capturedProcessorErrorHandler: ((Throwable ? ) -> Boolean )? = null
10354 private var capturedCriticalErrorHandler: ((Throwable ? ) -> Boolean )? = null
10455
105- private lateinit var adapter: ZcashAdapter
106-
107- @Before
108- fun setUp () {
109- Dispatchers .setMain(dispatcher)
110- CoreApp .instance = mockk(relaxed = true )
111-
112- startKoin {
113- modules(module {
114- single { clearZCashWalletDataUseCase }
115- single { backgroundKeepAliveManager }
116- })
117- }
118-
119- val testSeed = ByteArray (64 ) { it.toByte() }
120- val accountType = mockk<AccountType .Mnemonic >(relaxed = true ) {
121- every { seed } returns testSeed
122- }
123- val account = mockk<Account >(relaxed = true ) {
124- every { id } returns " test-account-id"
125- every { name } returns " Test"
126- every { type } returns accountType
127- every { origin } returns AccountOrigin .Created
128- }
129- every { wallet.account } returns account
130- every { localStorage.zcashAccountIds } returns setOf (" test-account-id" )
131- every { localStorage.torEnabled } returns false
132- every { backgroundManager.stateFlow } returns MutableStateFlow (BackgroundManagerState .Unknown )
133- every { clearZCashWalletDataUseCase.getValidAliasFromAccountId(any(), any()) } returns " zcash_test"
134-
135- mockkObject(BlockHeight .Companion )
136- coEvery { BlockHeight .ofLatestCheckpoint(any(), any()) } returns BlockHeight .new(2500000L )
137-
138- setupMockSynchronizer()
139- mockSynchronizerCompanion()
140- }
141-
142- private fun setupMockSynchronizer () {
143- mockSynchronizer = mockk<SdkSynchronizer >(relaxed = true ) {
144- every { status } returns statusFlow
145- every { progress } returns progressFlow
146- every { walletBalances } returns walletBalancesFlow
147- every { processorInfo } returns processorInfoFlow
148- every { allTransactions } returns allTransactionsFlow
149- every { coroutineScope } returns appScope
150- every { latestHeight } returns null
151- }
152-
56+ override fun stubSynchronizer () {
15357 val processorSlot = slot< (Throwable ? ) -> Boolean > ()
15458 every { mockSynchronizer.onProcessorErrorHandler = capture(processorSlot) } answers {
15559 capturedProcessorErrorHandler = processorSlot.captured
@@ -160,17 +64,8 @@ class ZcashAdapterCorruptionRecoveryTest {
16064 }
16165 }
16266
163- private fun mockSynchronizerCompanion () {
164- mockkObject(Synchronizer )
67+ override fun stubSynchronizerCompanion () {
16568 coEvery { Synchronizer .erase(any(), any(), any()) } returns true
166- every {
167- Synchronizer .newBlocking(
168- context = any(), zcashNetwork = any(), alias = any(),
169- lightWalletEndpoint = any(), birthday = any(), walletInitMode = any(),
170- setup = any(), isTorEnabled = any(), isExchangeRateEnabled = any()
171- )
172- } returns mockSynchronizer
173-
17469 coEvery {
17570 Synchronizer .new(
17671 context = any(), zcashNetwork = any(), alias = any(),
@@ -180,19 +75,6 @@ class ZcashAdapterCorruptionRecoveryTest {
18075 } returns mockSynchronizer
18176 }
18277
183- private fun createAdapter (): ZcashAdapter {
184- return ZcashAdapter (
185- context = context,
186- wallet = wallet,
187- restoreSettings = restoreSettings,
188- addressSpecTyped = null ,
189- localStorage = localStorage,
190- backgroundManager = backgroundManager,
191- singleUseAddressManager = singleUseAddressManager,
192- dispatcherProvider = TestDispatcherProvider (dispatcher, appScope),
193- )
194- }
195-
19678 private fun createAdapter (baseDelayMs : Long , maxDelayMs : Long ): ZcashAdapter {
19779 return ZcashAdapter (
19880 context, wallet, restoreSettings, null , localStorage, backgroundManager,
@@ -201,17 +83,6 @@ class ZcashAdapterCorruptionRecoveryTest {
20183 )
20284 }
20385
204- @After
205- fun tearDown () {
206- if (::adapter.isInitialized) {
207- adapter.stop()
208- }
209- appScope.cancel()
210- stopKoin()
211- Dispatchers .resetMain()
212- unmockkAll()
213- }
214-
21586 // --- onProcessorErrorHandler ---
21687
21788 @Test
0 commit comments