From dbfd47850e107f9dc4e25a9b98a75c6b99e46725 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 8 Oct 2026 12:08:08 +0800 Subject: [PATCH] fix(route): sync bot public key before requests --- .../android/ui/wallet/fiatmoney/RouteAPI.kt | 12 ++++ .../ui/common/LoginRecoveryPolicyTest.kt | 9 +++ .../ui/wallet/fiatmoney/RouteAPITest.kt | 61 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 app/src/test/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPITest.kt diff --git a/app/src/main/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPI.kt b/app/src/main/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPI.kt index df7dd03248..28e09e7649 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPI.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPI.kt @@ -32,6 +32,18 @@ suspend fun requestRouteAPI( ): R? { val response = try { + val preferences = MixinApplication.appContext.defaultSharedPreferences + if (preferences.getString(PREF_ROUTE_BOT_PK, null).isNullOrBlank()) { + val sessionResponse = requestSession(listOf(ROUTE_BOT_USER_ID)) + if (!sessionResponse.isSuccess) { + defaultErrorHandle(MixinResponse(requireNotNull(sessionResponse.error))) + endBlock?.invoke() + return null + } + val publicKey = sessionResponse.data?.firstOrNull { it.userId == ROUTE_BOT_USER_ID }?.publicKey + check(!publicKey.isNullOrBlank()) { "Route bot public key is missing" } + preferences.putString(PREF_ROUTE_BOT_PK, publicKey) + } invokeNetwork() } catch (t: Throwable) { if (exceptionBlock?.invoke(t) != true) { diff --git a/app/src/test/java/one/mixin/android/ui/common/LoginRecoveryPolicyTest.kt b/app/src/test/java/one/mixin/android/ui/common/LoginRecoveryPolicyTest.kt index 96b9362afa..ea0c110ed1 100644 --- a/app/src/test/java/one/mixin/android/ui/common/LoginRecoveryPolicyTest.kt +++ b/app/src/test/java/one/mixin/android/ui/common/LoginRecoveryPolicyTest.kt @@ -1,8 +1,12 @@ package one.mixin.android.ui.common +import androidx.test.core.app.ApplicationProvider import kotlinx.coroutines.runBlocking +import one.mixin.android.Constants.Account.PREF_ROUTE_BOT_PK +import one.mixin.android.MixinApplication import one.mixin.android.api.MixinResponse import one.mixin.android.api.ResponseError +import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.ui.wallet.fiatmoney.requestRouteAPI import one.mixin.android.util.ErrorHandler import one.mixin.android.vo.WalletCategory @@ -10,10 +14,15 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class LoginRecoveryPolicyTest { @Test fun missingWalletIsDeletedAndItsErrorIsHandledDuringLogin() = runBlocking { + MixinApplication.appContext = ApplicationProvider.getApplicationContext() + MixinApplication.appContext.defaultSharedPreferences.edit().putString(PREF_ROUTE_BOT_PK, "cached-key").commit() val wallets = mutableListOf("missing", "active") var defaultErrorHandled = false val result = requestRouteAPI( diff --git a/app/src/test/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPITest.kt b/app/src/test/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPITest.kt new file mode 100644 index 0000000000..e6cfe19c6c --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/wallet/fiatmoney/RouteAPITest.kt @@ -0,0 +1,61 @@ +package one.mixin.android.ui.wallet.fiatmoney + +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.runBlocking +import one.mixin.android.Constants.Account.PREF_ROUTE_BOT_PK +import one.mixin.android.Constants.RouteConfig.ROUTE_BOT_USER_ID +import one.mixin.android.MixinApplication +import one.mixin.android.api.MixinResponse +import one.mixin.android.api.ResponseError +import one.mixin.android.api.response.UserSession +import one.mixin.android.extension.defaultSharedPreferences +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RouteAPITest { + @Test + fun waitsForPublicKeyAndStopsWhenSynchronizationFails() = runBlocking { + MixinApplication.appContext = ApplicationProvider.getApplicationContext() + val preferences = MixinApplication.appContext.defaultSharedPreferences + val events = mutableListOf() + for (mode in listOf("missing", "cached", "failed", "empty")) { + preferences.edit().remove(PREF_ROUTE_BOT_PK).commit() + if (mode == "cached") preferences.edit().putString(PREF_ROUTE_BOT_PK, "key").commit() + events.clear() + val result = requestRouteAPI( + invokeNetwork = { + assertEquals("key", preferences.getString(PREF_ROUTE_BOT_PK, null)) + events.add("request") + MixinResponse() + }, + successBlock = { true }, + failureBlock = { error("Session failure must not invoke the wallet failure handler") }, + defaultErrorHandle = { events.add("error") }, + defaultExceptionHandle = { events.add("exception") }, + endBlock = { events.add("end") }, + requestSession = { ids -> + assertEquals(listOf(ROUTE_BOT_USER_ID), ids) + events.add("sync") + when (mode) { + "cached" -> error("Cached public key must be reused") + "failed" -> MixinResponse(ResponseError(404, 404, "Not found")) + else -> MixinResponse>().apply { + data = listOf(UserSession(ROUTE_BOT_USER_ID, "session", null, if (mode == "empty") "" else "key")) + } + } + }, + ) + when (mode) { + "missing" -> assertEquals(listOf("sync", "request", "end"), events) + "cached" -> assertEquals(listOf("request", "end"), events) + "failed" -> assertEquals(listOf("sync", "error", "end"), events) + "empty" -> assertEquals(listOf("sync", "exception", "end"), events) + } + if (mode == "failed" || mode == "empty") assertNull(result) else assertEquals(true, result) + } + } +}