Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-22 16:33:40 +04:00
parent 603f522cf9
commit 86d8877629
6 changed files with 315 additions and 4 deletions

View file

@ -4,6 +4,10 @@ plugins {
id("configuration")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
// region DI
@ -18,4 +22,9 @@ dependencies {
// region Time dependencies
implementation(deps.jodatime)
// endregion
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -0,0 +1,115 @@
package com.tangem.utils.extensions
import com.google.common.truth.Truth
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ByteArrayExtTest {
@Test
fun `toHexString converts byte array to hexadecimal string`() {
// Arrange
val byteArray = byteArrayOf(0x0F, 0xA5.toByte(), 0xFF.toByte())
// Act
val actual = byteArray.toHexString()
// Assert
val expected = "0FA5FF"
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `hexToBytes converts hexadecimal string to byte array`() {
// Arrange
val hexString = "0FA5FF"
// Act
val actual = hexString.hexToBytes()
// Assert
val expected = byteArrayOf(0x0F, 0xA5.toByte(), 0xFF.toByte())
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `hexToBytes handles hex string with prefix`() {
// Arrange
val hexString = "0x0FA5FF"
// Act
val actual = hexString.hexToBytes()
// Assert
val expected = byteArrayOf(0x0F, 0xA5.toByte(), 0xFF.toByte())
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `hexToBytes throws exception for invalid hex string`() {
// Arrange
val invalidHexString = "GHIJKL"
// Act
val actual = runCatching { invalidHexString.hexToBytes() }.exceptionOrNull()
// Assert
val expected = NumberFormatException()
Truth.assertThat(actual).isInstanceOf(expected::class.java)
Truth.assertThat(actual).hasMessageThat().isEqualTo("For input string: \"GH\" under radix 16")
}
@Test
fun `hexToBytes handles empty string`() {
// Arrange
val emptyHexString = ""
// Act
val actual = emptyHexString.hexToBytes()
// Assert
Truth.assertThat(actual).isEqualTo(byteArrayOf())
}
@Test
fun `hexToBytesOrNull returns byte array for valid hex string`() {
// Arrange
val hexString = "0FA5FF"
// Act
val actual = hexString.hexToBytesOrNull()
// Assert
val expected = byteArrayOf(0x0F, 0xA5.toByte(), 0xFF.toByte())
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `hexToBytesOrNull returns null for invalid hex string`() {
// Arrange
val invalidHexString = "GHIJKL"
// Act
val actual = invalidHexString.hexToBytesOrNull()
// Assert
Truth.assertThat(actual).isNull()
}
@Test
fun hexToBytesOrNullHandlesEmptyString() {
// Arrange
val emptyHexString = ""
// Act
val actual = emptyHexString.hexToBytesOrNull()
// Assert
val expected = byteArrayOf()
Truth.assertThat(actual).isEqualTo(expected)
}
}

View file

@ -7,6 +7,10 @@ plugins {
id("configuration")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
api(projects.domain.visa.models)
api(projects.core.utils)
@ -17,4 +21,9 @@ dependencies {
implementation(deps.kotlin.serialization)
ksp(deps.moshi.kotlin.codegen)
implementation(deps.arrow.core)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -0,0 +1,181 @@
package com.tangem.domain.models.account
import com.google.common.truth.Truth
import com.tangem.domain.models.account.CryptoPortfolioIcon.*
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import kotlin.random.Random
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CryptoPortfolioIconTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class OfMainAccount {
@Test
fun `ofMainAccount with empty exclude`() {
// Act
val actual = CryptoPortfolioIcon.ofMainAccount(exclude = emptySet())
// Assert
val expectedColor = Color.Azure
Truth.assertThat(actual.color).isEqualTo(expectedColor)
val expectedType = Type.Icon(value = Icon.Star)
Truth.assertThat(actual.type).isEqualTo(expectedType)
}
@ParameterizedTest
@MethodSource("provideTestModels")
fun ofMainAccount(model: OfMainAccountModel) {
// Arrange
mockkObject(Random.Default)
val size = (Color.entries.size - model.exclude.size).takeIf { it > 0 } ?: Color.entries.size
every { Random.nextInt(size) } returns model.randomNextInt
// Act
val actual = CryptoPortfolioIcon.ofMainAccount(exclude = model.exclude)
// Assert
val expectedColor = model.expectedColor
Truth.assertThat(actual.color).isEqualTo(expectedColor)
val expectedType = Type.Icon(value = Icon.Star)
Truth.assertThat(actual.type).isEqualTo(expectedType)
verify(exactly = 1) { Random.nextInt(size) }
unmockkObject(Random.Default)
}
private fun provideTestModels() = listOf(
// If the default color is already occupied (present in the exclude set), a random color from the
// remaining available colors will be selected for the main account icon.
OfMainAccountModel(
exclude = setOf(Color.Azure),
randomNextInt = 0,
expectedColor = Color.entries[1],
),
OfMainAccountModel(
exclude = setOf(Color.Azure, Color.CaribbeanBlue),
randomNextInt = 0,
expectedColor = Color.entries[2],
),
// If all colors are already occupied, a random one will be selected.
OfMainAccountModel(
exclude = Color.entries.toSet(),
randomNextInt = 1,
expectedColor = Color.entries[1],
),
)
}
data class OfMainAccountModel(
val exclude: Set<Color>,
val randomNextInt: Int,
val expectedColor: Color,
)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class OfCustomAccountBasedOnName {
@ParameterizedTest
@MethodSource("provideTestModels")
fun ofCustomAccount(model: OfCustomAccountModel.BasedOnName) {
// Arrange
mockkObject(Random.Default)
every { Random.nextInt(until = Color.entries.size) } returns model.randomNextInt
// Act
val actual = CryptoPortfolioIcon.ofCustomAccount(accountName = model.accountName)
// Assert
val expectedColor = model.expectedColor
Truth.assertThat(actual.color).isEqualTo(expectedColor)
val expectedType = Type.Symbol(value = model.accountName.first())
Truth.assertThat(actual.type).isEqualTo(expectedType)
verify(exactly = 1) { Random.nextInt(until = Color.entries.size) }
unmockkObject(Random.Default)
}
private fun provideTestModels() = listOf(
OfCustomAccountModel.BasedOnName(
accountName = "New account",
randomNextInt = 0,
expectedColor = Color.entries[0],
),
OfCustomAccountModel.BasedOnName(
accountName = "Awesome",
randomNextInt = Color.entries.lastIndex,
expectedColor = Color.entries.last(),
),
)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class OfCustomAccountWithTypeAndColor {
@ParameterizedTest
@MethodSource("provideTestModels")
fun ofCustomAccount(model: OfCustomAccountModel.WithTypeAndColor) {
// Act
val actual = CryptoPortfolioIcon.ofCustomAccount(type = model.type, color = model.color)
// Assert
val expectedColor = model.expectedColor
Truth.assertThat(actual.color).isEqualTo(expectedColor)
val expectedType = model.expectedType
Truth.assertThat(actual.type).isEqualTo(expectedType)
}
private fun provideTestModels() = listOf(
OfCustomAccountModel.WithTypeAndColor(
type = Type.Icon(value = Icon.User),
color = Color.CaribbeanBlue,
expectedType = Type.Icon(value = Icon.User),
expectedColor = Color.CaribbeanBlue,
),
OfCustomAccountModel.WithTypeAndColor(
type = Type.Symbol(value = 'A'),
color = Color.DullLavender,
expectedType = Type.Symbol(value = 'A'),
expectedColor = Color.DullLavender,
),
)
}
sealed interface OfCustomAccountModel {
data class BasedOnName(
val accountName: String,
val randomNextInt: Int,
val expectedColor: Color,
) : OfCustomAccountModel
data class WithTypeAndColor(
val type: Type,
val color: Color,
val expectedType: Type,
val expectedColor: Color,
) : OfCustomAccountModel
}
}

View file

@ -5,8 +5,8 @@ import com.ihsanbal.logging.LoggingInterceptor
import com.tangem.lib.visa.model.VisaContractInfo
import com.tangem.lib.visa.utils.Constants
import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG
import com.tangem.lib.visa.utils.toHexString
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.toHexString
import okhttp3.OkHttpClient
import org.web3j.crypto.Credentials
import org.web3j.protocol.Web3j

View file

@ -1,3 +0,0 @@
package com.tangem.lib.visa.utils
internal fun ByteArray.toHexString(): String = joinToString("") { "%02X".format(it) }