Updated on 2026-08-14
This commit is contained in:
parent
7c1bfc2c7c
commit
f35975398d
5 changed files with 60 additions and 37 deletions
|
|
@ -57,9 +57,11 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor(
|
|||
|
||||
scope.launch {
|
||||
// Only trust the redirect if it carries a request_id we issued for a sell this
|
||||
// app actually started (single-use, bound to the wallet + currency). Otherwise an external
|
||||
// deeplink could inject a locked attacker recipient/amount into the Send confirm screen.
|
||||
val pendingOfframp = offrampRepository.consumePendingOfframp(
|
||||
// app actually started (bound to the wallet + currency, valid until it expires). Otherwise an
|
||||
// external deeplink could inject a locked attacker recipient/amount into the Send confirm
|
||||
// screen. The record is kept until expiry so the user can re-open the redirect within that
|
||||
// window.
|
||||
val pendingOfframp = offrampRepository.resolvePendingOfframp(
|
||||
requestId = requestId,
|
||||
userWalletId = userWallet.walletId,
|
||||
currencyId = currencyId,
|
||||
|
|
|
|||
|
|
@ -43,28 +43,43 @@ internal class DefaultSellRedirectDeepLinkHandlerTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN matching pending offramp WHEN deeplink handled THEN request passes the gate`() = runTest {
|
||||
coEvery { offrampRepository.consumePendingOfframp(requestId, userWalletId, currencyId) } returns pendingOfframp()
|
||||
coEvery { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) } returns pendingOfframp()
|
||||
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { offrampRepository.consumePendingOfframp(requestId, userWalletId, currencyId) }
|
||||
coVerify(exactly = 1) { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) }
|
||||
coVerify(exactly = 1) { singleAccountListSupplier.getSyncOrNull(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN matching pending offramp WHEN deeplink handled twice THEN gate passes both times`() = runTest {
|
||||
// The pending sell is not single-use: resolving it does not remove it, so re-opening the same deeplink must
|
||||
// pass the gate again. Guards against reintroducing single-use behavior in the handler.
|
||||
coEvery { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) } returns pendingOfframp()
|
||||
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 2) { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) }
|
||||
coVerify(exactly = 2) { singleAccountListSupplier.getSyncOrNull(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no request_id WHEN deeplink handled THEN rejected without touching the store`() = runTest {
|
||||
createHandler(validParams() - REQUEST_ID_KEY)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { offrampRepository.consumePendingOfframp(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { offrampRepository.resolvePendingOfframp(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { singleAccountListSupplier.getSyncOrNull(any<UserWalletId>()) }
|
||||
verify(exactly = 0) { appRouter.push(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no matching pending offramp WHEN deeplink handled THEN rejected`() = runTest {
|
||||
coEvery { offrampRepository.consumePendingOfframp(requestId, userWalletId, currencyId) } returns null
|
||||
coEvery { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) } returns null
|
||||
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue