Skip to content

Commit cd98a71

Browse files
committed
test: add error mapping tests for Coinbase, SubmitIntent, and Swap
- CoinbaseOnRampEventHandler: 15 tests covering event routing (success, cancel, auto-click, all 4 error event types), unknown/invalid JSON - CoinbaseOnRampWebError: 3 tests for tryValueOf parsing - SubmitIntentError.typed(): 11 tests for proto→error mapping with reason string extraction from ErrorDetails - SwapError.typed(): 8 tests for proto→error mapping with deny/reason extraction
1 parent e07163f commit cd98a71

3 files changed

Lines changed: 451 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.flipcash.app.onramp.internal
2+
3+
import io.mockk.every
4+
import io.mockk.mockkStatic
5+
import io.mockk.unmockkStatic
6+
import org.junit.After
7+
import org.junit.Before
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
import org.robolectric.RobolectricTestRunner
11+
import org.robolectric.annotation.Config
12+
import kotlin.test.assertEquals
13+
import kotlin.test.assertTrue
14+
15+
@RunWith(RobolectricTestRunner::class)
16+
@Config(manifest = Config.NONE)
17+
class CoinbaseOnRampEventHandlerTest {
18+
19+
@Before
20+
fun setUp() {
21+
mockkStatic("com.getcode.utils.LoggingKt")
22+
every { com.getcode.utils.trace(any(), any(), any(), any(), any()) } returns Unit
23+
}
24+
25+
@After
26+
fun tearDown() {
27+
unmockkStatic("com.getcode.utils.LoggingKt")
28+
}
29+
30+
private var successCount = 0
31+
private var cancelCount = 0
32+
private var autoClickCount = 0
33+
private var lastError: CoinbaseOnRampWebError? = null
34+
35+
private val handler = CoinbaseOnRampEventHandler(
36+
onPaymentSuccess = { successCount++ },
37+
onPaymentFailure = { lastError = it },
38+
onCancel = { cancelCount++ },
39+
onAutoClickGPay = { autoClickCount++ },
40+
)
41+
42+
// --- Event routing ---
43+
44+
@Test
45+
fun loadSuccessTriggersAutoClick() {
46+
handler.handleEvent("""{"eventName":"onramp_api.load_success"}""")
47+
assertEquals(1, autoClickCount)
48+
}
49+
50+
@Test
51+
fun commitSuccessTriggersPaymentSuccess() {
52+
handler.handleEvent("""{"eventName":"onramp_api.commit_success"}""")
53+
assertEquals(1, successCount)
54+
}
55+
56+
@Test
57+
fun pollingSuccessTriggersPaymentSuccess() {
58+
handler.handleEvent("""{"eventName":"onramp_api.polling_success"}""")
59+
assertEquals(1, successCount)
60+
}
61+
62+
@Test
63+
fun cancelTriggersOnCancel() {
64+
handler.handleEvent("""{"eventName":"onramp_api.cancel"}""")
65+
assertEquals(1, cancelCount)
66+
}
67+
68+
// --- Error events ---
69+
70+
@Test
71+
fun commitErrorTriggersFailure() {
72+
handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":"ERROR_CODE_INTERNAL"}}""")
73+
assertEquals(CoinbaseOnRampWebError.ERROR_CODE_INTERNAL, lastError)
74+
}
75+
76+
@Test
77+
fun loadErrorTriggersFailure() {
78+
handler.handleEvent("""{"eventName":"onramp_api.load_error","data":{"errorCode":"ERROR_CODE_GUEST_GOOGLE_PAY_ERROR"}}""")
79+
assertEquals(CoinbaseOnRampWebError.ERROR_CODE_GUEST_GOOGLE_PAY_ERROR, lastError)
80+
}
81+
82+
@Test
83+
fun pollingErrorTriggersFailure() {
84+
handler.handleEvent("""{"eventName":"onramp_api.polling_error","data":{"errorCode":"ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED"}}""")
85+
assertEquals(CoinbaseOnRampWebError.ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED, lastError)
86+
}
87+
88+
@Test
89+
fun sessionErrorTriggersFailure() {
90+
handler.handleEvent("""{"eventName":"onramp_api.session_error","data":{"errorCode":"ERROR_CODE_GUEST_CARD_NOT_DEBIT"}}""")
91+
assertEquals(CoinbaseOnRampWebError.ERROR_CODE_GUEST_CARD_NOT_DEBIT, lastError)
92+
}
93+
94+
@Test
95+
fun errorWithUnknownCodeFallsBackToUnknown() {
96+
handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":"SOME_NEW_ERROR"}}""")
97+
assertEquals(CoinbaseOnRampWebError.UNKNOWN, lastError)
98+
}
99+
100+
@Test
101+
fun errorWithMissingDataFallsBackToUnknown() {
102+
handler.handleEvent("""{"eventName":"onramp_api.commit_error"}""")
103+
assertEquals(CoinbaseOnRampWebError.UNKNOWN, lastError)
104+
}
105+
106+
@Test
107+
fun errorWithEmptyErrorCodeFallsBackToUnknown() {
108+
handler.handleEvent("""{"eventName":"onramp_api.commit_error","data":{"errorCode":""}}""")
109+
assertEquals(CoinbaseOnRampWebError.UNKNOWN, lastError)
110+
}
111+
112+
// --- Edge cases ---
113+
114+
@Test
115+
fun invalidJsonDoesNotCrash() {
116+
handler.handleEvent("not json")
117+
assertEquals(0, successCount)
118+
assertEquals(0, cancelCount)
119+
}
120+
121+
@Test
122+
fun unknownEventNameIsIgnored() {
123+
handler.handleEvent("""{"eventName":"onramp_api.unknown_event"}""")
124+
assertEquals(0, successCount)
125+
assertEquals(0, cancelCount)
126+
assertTrue(lastError == null)
127+
}
128+
129+
@Test
130+
fun missingEventNameIsIgnored() {
131+
handler.handleEvent("""{"data":{"errorCode":"ERROR_CODE_INTERNAL"}}""")
132+
assertEquals(0, successCount)
133+
assertTrue(lastError == null)
134+
}
135+
}
136+
137+
class CoinbaseOnRampWebErrorTest {
138+
139+
@Test
140+
fun tryValueOfAllKnownCodes() {
141+
val expected = mapOf(
142+
"ERROR_CODE_MISSING_TRANSACTION_UUID" to CoinbaseOnRampWebError.ERROR_CODE_MISSING_TRANSACTION_UUID,
143+
"ERROR_CODE_GUEST_CARD_NOT_DEBIT" to CoinbaseOnRampWebError.ERROR_CODE_GUEST_CARD_NOT_DEBIT,
144+
"ERROR_CODE_GUEST_GOOGLE_PAY_ERROR" to CoinbaseOnRampWebError.ERROR_CODE_GUEST_GOOGLE_PAY_ERROR,
145+
"ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED" to CoinbaseOnRampWebError.ERROR_CODE_GUEST_TRANSACTION_BUY_FAILED,
146+
"ERROR_CODE_GUEST_TRANSACTION_SEND_FAILED" to CoinbaseOnRampWebError.ERROR_CODE_GUEST_TRANSACTION_SEND_FAILED,
147+
"ERROR_CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED" to CoinbaseOnRampWebError.ERROR_CODE_GUEST_TRANSACTION_AVS_VALIDATION_FAILED,
148+
"ERROR_CODE_GUEST_TRANSACTION_TRANSACTION_FAILED" to CoinbaseOnRampWebError.ERROR_CODE_GUEST_TRANSACTION_TRANSACTION_FAILED,
149+
"ERROR_CODE_INTERNAL" to CoinbaseOnRampWebError.ERROR_CODE_INTERNAL,
150+
"ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND" to CoinbaseOnRampWebError.ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND,
151+
)
152+
153+
for ((code, expectedError) in expected) {
154+
assertEquals(expectedError, CoinbaseOnRampWebError.tryValueOf(code), "Failed for code: $code")
155+
}
156+
}
157+
158+
@Test
159+
fun tryValueOfUnknownCodeReturnsUnknown() {
160+
assertEquals(CoinbaseOnRampWebError.UNKNOWN, CoinbaseOnRampWebError.tryValueOf("SOMETHING_NEW"))
161+
}
162+
163+
@Test
164+
fun tryValueOfEmptyStringReturnsUnknown() {
165+
assertEquals(CoinbaseOnRampWebError.UNKNOWN, CoinbaseOnRampWebError.tryValueOf(""))
166+
}
167+
168+
@Test
169+
fun tryValueOfCaseSensitive() {
170+
assertEquals(CoinbaseOnRampWebError.UNKNOWN, CoinbaseOnRampWebError.tryValueOf("error_code_internal"))
171+
}
172+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package com.getcode.opencode.model.core.errors
2+
3+
import com.codeinc.opencode.gen.transaction.v1.TransactionService.SubmitIntentResponse
4+
import com.codeinc.opencode.gen.transaction.v1.errorDetails
5+
import com.codeinc.opencode.gen.transaction.v1.reasonStringErrorDetails
6+
import com.codeinc.opencode.gen.transaction.v1.deniedErrorDetails
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertIs
10+
import kotlin.test.assertTrue
11+
12+
class SubmitIntentErrorTest {
13+
14+
private fun buildError(
15+
code: SubmitIntentResponse.Error.Code,
16+
reasonStrings: List<String> = emptyList(),
17+
deniedReasons: List<String> = emptyList(),
18+
): SubmitIntentResponse.Error {
19+
val builder = SubmitIntentResponse.Error.newBuilder()
20+
.setCode(code)
21+
22+
reasonStrings.forEach { reason ->
23+
builder.addErrorDetails(errorDetails {
24+
reasonString = reasonStringErrorDetails { this.reason = reason }
25+
})
26+
}
27+
28+
deniedReasons.forEach { reason ->
29+
builder.addErrorDetails(errorDetails {
30+
denied = deniedErrorDetails { this.reason = reason }
31+
})
32+
}
33+
34+
return builder.build()
35+
}
36+
37+
// --- Code mapping ---
38+
39+
@Test
40+
fun deniedCodeMapsToDenied() {
41+
val error = SubmitIntentError.typed(
42+
buildError(SubmitIntentResponse.Error.Code.DENIED)
43+
)
44+
assertIs<SubmitIntentError.Denied>(error)
45+
}
46+
47+
@Test
48+
fun invalidIntentCodeMapsToInvalidIntent() {
49+
val error = SubmitIntentError.typed(
50+
buildError(SubmitIntentResponse.Error.Code.INVALID_INTENT)
51+
)
52+
assertIs<SubmitIntentError.InvalidIntent>(error)
53+
}
54+
55+
@Test
56+
fun signatureErrorCodeMapsToSignature() {
57+
val error = SubmitIntentError.typed(
58+
buildError(SubmitIntentResponse.Error.Code.SIGNATURE_ERROR)
59+
)
60+
assertIs<SubmitIntentError.Signature>(error)
61+
}
62+
63+
@Test
64+
fun staleStateCodeMapsToStaleState() {
65+
val error = SubmitIntentError.typed(
66+
buildError(SubmitIntentResponse.Error.Code.STALE_STATE)
67+
)
68+
assertIs<SubmitIntentError.StaleState>(error)
69+
}
70+
71+
// Note: UNRECOGNIZED cannot be set via proto builders (throws IllegalArgumentException).
72+
// That code path is only reachable when the server sends an unknown enum value.
73+
74+
// --- Reason string extraction ---
75+
76+
@Test
77+
fun invalidIntentExtractsReasonStrings() {
78+
val error = SubmitIntentError.typed(
79+
buildError(
80+
SubmitIntentResponse.Error.Code.INVALID_INTENT,
81+
reasonStrings = listOf("bad amount", "missing account")
82+
)
83+
)
84+
assertIs<SubmitIntentError.InvalidIntent>(error)
85+
assertTrue(error.message!!.contains("bad amount"))
86+
assertTrue(error.message!!.contains("missing account"))
87+
}
88+
89+
@Test
90+
fun staleStateExtractsReasonStrings() {
91+
val error = SubmitIntentError.typed(
92+
buildError(
93+
SubmitIntentResponse.Error.Code.STALE_STATE,
94+
reasonStrings = listOf("nonce expired")
95+
)
96+
)
97+
assertIs<SubmitIntentError.StaleState>(error)
98+
assertEquals("nonce expired", error.message)
99+
}
100+
101+
@Test
102+
fun deniedExtractsDeniedReasons() {
103+
val error = SubmitIntentError.typed(
104+
buildError(
105+
SubmitIntentResponse.Error.Code.DENIED,
106+
deniedReasons = listOf("spam detected")
107+
)
108+
)
109+
assertIs<SubmitIntentError.Denied>(error)
110+
assertTrue(error.message!!.contains("spam detected"))
111+
}
112+
113+
@Test
114+
fun invalidIntentWithNoReasonsHasEmptyMessage() {
115+
val error = SubmitIntentError.typed(
116+
buildError(SubmitIntentResponse.Error.Code.INVALID_INTENT)
117+
)
118+
assertIs<SubmitIntentError.InvalidIntent>(error)
119+
assertEquals("", error.message)
120+
}
121+
122+
@Test
123+
fun emptyReasonStringsAreFiltered() {
124+
val error = SubmitIntentError.typed(
125+
buildError(
126+
SubmitIntentResponse.Error.Code.INVALID_INTENT,
127+
reasonStrings = listOf("", "real reason", "")
128+
)
129+
)
130+
assertIs<SubmitIntentError.InvalidIntent>(error)
131+
assertEquals("real reason", error.message)
132+
}
133+
134+
// --- Inheritance ---
135+
136+
@Test
137+
fun allVariantsAreThrowable() {
138+
val errors = listOf(
139+
SubmitIntentError.Denied(listOf("reason")),
140+
SubmitIntentError.InvalidIntent(listOf("reason")),
141+
SubmitIntentError.Signature(),
142+
SubmitIntentError.StaleState(listOf("reason")),
143+
SubmitIntentError.Unrecognized(),
144+
SubmitIntentError.Other(RuntimeException("test")),
145+
)
146+
errors.forEach { assertTrue(it is Throwable) }
147+
}
148+
149+
@Test
150+
fun otherWrausesCause() {
151+
val cause = RuntimeException("root cause")
152+
val error = SubmitIntentError.Other(cause)
153+
assertEquals(cause, error.cause)
154+
assertEquals("root cause", error.message)
155+
}
156+
}

0 commit comments

Comments
 (0)