Skip to content

Commit 0b4f7a7

Browse files
authored
test: add unit tests to high-risk untested modules (#765)
* test: add unit tests to high-risk untested modules Add ~134 unit tests across encryption, Solana instructions, network utilities, payments, onramp, and persistence modules. Also fix main branch reference in CLAUDE.md (main → code/cash). Signed-off-by: Brandon McAnsh <git@bmcreations.dev> * test: add ViewModel state and services mapper tests Add 56 tests across 6 new test files: - WithdrawalViewModel, DepositViewModel, BalanceViewModel, MyAccountScreenViewModel pure state reducer tests - ActivityFeedMessageMapper and UserProfileMapper protobuf mapping tests * test: add CashScreen, Swap, and OnRamp ViewModel state tests Add 56 tests across 3 new test files for high-use flows: - CashScreenViewModelStateTest (13 tests): state reducers, computed props (canGive, maxAvailableForGive, isError) - SwapViewModelStateTest (22 tests): state reducers, computed props (canTransact, sellFee, netTransferAmount, transactionLimit, isError), all progress states - OnRampViewModelStateTest (21 tests): state reducers, provider selection logic (canChangeCurrency), AmountEntryState computed props, minimumPurchaseAmount * test: expand CashScreen, Swap, and OnRamp ViewModel state coverage Add 18 tests covering currency change reducers, OnSwapIdChanged, Solflare/Backpack provider selection, loading state reset roundtrips, and purpose-specific computed properties (netTransferAmount, feeAmount, transactionLimit, maxAvailableToSwap). Signed-off-by: Brandon McAnsh <git@bmcreations.dev> --------- Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 709e485 commit 0b4f7a7

25 files changed

Lines changed: 2580 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,5 @@ The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:comp
9898
## Git Conventions
9999

100100
- Conventional commits: `feat:`, `fix:`, `chore:`, with optional scope in parens (e.g., `feat(oc):`, `fix(tokens):`)
101-
- Main branch: `main`
101+
- Main branch: `code/cash`
102102
- CI runs on all PRs (tests via Fastlane)

apps/flipcash/features/balance/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ android {
77
}
88

99
dependencies {
10+
testImplementation(kotlin("test"))
11+
1012
implementation(libs.compose.paging)
1113

1214
implementation(project(":apps:flipcash:shared:analytics"))
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.flipcash.app.balance.internal
2+
3+
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertNull
7+
8+
class BalanceViewModelStateTest {
9+
10+
private val reduce = BalanceViewModel.Companion.updateStateForEvent
11+
12+
@Test
13+
fun `default state has null provider`() {
14+
assertNull(BalanceViewModel.State().preferredOnRampProvider)
15+
}
16+
17+
@Test
18+
fun `OnPreferredOnRampProviderChanged updates provider`() {
19+
val provider = OnRampProvider.ManualDeposit
20+
val updated = reduce(
21+
BalanceViewModel.Event.OnPreferredOnRampProviderChanged(provider)
22+
)(BalanceViewModel.State())
23+
assertEquals(provider, updated.preferredOnRampProvider)
24+
}
25+
26+
@Test
27+
fun `OnPreferredOnRampProviderChanged with null clears provider`() {
28+
val state = BalanceViewModel.State(
29+
preferredOnRampProvider = OnRampProvider.ManualDeposit
30+
)
31+
val updated = reduce(
32+
BalanceViewModel.Event.OnPreferredOnRampProviderChanged(null)
33+
)(state)
34+
assertNull(updated.preferredOnRampProvider)
35+
}
36+
37+
@Test
38+
fun `OpenCurrencySelection is no-op`() {
39+
val state = BalanceViewModel.State(
40+
preferredOnRampProvider = OnRampProvider.ManualDeposit
41+
)
42+
val updated = reduce(BalanceViewModel.Event.OpenCurrencySelection)(state)
43+
assertEquals(state, updated)
44+
}
45+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.flipcash.app.cash.internal
2+
3+
import com.getcode.opencode.model.financial.Currency
4+
import com.getcode.opencode.model.financial.CurrencyCode
5+
import com.getcode.opencode.model.financial.Fiat
6+
import com.getcode.solana.keys.Mint
7+
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertFalse
11+
import kotlin.test.assertNotNull
12+
import kotlin.test.assertNull
13+
import kotlin.test.assertTrue
14+
15+
class CashScreenViewModelStateTest {
16+
17+
private val reduce = CashScreenViewModel.Companion.updateStateForEvent
18+
private fun mint() = Mint(ByteArray(32) { 1 }.toList())
19+
20+
// --- Default state ---
21+
22+
@Test
23+
fun `default state has null token and address`() {
24+
val state = CashScreenViewModel.State()
25+
assertNull(state.selectedTokenAddress)
26+
assertNull(state.token)
27+
assertNull(state.limits)
28+
assertNull(state.maxForGive)
29+
assertFalse(state.canGive)
30+
}
31+
32+
// --- State reducers ---
33+
34+
@Test
35+
fun `OnTokenSelected sets selectedTokenAddress`() {
36+
val mint = mint()
37+
val updated = reduce(CashScreenViewModel.Event.OnTokenSelected(mint))(CashScreenViewModel.State())
38+
assertEquals(mint, updated.selectedTokenAddress)
39+
}
40+
41+
@Test
42+
fun `OnAmountChanged updates amountAnimatedModel`() {
43+
val model = AmountAnimatedInputUiModel(lastPressedBackspace = true)
44+
val updated = reduce(CashScreenViewModel.Event.OnAmountChanged(model))(CashScreenViewModel.State())
45+
assertEquals(model, updated.amountAnimatedModel)
46+
}
47+
48+
@Test
49+
fun `UpdateLoadingState sets loading`() {
50+
val updated = reduce(
51+
CashScreenViewModel.Event.UpdateLoadingState(loading = true)
52+
)(CashScreenViewModel.State())
53+
assertTrue(updated.generatingBill.loading)
54+
assertFalse(updated.generatingBill.success)
55+
}
56+
57+
@Test
58+
fun `UpdateLoadingState sets success`() {
59+
val updated = reduce(
60+
CashScreenViewModel.Event.UpdateLoadingState(loading = false, success = true)
61+
)(CashScreenViewModel.State())
62+
assertTrue(updated.generatingBill.success)
63+
assertFalse(updated.generatingBill.loading)
64+
}
65+
66+
@Test
67+
fun `OnMaxDetermined sets maxForGive`() {
68+
val updated = reduce(
69+
CashScreenViewModel.Event.OnMaxDetermined(max = 50.0, currencyCode = CurrencyCode.USD)
70+
)(CashScreenViewModel.State())
71+
assertEquals(50.0 to CurrencyCode.USD, updated.maxForGive)
72+
}
73+
74+
@Test
75+
fun `OnLimitsChanged sets limits`() {
76+
val updated = reduce(
77+
CashScreenViewModel.Event.OnLimitsChanged(null)
78+
)(CashScreenViewModel.State())
79+
assertNull(updated.limits)
80+
}
81+
82+
@Test
83+
fun `OnCurrencyChanged sets currencyModel`() {
84+
val currency = Currency(code = "EUR", name = "Euro", symbol = "", rate = 0.92)
85+
val updated = reduce(
86+
CashScreenViewModel.Event.OnCurrencyChanged(currency)
87+
)(CashScreenViewModel.State())
88+
assertNotNull(updated.currencyModel.selected)
89+
assertEquals("EUR", updated.currencyModel.selected?.code)
90+
assertEquals(CurrencyCode.EUR, updated.currencyModel.code)
91+
}
92+
93+
@Test
94+
fun `OnCurrencyChanged preserves fractionUnits`() {
95+
val currency = Currency(code = "JPY", name = "Japanese Yen", symbol = "¥", fractionUnits = 0)
96+
val updated = reduce(
97+
CashScreenViewModel.Event.OnCurrencyChanged(currency)
98+
)(CashScreenViewModel.State())
99+
assertEquals(0, updated.currencyModel.fractionUnits)
100+
}
101+
102+
// --- No-op events ---
103+
104+
@Test
105+
fun `no-op events return state unchanged`() {
106+
val state = CashScreenViewModel.State(selectedTokenAddress = mint())
107+
val noOpEvents = listOf(
108+
CashScreenViewModel.Event.InitializeToken(null),
109+
CashScreenViewModel.Event.OnBackspace,
110+
CashScreenViewModel.Event.OnGive,
111+
CashScreenViewModel.Event.OnEnteredNumberChanged(),
112+
CashScreenViewModel.Event.OnNumberPressed(5),
113+
CashScreenViewModel.Event.OnDecimalPressed,
114+
CashScreenViewModel.Event.AddCashToWallet(Fiat.Zero),
115+
CashScreenViewModel.Event.OpenScreen(com.flipcash.app.core.AppRoute.Loading),
116+
)
117+
noOpEvents.forEach { event ->
118+
assertEquals(state, reduce(event)(state), "Event $event should be no-op")
119+
}
120+
}
121+
122+
// --- Computed: canGive ---
123+
124+
@Test
125+
fun `canGive is false when amount is zero`() {
126+
assertFalse(CashScreenViewModel.State().canGive)
127+
}
128+
129+
// --- Computed: maxAvailableForGive ---
130+
131+
@Test
132+
fun `maxAvailableForGive is empty when maxForGive is null`() {
133+
val state = CashScreenViewModel.State(maxForGive = null)
134+
assertEquals("", state.maxAvailableForGive)
135+
}
136+
137+
@Test
138+
fun `maxAvailableForGive is formatted when maxForGive is set`() {
139+
val state = CashScreenViewModel.State(maxForGive = 100.0 to CurrencyCode.USD)
140+
assertTrue(state.maxAvailableForGive.isNotEmpty())
141+
}
142+
143+
// --- Computed: isError ---
144+
145+
@Test
146+
fun `isError is true when maxForGive is null`() {
147+
// Default amountData has "0" text (not empty), and maxForGive is null → error
148+
assertTrue(CashScreenViewModel.State().isError)
149+
}
150+
151+
@Test
152+
fun `isError is false when amount within maxForGive`() {
153+
// Default amount 0.0 <= 100.0
154+
val state = CashScreenViewModel.State(maxForGive = 100.0 to CurrencyCode.USD)
155+
assertFalse(state.isError)
156+
}
157+
}

apps/flipcash/features/deposit/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ android {
77
}
88

99
dependencies {
10+
testImplementation(kotlin("test"))
11+
1012
implementation(project(":libs:messaging"))
1113

1214
implementation(project(":apps:flipcash:shared:featureflags"))
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.flipcash.app.deposit.internal
2+
3+
import com.getcode.solana.keys.PublicKey
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertFalse
7+
import kotlin.test.assertNull
8+
import kotlin.test.assertTrue
9+
10+
class DepositViewModelStateTest {
11+
12+
private val reduce = DepositViewModel.Companion.updateStateForEvent
13+
private fun mint() = com.getcode.solana.keys.Mint(ByteArray(32) { 1 }.toList())
14+
15+
@Test
16+
fun `default state has null address and token name`() {
17+
val state = DepositViewModel.State()
18+
assertNull(state.selectedTokenAddress)
19+
assertNull(state.tokenName)
20+
assertEquals("", state.depositAddress)
21+
assertFalse(state.isCopied)
22+
}
23+
24+
@Test
25+
fun `OnMintSelected sets selected token address`() {
26+
val mint = mint()
27+
val updated = reduce(DepositViewModel.Event.OnMintSelected(mint))(DepositViewModel.State())
28+
assertEquals(mint, updated.selectedTokenAddress)
29+
}
30+
31+
@Test
32+
fun `OnTokenChanged sets address and name`() {
33+
val updated = reduce(
34+
DepositViewModel.Event.OnTokenChanged(address = "abc123", name = "TestToken")
35+
)(DepositViewModel.State())
36+
assertEquals("abc123", updated.depositAddress)
37+
assertEquals("TestToken", updated.tokenName)
38+
}
39+
40+
@Test
41+
fun `SetCopied true then false roundtrip`() {
42+
val state = DepositViewModel.State()
43+
val copied = reduce(DepositViewModel.Event.SetCopied(true))(state)
44+
assertTrue(copied.isCopied)
45+
val uncopied = reduce(DepositViewModel.Event.SetCopied(false))(copied)
46+
assertFalse(uncopied.isCopied)
47+
}
48+
49+
@Test
50+
fun `CopyAddress is no-op`() {
51+
val state = DepositViewModel.State(depositAddress = "addr", tokenName = "T")
52+
val updated = reduce(DepositViewModel.Event.CopyAddress)(state)
53+
assertEquals(state, updated)
54+
}
55+
56+
@Test
57+
fun `Exit is no-op`() {
58+
val state = DepositViewModel.State(depositAddress = "addr")
59+
val updated = reduce(DepositViewModel.Event.Exit)(state)
60+
assertEquals(state, updated)
61+
}
62+
}

apps/flipcash/features/myaccount/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ android {
77
}
88

99
dependencies {
10+
testImplementation(kotlin("test"))
11+
1012
implementation(project(":apps:flipcash:shared:authentication"))
1113
implementation(project(":apps:flipcash:shared:featureflags"))
1214
implementation(project(":apps:flipcash:shared:menu"))

0 commit comments

Comments
 (0)