mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-05-12 21:20:42 +01:00
lib: expand test coverage
This commit is contained in:
parent
46637358db
commit
6f0e214d87
12 changed files with 378 additions and 8 deletions
|
@ -15,4 +15,5 @@ androidx-test-runner = { module = "androidx.test:runner", version.ref = "android
|
|||
androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-truth" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
|
||||
truth = { module = "com.google.truth:truth", version.ref = "truth" }
|
||||
|
|
|
@ -116,5 +116,5 @@ dependencies {
|
|||
androidTestImplementation(testLibs.androidx.test.truth)
|
||||
androidTestImplementation(testLibs.androidx.test.rules)
|
||||
androidTestImplementation(testLibs.androidx.test.runner)
|
||||
androidTestImplementation(testLibs.mockk)
|
||||
androidTestImplementation(testLibs.mockk.android)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package im.molly.monero
|
||||
|
||||
import com.google.common.truth.FailureMetadata
|
||||
import com.google.common.truth.Subject
|
||||
import com.google.common.truth.Truth.assertAbout
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class MoneroWalletSubject private constructor(
|
||||
metadata: FailureMetadata,
|
||||
private val actual: MoneroWallet,
|
||||
) : Subject(metadata, actual) {
|
||||
|
||||
companion object {
|
||||
fun assertThat(wallet: MoneroWallet): MoneroWalletSubject {
|
||||
return assertAbout(factory).that(wallet)
|
||||
}
|
||||
|
||||
private val factory = Factory { metadata, actual: MoneroWallet ->
|
||||
MoneroWalletSubject(metadata, actual)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun matchesStateOf(expected: MoneroWallet) {
|
||||
with(actual) {
|
||||
check("publicAddress").that(publicAddress).isEqualTo(expected.publicAddress)
|
||||
check("ledger").that(ledger().first()).isEqualTo(expected.ledger().first())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import kotlin.random.Random
|
|||
class SecretKeyParcelableTest {
|
||||
|
||||
@Test
|
||||
fun testParcel() {
|
||||
fun secretKeyIsParcelable() {
|
||||
val secret = Random.nextBytes(32)
|
||||
val originalKey = SecretKey(secret)
|
||||
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
package im.molly.monero.e2etest
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ServiceTestRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.InMemoryWalletDataStore
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.MoneroWallet
|
||||
import im.molly.monero.MoneroWalletSubject
|
||||
import im.molly.monero.RestorePoint
|
||||
import im.molly.monero.SecretKey
|
||||
import im.molly.monero.WalletProvider
|
||||
import im.molly.monero.internal.IWalletService
|
||||
import im.molly.monero.internal.WalletServiceClient
|
||||
import im.molly.monero.mnemonics.MnemonicCode
|
||||
import im.molly.monero.mnemonics.MoneroMnemonic
|
||||
import im.molly.monero.service.BaseWalletService
|
||||
import im.molly.monero.service.InProcessWalletService
|
||||
import im.molly.monero.service.SandboxedWalletService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@LargeTest
|
||||
//@RunWith(Parameterized::class)
|
||||
abstract class MoneroWalletTest(private val serviceClass: Class<out BaseWalletService>) {
|
||||
|
||||
@get:Rule
|
||||
val walletServiceRule = ServiceTestRule()
|
||||
|
||||
private lateinit var walletProvider: WalletProvider
|
||||
|
||||
private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
|
||||
private fun bindService(): IWalletService {
|
||||
val binder = walletServiceRule.bindService(Intent(context, serviceClass))
|
||||
return IWalletService.Stub.asInterface(binder)
|
||||
}
|
||||
|
||||
private fun unbindService() {
|
||||
walletServiceRule.unbindService()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val walletService = bindService()
|
||||
walletProvider = WalletServiceClient.withBoundService(context, walletService)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
walletProvider.disconnect()
|
||||
unbindService()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restoredWalletHasExpectedAddress() = runTest {
|
||||
val key = SecretKey("148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e".hexToByteArray())
|
||||
val wallet = walletProvider.restoreWallet(
|
||||
network = MoneroNetwork.Mainnet,
|
||||
secretSpendKey = key,
|
||||
restorePoint = RestorePoint.Genesis,
|
||||
)
|
||||
assertThat(wallet.publicAddress.address)
|
||||
.isEqualTo("42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveToMultipleStores() = runTest {
|
||||
val defaultStore = InMemoryWalletDataStore()
|
||||
val wallet = walletProvider.createNewWallet(MoneroNetwork.Mainnet, defaultStore)
|
||||
wallet.save()
|
||||
|
||||
val newStore = InMemoryWalletDataStore()
|
||||
wallet.save(newStore)
|
||||
|
||||
assertThat(defaultStore.toByteArray()).isEqualTo(newStore.toByteArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateAccountAndSave() = runTest {
|
||||
val wallet = walletProvider.createNewWallet(MoneroNetwork.Mainnet)
|
||||
val newAccount = wallet.createAccount()
|
||||
|
||||
withReopenedWallet(wallet) { original, reopened ->
|
||||
MoneroWalletSubject.assertThat(reopened).matchesStateOf(original)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun withReopenedWallet(
|
||||
wallet: MoneroWallet,
|
||||
action: suspend (original: MoneroWallet, reopened: MoneroWallet) -> Unit,
|
||||
) {
|
||||
walletProvider.openWallet(
|
||||
network = wallet.network,
|
||||
dataStore = InMemoryWalletDataStore().apply {
|
||||
wallet.save(targetStore = this)
|
||||
},
|
||||
).use { reopened ->
|
||||
action(wallet, reopened)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MoneroWalletInProcessTest : MoneroWalletTest(InProcessWalletService::class.java)
|
||||
class MoneroWalletSandboxedTest : MoneroWalletTest(SandboxedWalletService::class.java)
|
|
@ -0,0 +1,116 @@
|
|||
package im.molly.monero.internal
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.WalletDataStore
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
|
||||
class DataStoreAdapterTest {
|
||||
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
private val dataStore = mockk<WalletDataStore>(relaxed = true)
|
||||
private val testDispatcher: TestDispatcher = StandardTestDispatcher()
|
||||
private val testIOException = IOException("Test IO Exception")
|
||||
|
||||
private lateinit var adapter: DataStoreAdapter
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
adapter = DataStoreAdapter(dataStore, ioDispatcher = testDispatcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overwriteIsPassedToDataStore() = runTest(testDispatcher) {
|
||||
coEvery { dataStore.save(any(), any()) } returns Unit
|
||||
|
||||
listOf(true, false).forEach {
|
||||
adapter.saveWithFd(overwrite = it) {}
|
||||
coVerify(exactly = 1) { dataStore.save(any(), overwrite = it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propagatesIOExceptionWhenLoadFails() = runTest(testDispatcher) {
|
||||
coEvery { dataStore.load() } throws testIOException
|
||||
|
||||
val exception = runCatching {
|
||||
adapter.loadWithFd {}
|
||||
}.exceptionOrNull()
|
||||
|
||||
assertThat(exception).isEqualTo(testIOException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propagatesIOExceptionWhenSaveFails() = runTest(testDispatcher) {
|
||||
coEvery { dataStore.save(any(), any()) } throws testIOException
|
||||
|
||||
val exception = runCatching {
|
||||
adapter.saveWithFd(overwrite = true) {}
|
||||
}.exceptionOrNull()
|
||||
|
||||
assertThat(exception).isEqualTo(testIOException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pipeIsAlwaysClosedAfterLoad() = runTest(testDispatcher) {
|
||||
val (readFd, writeFd) = mockPipe()
|
||||
|
||||
coEvery { dataStore.load() } returns ByteArrayInputStream(byteArrayOf(1, 2, 3))
|
||||
|
||||
adapter.loadWithFd({})
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
|
||||
clearAllMocks(answers = false)
|
||||
|
||||
runCatching {
|
||||
adapter.loadWithFd({ throw RuntimeException() })
|
||||
}
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pipeIsAlwaysClosedAfterSave() = runTest(testDispatcher) {
|
||||
val (readFd, writeFd) = mockPipe()
|
||||
|
||||
coEvery { dataStore.save(any(), any()) } returns Unit
|
||||
|
||||
adapter.saveWithFd(overwrite = true, {})
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
|
||||
clearAllMocks(answers = false)
|
||||
|
||||
runCatching {
|
||||
adapter.saveWithFd(overwrite = true, { throw RuntimeException() })
|
||||
}
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
}
|
||||
|
||||
private fun mockPipe(): Pair<ParcelFileDescriptor, ParcelFileDescriptor> {
|
||||
val readFd = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
val writeFd = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
|
||||
mockkStatic(ParcelFileDescriptor::class)
|
||||
coEvery { ParcelFileDescriptor.createPipe() } returns arrayOf(readFd, writeFd)
|
||||
|
||||
return readFd to writeFd
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ class NativeWalletTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun atGenesisBalanceIsZero() = runTest {
|
||||
fun balanceIsZeroAtGenesis() = runTest {
|
||||
with(
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
|
|
|
@ -20,7 +20,7 @@ class MoneroMnemonicTest {
|
|||
)
|
||||
|
||||
@Test
|
||||
fun testKnownMnemonics() {
|
||||
fun knownMnemonics() {
|
||||
testCases.forEach {
|
||||
validateMnemonicGeneration(it)
|
||||
validateEntropyRecovery(it)
|
||||
|
@ -28,22 +28,22 @@ class MoneroMnemonicTest {
|
|||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testEmptyEntropy() {
|
||||
fun emptyEntropy() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(0))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testInvalidEntropy() {
|
||||
fun invalidEntropy() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(2))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testEmptyWords() {
|
||||
fun emptyWords() {
|
||||
MoneroMnemonic.recoverEntropy("")
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testInvalidLanguage() {
|
||||
fun invalidLanguage() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(32), Locale("ZZ"))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package im.molly.monero.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class WalletServiceSandboxingTest {
|
||||
|
||||
private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
|
||||
@Test
|
||||
fun inProcessWalletServiceIsNotIsolated() = runTest {
|
||||
InProcessWalletService.connect(context).use { walletProvider ->
|
||||
assertThat(walletProvider.isServiceIsolated()).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sandboxedWalletServiceIsIsolated() = runTest {
|
||||
SandboxedWalletService.connect(context).use { walletProvider ->
|
||||
assertThat(walletProvider.isServiceIsolated()).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ interface WalletProvider : Closeable {
|
|||
client: MoneroNodeClient? = null,
|
||||
): MoneroWallet
|
||||
|
||||
fun isServiceIsolated(): Boolean
|
||||
|
||||
fun disconnect()
|
||||
|
||||
override fun close() {
|
||||
|
|
|
@ -146,6 +146,8 @@ internal class WalletServiceClient(
|
|||
}
|
||||
}
|
||||
|
||||
override fun isServiceIsolated(): Boolean = service.isRemote()
|
||||
|
||||
override fun disconnect() {
|
||||
context.unbindService(serviceConnection ?: return)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package im.molly.monero
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.util.decodeBase58
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class PublicAddressParsingTest(
|
||||
private val expectedNetwork: MoneroNetwork,
|
||||
private val isSubAddress: Boolean,
|
||||
private val address: String,
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): List<Array<Any>> = listOf(
|
||||
arrayOf(MoneroNetwork.Mainnet, false, "42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm"),
|
||||
arrayOf(MoneroNetwork.Mainnet, false, "44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW"),
|
||||
arrayOf(MoneroNetwork.Testnet, false, "9ujeXrjzf7bfeK3KZdCqnYaMwZVFuXemPU8Ubw335rj2FN1CdMiWNyFV3ksEfMFvRp9L9qum5UxkP5rN9aLcPxbH1au4WAB"),
|
||||
arrayOf(MoneroNetwork.Stagenet, false, "53teqCAESLxeJ1REzGMAat1ZeHvuajvDiXqboEocPaDRRmqWoVPzy46GLo866qRFjbNhfkNckyhST3WEvBviDwpUDd7DSzB"),
|
||||
arrayOf(MoneroNetwork.Mainnet, true, "8AsN91rznfkBGTY8psSNkJBg9SZgxxGGRUhGwRptBhgr5XSQ1XzmA9m8QAnoxydecSh5aLJXdrgXwTDMMZ1AuXsN1EX5Mtm"),
|
||||
arrayOf(MoneroNetwork.Mainnet, true, "86kKnBKFqzCLxtK1Jmx2BkNBDBSMDEVaRYMMyVbeURYDWs8uNGDZURKCA5yRcyMxHzPcmCf1q2fSdhQVcaKsFrtGRsdGfNk"),
|
||||
arrayOf(MoneroNetwork.Testnet, true,"BdKg9udkvckC5T58a8Nmtb6BNsgRAxs7uA2D49sWNNX5HPW5Us6Wxu8QMXrnSx3xPBQQ2iu9kwEcRGAoiz6EPmcZKbF62GS"),
|
||||
arrayOf(MoneroNetwork.Testnet, true, "BcFvPa3fT4gVt5QyRDe5Vv7VtUFao9ci8NFEy3r254KF7R1N2cNB5FYhGvrHbMStv4D6VDzZ5xtxeKV8vgEPMnDcNFuwZb9"),
|
||||
arrayOf(MoneroNetwork.Stagenet, true, "73LhUiix4DVFMcKhsPRG51QmCsv8dYYbL6GcQoLwEEFvPvkVvc7BhebfA4pnEFF9Lq66hwvLqBvpHjTcqvpJMHmmNjPPBqa"),
|
||||
arrayOf(MoneroNetwork.Stagenet, true, "7A1Hr63MfgUa8pkWxueD5xBqhQczkusYiCMYMnJGcGmuQxa7aDBxN1G7iCuLCNB3VPeb2TW7U9FdxB27xKkWKfJ8VhUZthF"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse determines correct type and network`() {
|
||||
val parsed = PublicAddress.parse(address)
|
||||
|
||||
assertThat(parsed.address).isEqualTo(address)
|
||||
assertThat(parsed.toString()).isEqualTo(address)
|
||||
assertThat(parsed.network).isEqualTo(expectedNetwork)
|
||||
assertThat(parsed.isSubAddress()).isEqualTo(isSubAddress)
|
||||
|
||||
when (parsed) {
|
||||
is SubAddress -> assertThat(isSubAddress).isTrue()
|
||||
is StandardAddress -> assertThat(isSubAddress).isFalse()
|
||||
else -> error("Unexpected address type: ${parsed::class}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PublicAddressExceptionTest {
|
||||
|
||||
@Test
|
||||
fun `throws InvalidAddress on unknown prefix`() {
|
||||
mockkStatic("im.molly.monero.util.Base58Kt")
|
||||
|
||||
val unknownPrefix = "unknownprefix"
|
||||
every { unknownPrefix.decodeBase58() } returns byteArrayOf(99, 1, 2, 3, 4, 5)
|
||||
|
||||
val thrown = Assert.assertThrows(InvalidAddress::class.java) {
|
||||
PublicAddress.parse(unknownPrefix)
|
||||
}
|
||||
assertThat(thrown.message).contains("Unrecognized address prefix")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws InvalidAddress when address is too short`() {
|
||||
val thrown = Assert.assertThrows(InvalidAddress::class.java) {
|
||||
PublicAddress.parse("111")
|
||||
}
|
||||
assertThat(thrown.message).contains("Address too short")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws InvalidAddress on decoding error`() {
|
||||
val thrown = Assert.assertThrows(InvalidAddress::class.java) {
|
||||
PublicAddress.parse("zz")
|
||||
}
|
||||
assertThat(thrown.message).contains("Base58 decoding error")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue