Skip to content

Commit 465a109

Browse files
committed
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>
1 parent 4a0dac0 commit 465a109

14 files changed

Lines changed: 1414 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)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.flipcash.app.payments
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertIs
7+
import kotlin.test.assertTrue
8+
9+
class PurchaseMethodStateTest {
10+
11+
@Test
12+
fun defaultStateHasOnlyPhantomWallet() {
13+
val state = PurchaseMethodState()
14+
assertEquals(1, state.availableMethods.size)
15+
assertIs<PurchaseMethod.PhantomWallet>(state.availableMethods[0])
16+
}
17+
18+
@Test
19+
fun hasReservesIsFalseForZeroBalance() {
20+
val state = PurchaseMethodState()
21+
assertFalse(state.hasReserves)
22+
}
23+
24+
@Test
25+
fun coinbaseOnRampIncludedWhenAvailable() {
26+
val state = PurchaseMethodState(coinbaseOnRampAvailable = true)
27+
assertTrue(state.availableMethods.any { it is PurchaseMethod.CoinbaseOnRamp })
28+
}
29+
30+
@Test
31+
fun coinbaseOnRampExcludedWhenUnavailable() {
32+
val state = PurchaseMethodState(coinbaseOnRampAvailable = false)
33+
assertFalse(state.availableMethods.any { it is PurchaseMethod.CoinbaseOnRamp })
34+
}
35+
36+
@Test
37+
fun otherWalletIncludedWhenAllowed() {
38+
val state = PurchaseMethodState(canUseOtherWallets = true)
39+
assertTrue(state.availableMethods.any { it is PurchaseMethod.OtherWallet })
40+
}
41+
42+
@Test
43+
fun otherWalletExcludedWhenNotAllowed() {
44+
val state = PurchaseMethodState(canUseOtherWallets = false)
45+
assertFalse(state.availableMethods.any { it is PurchaseMethod.OtherWallet })
46+
}
47+
48+
@Test
49+
fun cashReservesExcludedWhenZeroBalance() {
50+
val state = PurchaseMethodState()
51+
assertFalse(state.availableMethods.any { it is PurchaseMethod.CashReserves })
52+
}
53+
54+
@Test
55+
fun phantomWalletAlwaysPresent() {
56+
val state = PurchaseMethodState(
57+
coinbaseOnRampAvailable = true,
58+
canUseOtherWallets = true,
59+
)
60+
assertTrue(state.availableMethods.any { it is PurchaseMethod.PhantomWallet })
61+
}
62+
63+
@Test
64+
fun orderingCoinbaseBeforePhantomBeforeOther() {
65+
val state = PurchaseMethodState(
66+
coinbaseOnRampAvailable = true,
67+
canUseOtherWallets = true,
68+
)
69+
val methods = state.availableMethods
70+
val coinbaseIdx = methods.indexOfFirst { it is PurchaseMethod.CoinbaseOnRamp }
71+
val phantomIdx = methods.indexOfFirst { it is PurchaseMethod.PhantomWallet }
72+
val otherIdx = methods.indexOfFirst { it is PurchaseMethod.OtherWallet }
73+
assertTrue(coinbaseIdx < phantomIdx)
74+
assertTrue(phantomIdx < otherIdx)
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.flipcash.app.persistence.sources.mapper.notifications
2+
3+
import com.flipcash.app.core.feed.MessageState
4+
import com.flipcash.app.persistence.entities.MessageEntity
5+
import com.getcode.opencode.model.financial.CurrencyCode
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
import kotlin.test.assertNotNull
9+
import kotlin.test.assertNull
10+
import kotlin.time.Instant
11+
12+
class MessageEntityToFeedMessageMapperTest {
13+
14+
private val mapper = MessageEntityToFeedMessageMapper()
15+
16+
// Hardcoded mint addresses to avoid Mint companion object (uses Base58 + android deps)
17+
private val USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
18+
private val USDF_MINT = "5AMAA9JV9H97YYVxx8F6FsCMmTwXSuTTQneiup4RYAUQ"
19+
20+
private fun entity(
21+
amountUsdc: Long? = 100_000L,
22+
amountNative: Long? = 200_000L,
23+
nativeCurrency: String? = "cad",
24+
rate: Double? = 1.35,
25+
mintBase58: String? = USDC_MINT,
26+
state: String = "COMPLETED",
27+
metadata: String? = null,
28+
timestamp: Long = 1_700_000_000_000L,
29+
) = MessageEntity(
30+
idBase58 = "11111111111111111111111111111111",
31+
text = "test message",
32+
amountUsdc = amountUsdc,
33+
amountNative = amountNative,
34+
nativeCurrency = nativeCurrency,
35+
rate = rate,
36+
state = state,
37+
timestamp = timestamp,
38+
metadata = metadata,
39+
mintBase58 = mintBase58,
40+
)
41+
42+
// --- Amount resolution ---
43+
44+
@Test
45+
fun amountIsNonNullWhenAllFieldsPresent() {
46+
val result = mapper.map(entity())
47+
assertNotNull(result.amount)
48+
assertEquals(CurrencyCode.USD, result.amount!!.underlyingTokenAmount.currencyCode)
49+
assertEquals(CurrencyCode.CAD, result.amount!!.nativeAmount.currencyCode)
50+
}
51+
52+
@Test
53+
fun amountIsNullWhenAmountUsdcMissing() {
54+
val result = mapper.map(entity(amountUsdc = null))
55+
assertNull(result.amount)
56+
}
57+
58+
@Test
59+
fun amountIsNullWhenAmountNativeMissing() {
60+
val result = mapper.map(entity(amountNative = null))
61+
assertNull(result.amount)
62+
}
63+
64+
@Test
65+
fun amountIsNullWhenCurrencyMissing() {
66+
val result = mapper.map(entity(nativeCurrency = null))
67+
assertNull(result.amount)
68+
}
69+
70+
@Test
71+
fun amountIsNullWhenRateMissing() {
72+
val result = mapper.map(entity(rate = null))
73+
assertNull(result.amount)
74+
}
75+
76+
@Test
77+
fun amountIsNullWhenMintMissing() {
78+
val result = mapper.map(entity(mintBase58 = null))
79+
assertNull(result.amount)
80+
}
81+
82+
@Test
83+
fun amountIsNullWhenInvalidCurrency() {
84+
val result = mapper.map(entity(nativeCurrency = "zzz_invalid"))
85+
assertNull(result.amount)
86+
}
87+
88+
// --- USDF mint path ---
89+
90+
@Test
91+
fun usdfMintUsesUsdfLocalFiatConstructor() {
92+
val result = mapper.map(entity(mintBase58 = USDF_MINT))
93+
assertNotNull(result.amount)
94+
assertEquals(CurrencyCode.USD, result.amount!!.underlyingTokenAmount.currencyCode)
95+
}
96+
97+
// --- Timestamp mapping ---
98+
99+
@Test
100+
fun timestampMappedCorrectly() {
101+
val ts = 1_700_000_000_000L
102+
val result = mapper.map(entity(timestamp = ts))
103+
assertEquals(Instant.fromEpochMilliseconds(ts), result.timestamp)
104+
}
105+
106+
// --- State mapping ---
107+
108+
@Test
109+
fun stateMapsCompleted() {
110+
val result = mapper.map(entity(state = "COMPLETED"))
111+
assertEquals(MessageState.COMPLETED, result.state)
112+
}
113+
114+
@Test
115+
fun stateMapsUnknownForInvalidValue() {
116+
val result = mapper.map(entity(state = "not_a_real_state"))
117+
assertEquals(MessageState.UNKNOWN, result.state)
118+
}
119+
120+
// --- Metadata mapping ---
121+
122+
@Test
123+
fun metadataNullWhenEntityMetadataNull() {
124+
val result = mapper.map(entity(metadata = null))
125+
assertNull(result.metadata)
126+
}
127+
128+
// --- Text passthrough ---
129+
130+
@Test
131+
fun textPassedThrough() {
132+
val result = mapper.map(entity())
133+
assertEquals("test message", result.text)
134+
}
135+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.getcode.solana.instructions.programs
2+
3+
import com.getcode.solana.keys.PublicKey
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertTrue
7+
8+
class AssociatedTokenProgramTest {
9+
10+
private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList())
11+
12+
@Test
13+
fun createAccountRoundtrip() {
14+
val original = AssociatedTokenProgram_CreateAccount(
15+
subsidizer = key(1), owner = key(2),
16+
associatedTokenAccount = key(3), mint = key(4),
17+
)
18+
val decoded = AssociatedTokenProgram_CreateAccount.newInstance(original.instruction())
19+
assertEquals(original.subsidizer, decoded.subsidizer)
20+
assertEquals(original.mint, decoded.mint)
21+
}
22+
23+
@Test
24+
fun createAccountEncodeIsEmpty() {
25+
val inst = AssociatedTokenProgram_CreateAccount(key(1), key(2), key(3), key(4))
26+
assertTrue(inst.encode().isEmpty())
27+
}
28+
29+
@Test
30+
fun createAccountInstructionHasSevenAccounts() {
31+
val inst = AssociatedTokenProgram_CreateAccount(key(1), key(2), key(3), key(4))
32+
assertEquals(7, inst.instruction().accounts.size)
33+
}
34+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.getcode.solana.instructions.programs
2+
3+
import com.getcode.solana.keys.AccountMeta
4+
import com.getcode.solana.keys.PublicKey
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
import kotlin.test.assertTrue
8+
9+
class SwapValidatorProgramTest {
10+
11+
private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList())
12+
13+
// --- PreSwap ---
14+
15+
@Test
16+
fun preSwapRoundtrip() {
17+
val remaining = listOf(AccountMeta.readonly(publicKey = key(10)))
18+
val original = SwapValidatorProgram_PreSwap(
19+
preSwapState = key(1), user = key(2), source = key(3), destination = key(4),
20+
nonce = key(5), payer = key(6), remainingAccounts = remaining,
21+
)
22+
val decoded = SwapValidatorProgram_PreSwap.newInstance(original.instruction())
23+
assertEquals(original.preSwapState, decoded.preSwapState)
24+
assertEquals(original.user, decoded.user)
25+
assertEquals(original.source, decoded.source)
26+
assertEquals(original.destination, decoded.destination)
27+
assertEquals(original.nonce, decoded.nonce)
28+
assertEquals(original.payer, decoded.payer)
29+
}
30+
31+
@Test
32+
fun preSwapNoRemainingAccounts() {
33+
val original = SwapValidatorProgram_PreSwap(
34+
preSwapState = key(1), user = key(2), source = key(3), destination = key(4),
35+
nonce = key(5), payer = key(6), remainingAccounts = emptyList(),
36+
)
37+
val decoded = SwapValidatorProgram_PreSwap.newInstance(original.instruction())
38+
assertEquals(original.preSwapState, decoded.preSwapState)
39+
}
40+
41+
@Test
42+
fun preSwapEncodeLength() {
43+
val inst = SwapValidatorProgram_PreSwap(
44+
key(1), key(2), key(3), key(4), key(5), key(6), emptyList(),
45+
)
46+
// 8 bytes command only (no data fields)
47+
assertEquals(8, inst.encode().size)
48+
}
49+
50+
// --- PostSwap ---
51+
52+
@Test
53+
fun postSwapRoundtrip() {
54+
val original = SwapValidatorProgram_PostSwap(
55+
stateBump = 7.toByte(), maxToSend = 5_000_000L, minToReceive = 4_800_000L,
56+
preSwapState = key(1), source = key(2), destination = key(3), payer = key(4),
57+
)
58+
val decoded = SwapValidatorProgram_PostSwap.newInstance(original.instruction())
59+
assertEquals(7.toByte(), decoded.stateBump)
60+
assertEquals(5_000_000L, decoded.maxToSend)
61+
assertEquals(4_800_000L, decoded.minToReceive)
62+
assertEquals(original.preSwapState, decoded.preSwapState)
63+
}
64+
65+
@Test
66+
fun postSwapZeroValues() {
67+
val original = SwapValidatorProgram_PostSwap(
68+
stateBump = 0, maxToSend = 0L, minToReceive = 0L,
69+
preSwapState = key(1), source = key(2), destination = key(3), payer = key(4),
70+
)
71+
val decoded = SwapValidatorProgram_PostSwap.newInstance(original.instruction())
72+
assertEquals(0.toByte(), decoded.stateBump)
73+
assertEquals(0L, decoded.maxToSend)
74+
assertEquals(0L, decoded.minToReceive)
75+
}
76+
77+
@Test
78+
fun postSwapEncodeLength() {
79+
val inst = SwapValidatorProgram_PostSwap(
80+
stateBump = 0, maxToSend = 1L, minToReceive = 1L,
81+
preSwapState = key(1), source = key(2), destination = key(3), payer = key(4),
82+
)
83+
// 8 bytes command + 1 byte bump + 8 bytes maxToSend + 8 bytes minToReceive
84+
assertEquals(25, inst.encode().size)
85+
}
86+
87+
// --- Command enum ---
88+
89+
@Test
90+
fun commandValuesAreDistinct() {
91+
val commands = SwapValidatorProgram.Command.entries
92+
assertEquals(commands.size, commands.map { it.value }.toSet().size)
93+
}
94+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.getcode.solana.instructions.programs
2+
3+
import com.getcode.solana.keys.PublicKey
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
7+
class SystemProgramTest {
8+
9+
private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList())
10+
11+
@Test
12+
fun advanceNonceRoundtrip() {
13+
val original = SystemProgram_AdvanceNonce(nonce = key(1), authority = key(2))
14+
val decoded = SystemProgram_AdvanceNonce.newInstance(original.instruction())
15+
assertEquals(original.nonce, decoded.nonce)
16+
assertEquals(original.authority, decoded.authority)
17+
}
18+
19+
@Test
20+
fun advanceNonceEncodeLength() {
21+
val inst = SystemProgram_AdvanceNonce(key(1), key(2))
22+
// 4 bytes Int command
23+
assertEquals(4, inst.encode().size)
24+
}
25+
26+
@Test
27+
fun advanceNonceCommandByteValue() {
28+
val inst = SystemProgram_AdvanceNonce(key(1), key(2))
29+
val encoded = inst.encode()
30+
// advanceNonceAccount ordinal is 4 → LE bytes [4, 0, 0, 0]
31+
assertEquals(4, encoded[0].toInt())
32+
assertEquals(0, encoded[1].toInt())
33+
assertEquals(0, encoded[2].toInt())
34+
assertEquals(0, encoded[3].toInt())
35+
}
36+
}

0 commit comments

Comments
 (0)