Skip to content

Commit 8728742

Browse files
committed
test(deposit): add unit tests for USDC deposit sweep
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 8704057 commit 8728742

3 files changed

Lines changed: 525 additions & 0 deletions

File tree

apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/SessionControllerGiftCardErrorTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class SessionControllerGiftCardErrorTest {
102102
featureFlagController = mockk(relaxed = true),
103103
analytics = analytics,
104104
appSettingsCoordinator = mockk(relaxed = true),
105+
usdcSweep = mockk(relaxed = true),
105106
)
106107
}
107108

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package com.flipcash.app.tokens
2+
3+
import com.getcode.opencode.controllers.AccountController
4+
import com.getcode.opencode.controllers.TransactionOperations
5+
import com.getcode.opencode.model.accounts.AccountCluster
6+
import com.getcode.opencode.model.accounts.AccountInfo
7+
import com.getcode.opencode.model.accounts.AccountType
8+
import com.getcode.solana.keys.PublicKey
9+
import io.mockk.coEvery
10+
import io.mockk.coVerify
11+
import io.mockk.every
12+
import io.mockk.mockk
13+
import kotlinx.coroutines.ExperimentalCoroutinesApi
14+
import kotlinx.coroutines.test.advanceUntilIdle
15+
import kotlinx.coroutines.test.runTest
16+
import org.junit.After
17+
import org.junit.Before
18+
import org.junit.Test
19+
20+
@OptIn(ExperimentalCoroutinesApi::class)
21+
class UsdcDepositSweepTest {
22+
23+
private val transactionOperations: TransactionOperations = mockk(relaxed = true)
24+
private val accountController: AccountController = mockk(relaxed = true)
25+
private val tokenCoordinator: TokenCoordinator = mockk(relaxed = true)
26+
27+
private val owner: AccountCluster = mockk(relaxed = true)
28+
29+
private lateinit var sweep: UsdcDepositSweep
30+
31+
@Before
32+
fun setUp() {
33+
sweep = UsdcDepositSweep(
34+
transactionOperations = transactionOperations,
35+
accountController = accountController,
36+
tokenCoordinator = tokenCoordinator,
37+
)
38+
}
39+
40+
@After
41+
fun tearDown() {
42+
sweep.cancel()
43+
}
44+
45+
private fun stubUsdcAccount(balance: Long, type: AccountType = AccountType.AssociatedToken) {
46+
val accountInfo = mockk<AccountInfo> {
47+
every { accountType } returns type
48+
every { this@mockk.balance } returns balance
49+
every { address } returns mockk<PublicKey>(relaxed = true)
50+
}
51+
coEvery {
52+
accountController.getAccount(any(), any(), any())
53+
} returns Result.success(accountInfo)
54+
}
55+
56+
private fun stubNoUsdcAccount() {
57+
coEvery {
58+
accountController.getAccount(any(), any(), any())
59+
} returns Result.failure(RuntimeException("not found"))
60+
}
61+
62+
@Test
63+
fun `skips swap when USDC account is not found`() = runTest {
64+
stubNoUsdcAccount()
65+
66+
sweep.execute(owner)
67+
advanceUntilIdle()
68+
69+
// Give the internal scope time to complete
70+
Thread.sleep(100)
71+
72+
coVerify(exactly = 0) {
73+
transactionOperations.swapUsdc(any(), any())
74+
}
75+
}
76+
77+
@Test
78+
fun `skips swap when USDC account type is not AssociatedToken`() = runTest {
79+
stubUsdcAccount(balance = 1_000_000L, type = AccountType.Primary)
80+
81+
sweep.execute(owner)
82+
advanceUntilIdle()
83+
Thread.sleep(100)
84+
85+
coVerify(exactly = 0) {
86+
transactionOperations.swapUsdc(any(), any())
87+
}
88+
}
89+
90+
@Test
91+
fun `skips swap when USDC balance is zero`() = runTest {
92+
stubUsdcAccount(balance = 0L)
93+
94+
sweep.execute(owner)
95+
advanceUntilIdle()
96+
Thread.sleep(100)
97+
98+
coVerify(exactly = 0) {
99+
transactionOperations.swapUsdc(any(), any())
100+
}
101+
}
102+
103+
@Test
104+
fun `skips swap when USDC balance is negative`() = runTest {
105+
stubUsdcAccount(balance = -1L)
106+
107+
sweep.execute(owner)
108+
advanceUntilIdle()
109+
Thread.sleep(100)
110+
111+
coVerify(exactly = 0) {
112+
transactionOperations.swapUsdc(any(), any())
113+
}
114+
}
115+
116+
@Test
117+
fun `calls swapUsdc with correct amount when balance is positive`() = runTest {
118+
val amount = 5_000_000L
119+
stubUsdcAccount(balance = amount)
120+
coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit)
121+
122+
sweep.execute(owner)
123+
advanceUntilIdle()
124+
Thread.sleep(200)
125+
126+
coVerify {
127+
transactionOperations.swapUsdc(owner, amount)
128+
}
129+
}
130+
131+
@Test
132+
fun `calls tokenCoordinator update on successful swap`() = runTest {
133+
stubUsdcAccount(balance = 1_000_000L)
134+
coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit)
135+
136+
sweep.execute(owner)
137+
advanceUntilIdle()
138+
Thread.sleep(200)
139+
140+
coVerify {
141+
tokenCoordinator.update()
142+
}
143+
}
144+
145+
@Test
146+
fun `does not call tokenCoordinator update on failed swap`() = runTest {
147+
stubUsdcAccount(balance = 1_000_000L)
148+
coEvery {
149+
transactionOperations.swapUsdc(any(), any())
150+
} returns Result.failure(RuntimeException("swap failed"))
151+
152+
sweep.execute(owner)
153+
advanceUntilIdle()
154+
Thread.sleep(200)
155+
156+
coVerify(exactly = 0) {
157+
tokenCoordinator.update()
158+
}
159+
}
160+
161+
@Test
162+
fun `does not execute concurrently when job is active`() = runTest {
163+
stubUsdcAccount(balance = 1_000_000L)
164+
coEvery { transactionOperations.swapUsdc(any(), any()) } coAnswers {
165+
kotlinx.coroutines.delay(500)
166+
Result.success(Unit)
167+
}
168+
169+
// First call starts the job
170+
sweep.execute(owner)
171+
// Second call should be ignored since the first is still active
172+
sweep.execute(owner)
173+
174+
Thread.sleep(700)
175+
176+
coVerify(exactly = 1) {
177+
transactionOperations.swapUsdc(any(), any())
178+
}
179+
}
180+
181+
@Test
182+
fun `cancel stops active job`() = runTest {
183+
// Make getAccount slow so we can cancel before swapUsdc is reached
184+
coEvery {
185+
accountController.getAccount(any(), any(), any())
186+
} coAnswers {
187+
kotlinx.coroutines.delay(5000)
188+
val accountInfo = mockk<AccountInfo> {
189+
every { accountType } returns AccountType.AssociatedToken
190+
every { balance } returns 1_000_000L
191+
every { address } returns mockk<PublicKey>(relaxed = true)
192+
}
193+
Result.success(accountInfo)
194+
}
195+
coEvery { transactionOperations.swapUsdc(any(), any()) } returns Result.success(Unit)
196+
197+
sweep.execute(owner)
198+
Thread.sleep(50) // Let the coroutine start
199+
sweep.cancel()
200+
Thread.sleep(100)
201+
202+
coVerify(exactly = 0) {
203+
transactionOperations.swapUsdc(any(), any())
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)