diff --git a/.idea/misc.xml b/.idea/misc.xml index 6e4b8a0d..435c7127 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 00000000..4604c446 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,252 @@ + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 61ba9241..fe59d84f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,24 +1,17 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.disableAutomaticComponentCreation=true +android.nonTransitiveRClass=true android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true -android.disableAutomaticComponentCreation=true +org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Dfile.encoding\=UTF-8 diff --git a/metamask-android-sdk/build.gradle b/metamask-android-sdk/build.gradle index 3a2492b1..9da2f575 100644 --- a/metamask-android-sdk/build.gradle +++ b/metamask-android-sdk/build.gradle @@ -57,6 +57,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'org.mockito:mockito-core:4.0.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0' androidTestImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt new file mode 100644 index 00000000..5f2a8595 --- /dev/null +++ b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt @@ -0,0 +1,12 @@ +package io.metamask.androidsdk + +import android.os.Bundle +import io.metamask.nativesdk.IMessegeServiceCallback + +open class ClientMessageServiceCallback( + var onMessage: ((Bundle) -> Unit)? = null +) : IMessegeServiceCallback.Stub() { + override fun onMessageReceived(bundle: Bundle) { + onMessage?.invoke(bundle) + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt new file mode 100644 index 00000000..0d440318 --- /dev/null +++ b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt @@ -0,0 +1,42 @@ +package io.metamask.androidsdk + +import android.content.ComponentName +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import io.metamask.nativesdk.IMessegeService +import io.metamask.nativesdk.IMessegeServiceCallback + +open class ClientServiceConnection( + var onConnected: (() -> Unit)? = null, + var onDisconnected: ((ComponentName?) -> Unit)? = null, + var onBindingDied: ((ComponentName?) -> Unit)? = null, + var onNullBinding: ((ComponentName?) -> Unit)? = null +) : ServiceConnection { + private var messageService: IMessegeService? = null + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + messageService = IMessegeService.Stub.asInterface(service) + onConnected?.invoke() + } + + override fun onServiceDisconnected(name: ComponentName?) { + onDisconnected?.invoke(name) + } + + override fun onBindingDied(name: ComponentName?) { + onBindingDied?.invoke(name) + } + + override fun onNullBinding(name: ComponentName?) { + onNullBinding?.invoke(name) + } + + open fun registerCallback(callback: IMessegeServiceCallback) { + messageService?.registerCallback(callback) + } + + open fun sendMessage(bundle: Bundle) { + messageService?.sendMessage(bundle) + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClient.kt b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClient.kt index 18c7b76b..d8380ac2 100644 --- a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClient.kt +++ b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClient.kt @@ -3,15 +3,11 @@ package io.metamask.androidsdk import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.ServiceConnection import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.os.IBinder import com.google.gson.Gson import com.google.gson.reflect.TypeToken -import io.metamask.nativesdk.IMessegeService -import io.metamask.nativesdk.IMessegeServiceCallback import kotlinx.serialization.Serializable import org.json.JSONObject import java.lang.ref.WeakReference @@ -21,6 +17,9 @@ class CommunicationClient( callback: EthereumEventCallback?, private val sessionManager: SessionManager, private val keyExchange: KeyExchange, + private val serviceConnection: ClientServiceConnection, + private val messageServiceCallback: ClientMessageServiceCallback, + private val tracker: Tracker, private val logger: Logger = DefaultLogger) { var sessionId: String = "" @@ -29,23 +28,23 @@ class CommunicationClient( var isServiceConnected = false private set - private val tracker: Tracker = Analytics() - - private var messageService: IMessegeService? = null private val appContextRef: WeakReference = WeakReference(context) var ethereumEventCallbackRef: WeakReference = WeakReference(callback) - private var requestJobs: MutableList<() -> Unit> = mutableListOf() - private var submittedRequests: MutableMap = mutableMapOf() - private var queuedRequests: MutableMap = mutableMapOf() + var requestJobs: MutableList<() -> Unit> = mutableListOf() + private set + + var submittedRequests: MutableMap = mutableMapOf() + private set + + var queuedRequests: MutableMap = mutableMapOf() + private set private var isMetaMaskReady = false private var sentOriginatorInfo = false - private var requestedBindService = false - var hasSubmittedRequests: Boolean = submittedRequests.isEmpty() - var hasRequestJobs: Boolean = requestJobs.isEmpty() - var hasQueuedRequests: Boolean = queuedRequests.isEmpty() + var requestedBindService = false + private set var enableDebug: Boolean = false set(value) { @@ -59,6 +58,8 @@ class CommunicationClient( sessionManager.onInitialized = { sessionId = sessionManager.sessionId } + setupServiceConnection() + setupMessageServiceCallback() } fun resetState() { @@ -68,33 +69,31 @@ class CommunicationClient( requestJobs.clear() } - private val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - messageService = IMessegeService.Stub.asInterface(service) - messageService?.registerCallback(messageServiceCallback) - isServiceConnected = true + private fun setupServiceConnection() { + serviceConnection.onConnected = { logger.log("CommunicationClient:: Service connected") + isServiceConnected = true + serviceConnection.registerCallback(messageServiceCallback) initiateKeyExchange() } - override fun onServiceDisconnected(name: ComponentName?) { - messageService = null + serviceConnection.onDisconnected = { name -> isServiceConnected = false logger.error("CommunicationClient:: Service disconnected $name") trackEvent(Event.SDK_DISCONNECTED) } - override fun onBindingDied(name: ComponentName?) { + serviceConnection.onBindingDied = { name -> logger.error("CommunicationClient:: binding died: $name") } - override fun onNullBinding(name: ComponentName?) { + serviceConnection.onNullBinding = { name -> logger.error("CommunicationClient:: null binding: $name") } } - private val messageServiceCallback: IMessegeServiceCallback = object : IMessegeServiceCallback.Stub() { - override fun onMessageReceived(bundle: Bundle) { + private fun setupMessageServiceCallback() { + messageServiceCallback.onMessage = { bundle -> val keyExchange = bundle.getString(KEY_EXCHANGE) val message = bundle.getString(MESSAGE) @@ -214,7 +213,7 @@ class CommunicationClient( submittedRequests = mutableMapOf() } - private fun handleResponse(id: String, data: JSONObject) { + fun handleResponse(id: String, data: JSONObject) { val submittedRequest = submittedRequests[id]?.request ?: return val error = data.optString("error") @@ -359,7 +358,7 @@ class CommunicationClient( val paramsJson = event.optJSONObject("params") val chainId = paramsJson?.optString("chainId") - if (chainId != null && chainId.isNotEmpty()) { + if (!chainId.isNullOrEmpty()) { updateChainId(chainId) } } @@ -409,10 +408,10 @@ class CommunicationClient( } if (keyExchange.keysExchanged()) { - messageService?.sendMessage(bundle) + serviceConnection.sendMessage(bundle) } else { logger.log("CommunicationClient::sendMessage keys not exchanged, queueing job") - queueRequestJob { messageService?.sendMessage(bundle) } + queueRequestJob { serviceConnection.sendMessage(bundle) } } } @@ -549,6 +548,6 @@ class CommunicationClient( val bundle = Bundle().apply { putString(KEY_EXCHANGE, message) } - messageService?.sendMessage(bundle) + serviceConnection.sendMessage(bundle) } } \ No newline at end of file diff --git a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt index f9b39bc2..0a6b392a 100644 --- a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt +++ b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt @@ -2,8 +2,8 @@ package io.metamask.androidsdk import android.content.Context -class CommunicationClientModule(private val context: Context): CommunicationClientModuleInterface { - override fun provideKeyStorage(): KeyStorage { +open class CommunicationClientModule(private val context: Context): CommunicationClientModuleInterface { + override fun provideKeyStorage(): SecureStorage { return KeyStorage(context) } @@ -19,11 +19,35 @@ class CommunicationClientModule(private val context: Context): CommunicationClie return DefaultLogger } + override fun provideTracker(): Tracker { + return Analytics() + } + + override fun provideClientServiceConnection(): ClientServiceConnection { + return ClientServiceConnection() + } + + override fun provideClientMessageServiceCallback(): ClientMessageServiceCallback { + return ClientMessageServiceCallback() + } + override fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient { val keyStorage = provideKeyStorage() val sessionManager = provideSessionManager(keyStorage) val keyExchange = provideKeyExchange() + val serviceConnection = provideClientServiceConnection() + val messageServiceCallback = provideClientMessageServiceCallback() val logger = provideLogger() - return CommunicationClient(context, callback, sessionManager, keyExchange, logger) + val tracker = provideTracker() + + return CommunicationClient( + context, + callback, + sessionManager, + keyExchange, + serviceConnection, + messageServiceCallback, + tracker, + logger) } } \ No newline at end of file diff --git a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt index 95b343c5..fdb3a4cc 100644 --- a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt +++ b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt @@ -5,5 +5,8 @@ interface CommunicationClientModuleInterface { fun provideSessionManager(keyStorage: SecureStorage): SessionManager fun provideKeyExchange(): KeyExchange fun provideLogger(): Logger + fun provideTracker(): Tracker + fun provideClientServiceConnection(): ClientServiceConnection + fun provideClientMessageServiceCallback(): ClientMessageServiceCallback fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient } \ No newline at end of file diff --git a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyExchange.kt b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyExchange.kt index 357909cf..d44e4c89 100644 --- a/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyExchange.kt +++ b/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyExchange.kt @@ -19,6 +19,7 @@ class KeyExchange(private val crypto: Encryption = Crypto(), private val logger: private var isKeysExchanged = false init { + reset() crypto.onInitialized = { reset() } diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/CommunicationClientTests.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/CommunicationClientTests.kt new file mode 100644 index 00000000..a49d7487 --- /dev/null +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/CommunicationClientTests.kt @@ -0,0 +1,363 @@ +package io.metamask.androidsdk + +import android.content.ComponentName +import android.content.Context +import android.os.Bundle +import android.os.IBinder +import io.metamask.androidsdk.KeyExchangeMessageType.* +import io.metamask.nativesdk.IMessegeService +import io.metamask.androidsdk.Event.* +import org.json.JSONObject +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.any +import org.mockito.Mockito + +import org.robolectric.RobolectricTestRunner +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +@RunWith(RobolectricTestRunner::class) +class CommunicationClientTest { + + private lateinit var context: Context + + private lateinit var mockEthereumEventCallback: MockEthereumEventCallback + private lateinit var logger: Logger + private lateinit var keyExchange: KeyExchange + private lateinit var sessionManager: SessionManager + private lateinit var mockClientServiceConnection: MockClientServiceConnection + private lateinit var mockClientMessageServiceCallback: MockClientMessageServiceCallback + private lateinit var communicationClient: CommunicationClient + private lateinit var mockCrypto: MockCrypto + private lateinit var mockTracker: MockTracker + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + context = mock() + + logger = TestLogger + mockEthereumEventCallback = MockEthereumEventCallback() + mockClientServiceConnection = MockClientServiceConnection() + mockClientMessageServiceCallback = MockClientMessageServiceCallback() + + mockCrypto = MockCrypto() + mockTracker = MockTracker() + keyExchange = KeyExchange(mockCrypto, logger) + sessionManager = SessionManager(MockKeyStorage()) + + communicationClient = CommunicationClient( + context, + mockEthereumEventCallback, + sessionManager, + keyExchange, + mockClientServiceConnection, + mockClientMessageServiceCallback, + mockTracker, + logger + ) + } + + @Test + fun testInit() { + assertNotNull(communicationClient) + assertEquals(sessionManager.sessionId, communicationClient.sessionId) + } + + @Test + fun testServiceConnection() { + val mockBinder = Mockito.mock(IBinder::class.java) + val mockMessageService = Mockito.mock(IMessegeService::class.java) + `when`(IMessegeService.Stub.asInterface(mockBinder)).thenReturn(mockMessageService) + + mockClientServiceConnection.onServiceConnected(ComponentName(context, "Service"), mockBinder) + + assertTrue(mockClientServiceConnection.serviceConectionCalled) + assertTrue(mockClientServiceConnection.registerCallbackCalled) + assertTrue(communicationClient.isServiceConnected) + verify(mockMessageService).registerCallback(any()) + } + + @Test + fun testSendMessageBeforeKeysExchanged() { + val testMessage = "test_message" + + assertTrue(communicationClient.submittedRequests.isEmpty()) + assertTrue(communicationClient.requestJobs.isEmpty()) + assertTrue(communicationClient.queuedRequests.isEmpty()) + + communicationClient.sendMessage(testMessage) + + assertTrue(communicationClient.submittedRequests.isEmpty()) + assertFalse(communicationClient.requestJobs.isEmpty()) + + assertFalse(mockClientServiceConnection.sendMessageCalled) + } + + @Test + fun testSendMessageAfterKeysExchanged() { + val testMessage = "test_message" + + assertTrue(communicationClient.submittedRequests.isEmpty()) + assertTrue(communicationClient.requestJobs.isEmpty()) + assertTrue(communicationClient.queuedRequests.isEmpty()) + + // force key exchange + keyExchange.complete() + + communicationClient.sendMessage(testMessage) + + assertTrue(mockClientServiceConnection.sendMessageCalled) + assertEquals(testMessage, mockClientServiceConnection.sentMessage?.getString(MESSAGE)) + } + + @Test + fun testMessageReception() { + val testMessage = "test_message" + + val sentMessage = Bundle().apply { + putString(MESSAGE, testMessage) + } + + var receivedMessage: Bundle? = null + + // force key exchange + keyExchange.complete() + + mockClientMessageServiceCallback.onMessage = { message -> + receivedMessage = message + } + + mockClientMessageServiceCallback.onMessageReceived(sentMessage) + assertTrue(mockClientMessageServiceCallback.messageReceived) + + assertEquals(testMessage, receivedMessage?.getString(MESSAGE)) + } + + @Test + fun testBindServiceCalledWhenServiceNotConnected() { + assertFalse(mockClientServiceConnection.serviceConectionCalled) + assertFalse(mockClientServiceConnection.registerCallbackCalled) + assertFalse(communicationClient.isServiceConnected) + + val request = EthereumRequest(method = EthereumMethod.ETH_REQUEST_ACCOUNTS.value) + + communicationClient.sendRequest(request) { } + assertTrue(communicationClient.requestedBindService) + verify(context, times(1)).bindService(any(), any(), eq(Context.BIND_AUTO_CREATE)) + } + + @Test + fun testSendRequestBeforeServiceConnection() { + assertFalse(mockClientServiceConnection.serviceConectionCalled) + assertFalse(mockClientServiceConnection.registerCallbackCalled) + assertFalse(communicationClient.isServiceConnected) + + val request = EthereumRequest(method = EthereumMethod.ETH_REQUEST_ACCOUNTS.value) + communicationClient.sendRequest(request) { } + + // check that bindService is called + assertTrue(communicationClient.requestedBindService) + + assertTrue(communicationClient.queuedRequests.isNotEmpty()) + assertTrue(communicationClient.requestJobs.isNotEmpty()) + + assertFalse(mockClientServiceConnection.sendMessageCalled) + } + + @Test + fun testSendRequestAfterServiceConnectionBeforeKeysExchangeInitiatesKeyExchange() { + val mockBinder = Mockito.mock(IBinder::class.java) + val mockMessageService = Mockito.mock(IMessegeService::class.java) + `when`(IMessegeService.Stub.asInterface(mockBinder)).thenReturn(mockMessageService) + + mockClientServiceConnection.onServiceConnected(ComponentName(context, "Service"), mockBinder) + assertTrue(communicationClient.isServiceConnected) + + val request = EthereumRequest(method = EthereumMethod.ETH_REQUEST_ACCOUNTS.value) + communicationClient.sendRequest(request) { } + + assertTrue(mockClientServiceConnection.sendMessageCalled) + + keyExchange.reset() + + // test that sent message is key exchange + val sentMessage = mockClientServiceConnection.sentMessage + assertNotNull(sentMessage) + + val keyExchangeJsonString = sentMessage?.getString(KEY_EXCHANGE) ?: "" + val keyExchangeJsonObject = JSONObject(keyExchangeJsonString) + assertEquals(keyExchangeJsonObject.getString(KeyExchange.TYPE), KeyExchangeMessageType.KEY_HANDSHAKE_SYN.name) + assertEquals(keyExchangeJsonObject.getString(KeyExchange.PUBLIC_KEY), keyExchange.publicKey) + } + + @Test + fun testSendMessageBeforeMetamaskIsReadySendsOriginatorInfo() { + val mockBinder = Mockito.mock(IBinder::class.java) + val mockMessageService = Mockito.mock(IMessegeService::class.java) + `when`(IMessegeService.Stub.asInterface(mockBinder)).thenReturn(mockMessageService) + + // mock service connection + mockClientServiceConnection.onServiceConnected(ComponentName(context, "Service"), mockBinder) + + // mock receiver + val receiverKeyExchange = KeyExchange(MockCrypto(), logger) + + // exchange public keys + val receiverKeyExchangeMessage = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, receiverKeyExchange.publicKey) + val senderKeyExchangeMessage = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, keyExchange.publicKey) + + keyExchange.nextKeyExchangeMessage(receiverKeyExchangeMessage) + receiverKeyExchange.nextKeyExchangeMessage(senderKeyExchangeMessage) + + // mock key exchange complete + keyExchange.complete() + + val request = EthereumRequest(method = EthereumMethod.ETH_REQUEST_ACCOUNTS.value) + communicationClient.sendRequest(request) { } + + // test that message sent message is OriginatorInfo + val sentMessageBundle = mockClientServiceConnection.sentMessage + val sentMessageJsonString = sentMessageBundle?.getString(MESSAGE) ?: "" + + val messageJsonObject = JSONObject(sentMessageJsonString) + val encryptedMessage = messageJsonObject.getString(MESSAGE) + val decryptedMessage = receiverKeyExchange.decrypt(encryptedMessage) + val messageJSON = JSONObject(decryptedMessage) + assertEquals(messageJSON.getString("type"), "originator_info") + } + + @Test + fun testSendRequestMessageWhenMetaMaskIsReady() { + val mockBinder = Mockito.mock(IBinder::class.java) + val mockMessageService = Mockito.mock(IMessegeService::class.java) + `when`(IMessegeService.Stub.asInterface(mockBinder)).thenReturn(mockMessageService) + + // mock service connection + mockClientServiceConnection.onServiceConnected(ComponentName(context, "Service"), mockBinder) + + // mock receiver + val receiverKeyExchange = KeyExchange(MockCrypto(), logger) + + // exchange public keys + val receiverKeyExchangeMessage = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, receiverKeyExchange.publicKey) + val senderKeyExchangeMessage = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, keyExchange.publicKey) + + keyExchange.nextKeyExchangeMessage(receiverKeyExchangeMessage) + receiverKeyExchange.nextKeyExchangeMessage(senderKeyExchangeMessage) + + // mock key exchange complete + keyExchange.complete() + + val request = EthereumRequest(method = EthereumMethod.ETH_REQUEST_ACCOUNTS.value) + communicationClient.sendRequest(request) { } + + // mock receiving ready message + val readyMessage = JSONObject().apply { + put(MessageType.TYPE.value, MessageType.READY.value) + }.toString() + val encryptedReadyMessage = receiverKeyExchange.encrypt(readyMessage) + + // simulate MetaMask Ready + communicationClient.handleMessage(encryptedReadyMessage) + + // test that message sent message is request message + val sentMessageBundle = mockClientServiceConnection.sentMessage + val sentMessageJsonString = sentMessageBundle?.getString(MESSAGE) ?: "" + + val messageJsonObject = JSONObject(sentMessageJsonString) + val encryptedMessage = messageJsonObject.getString(MESSAGE) + val decryptedMessage = receiverKeyExchange.decrypt(encryptedMessage) + val messageJSON = JSONObject(decryptedMessage) + assertEquals(messageJSON.getString("method"), request.method) + assertEquals(messageJSON.getString("id"), request.id) + } + + @Test + fun testTrackSDKDisconnectedEvent() { + // Prepare the name of the component that got disconnected + val componentName = ComponentName("service_package", "service_class") + + // Trigger the onDisconnected event + mockClientServiceConnection.onDisconnected?.invoke(componentName) + + // Verify that the event is logged and tracked correctly + val trackedEvent = mockTracker.trackedEvent + assertEquals(trackedEvent, SDK_DISCONNECTED) + assertNotNull(mockTracker.trackedEventParams) + assertEquals(SDK_DISCONNECTED.value, mockTracker.trackedEventParams?.get("event")) + + // Ensure that the internal state is updated + assertFalse(communicationClient.isServiceConnected) + } + + @Test + fun testTrackSdkRpcRequestDoneEvent() { + val mockBinder = Mockito.mock(IBinder::class.java) + val mockMessageService = Mockito.mock(IMessegeService::class.java) + `when`(IMessegeService.Stub.asInterface(mockBinder)).thenReturn(mockMessageService) + + // mock service connection + mockClientServiceConnection.onServiceConnected(ComponentName(context, "Service"), mockBinder) + + // mock receiver + val receiverKeyExchange = KeyExchange(MockCrypto(), logger) + + // exchange public keys + val receiverKeyExchangeMessage = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, receiverKeyExchange.publicKey) + val senderKeyExchangeMessage = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, keyExchange.publicKey) + + keyExchange.nextKeyExchangeMessage(receiverKeyExchangeMessage) + receiverKeyExchange.nextKeyExchangeMessage(senderKeyExchangeMessage) + + // mock key exchange complete + keyExchange.complete() + + val request = EthereumRequest(method = EthereumMethod.ETH_SIGN_TRANSACTION.value) + communicationClient.sendRequest(request) { } + + // mock receiving ready message + val readyMessage = JSONObject().apply { + put(MessageType.TYPE.value, MessageType.READY.value) + }.toString() + val encryptedReadyMessage = receiverKeyExchange.encrypt(readyMessage) + + // simulate MetaMask Ready. Sends queued request above + communicationClient.handleMessage(encryptedReadyMessage) + + // simulate receiving response for RPC request + val responseData = JSONObject().apply { + put("id", request.id) + put("result", "0x123456789") + } + communicationClient.handleResponse(request.id, responseData) + + val trackedEvent = mockTracker.trackedEvent + assertEquals(trackedEvent, SDK_RPC_REQUEST_DONE) + assertNotNull(mockTracker.trackedEventParams) + assertEquals(SDK_RPC_REQUEST_DONE.value, mockTracker.trackedEventParams?.get("event")) + } + + @Test + fun testUpdateAccount() { + val testAccount = "0x123" + communicationClient.updateAccount(testAccount) + + assertEquals(mockEthereumEventCallback.account, testAccount) + } + + @Test + fun testUpdateChainId() { + val testChainId = "0x1" + communicationClient.updateChainId(testChainId) + + assertEquals(mockEthereumEventCallback.chainId, testChainId) + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientMessageServiceCallback.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientMessageServiceCallback.kt new file mode 100644 index 00000000..75bcec92 --- /dev/null +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientMessageServiceCallback.kt @@ -0,0 +1,14 @@ +package io.metamask.androidsdk + +import android.os.Bundle + +class MockClientMessageServiceCallback: ClientMessageServiceCallback() { + var messageReceived = false + var receivedMessage: Bundle? = null + + override fun onMessageReceived(bundle: Bundle) { + super.onMessageReceived(bundle) + messageReceived = true + receivedMessage = bundle + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientServiceConnection.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientServiceConnection.kt new file mode 100644 index 00000000..694bc475 --- /dev/null +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientServiceConnection.kt @@ -0,0 +1,44 @@ +package io.metamask.androidsdk + +import android.content.ComponentName +import android.os.Bundle +import android.os.IBinder +import io.metamask.nativesdk.IMessegeService +import io.metamask.nativesdk.IMessegeServiceCallback + +class MockClientServiceConnection: ClientServiceConnection() { + var serviceConectionCalled = false + var serviceDisconnectionCalled = false + var registerCallbackCalled = false + var sendMessageCalled = false + var sentMessage: Bundle? = null + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + super.onServiceConnected(name, service) + serviceConectionCalled = true + } + + override fun onServiceDisconnected(name: ComponentName?) { + onDisconnected?.invoke(name) + serviceDisconnectionCalled = true + } + + override fun onBindingDied(name: ComponentName?) { + onBindingDied?.invoke(name) + } + + override fun onNullBinding(name: ComponentName?) { + onNullBinding?.invoke(name) + } + + override fun registerCallback(callback: IMessegeServiceCallback) { + super.registerCallback(callback) + registerCallbackCalled = true + } + + override fun sendMessage(bundle: Bundle) { + super.sendMessage(bundle) + sendMessageCalled = true + sentMessage = bundle + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCommunicationClientModule.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCommunicationClientModule.kt new file mode 100644 index 00000000..760a46b1 --- /dev/null +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCommunicationClientModule.kt @@ -0,0 +1,54 @@ +package io.metamask.androidsdk + +import android.content.Context +import io.metamask.androidsdk.MockTracker + +class MockCommunicationClientModule(private val context: Context): CommunicationClientModuleInterface{ + override fun provideKeyStorage(): SecureStorage { + return MockKeyStorage() + } + + override fun provideSessionManager(keyStorage: SecureStorage): SessionManager { + return SessionManager(keyStorage) + } + + override fun provideKeyExchange(): KeyExchange { + return KeyExchange(MockCrypto()) + } + + override fun provideLogger(): Logger { + return TestLogger + } + + override fun provideTracker(): Tracker { + return MockTracker() + } + + override fun provideClientServiceConnection(): ClientServiceConnection { + return MockClientServiceConnection() + } + + override fun provideClientMessageServiceCallback(): ClientMessageServiceCallback { + return MockClientMessageServiceCallback() + } + + override fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient { + val keyStorage = provideKeyStorage() + val sessionManager = provideSessionManager(keyStorage) + val keyExchange = provideKeyExchange() + val logger = provideLogger() + val tracker = provideTracker() + val serviceConnection = provideClientServiceConnection() + val clientMessageServiceCallback = provideClientMessageServiceCallback() + + return CommunicationClient( + context, + callback, + sessionManager, + keyExchange, + serviceConnection, + clientMessageServiceCallback, + tracker, + logger) + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCrypto.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCrypto.kt index b022c205..87a90670 100644 --- a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCrypto.kt +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCrypto.kt @@ -1,9 +1,17 @@ package io.metamask.androidsdk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + class MockCrypto() : Encryption { - private val rsaEncryption = RSAEncryption() + private val rsaEncryption: RSAEncryption = RSAEncryption() override var onInitialized: () -> Unit = {} + init { + onInitialized() + } + override fun generatePrivateKey(): String { return rsaEncryption.generatePrivateKey() } diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockEthereumEventCallback.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockEthereumEventCallback.kt new file mode 100644 index 00000000..87b9427b --- /dev/null +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockEthereumEventCallback.kt @@ -0,0 +1,14 @@ +package io.metamask.androidsdk + +class MockEthereumEventCallback : EthereumEventCallback { + var account: String = "" + var chainId: String = "" + + override fun updateAccount(account: String) { + this.account = account + } + + override fun updateChainId(newChainId: String) { + this.chainId = newChainId + } +} \ No newline at end of file diff --git a/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockTracker.kt b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockTracker.kt new file mode 100644 index 00000000..80b10962 --- /dev/null +++ b/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockTracker.kt @@ -0,0 +1,15 @@ +package io.metamask.androidsdk + +class MockTracker(override var enableDebug: Boolean = true) : Tracker { + + var trackedEvent: Event? = null + var trackedEventParams: MutableMap? = null + + override fun trackEvent(event: Event, params: MutableMap) { + if (!enableDebug) { return } + + params["event"] = event.value + trackedEvent = event + trackedEventParams = params + } +} \ No newline at end of file