Skip to content

Commit

Permalink
Merge branch 'dev' into SUITEDEV-35599-MultiID-for-predict
Browse files Browse the repository at this point in the history
  • Loading branch information
megamegax committed Feb 13, 2025
2 parents 5e88793 + 3ac4763 commit cbffb02
Show file tree
Hide file tree
Showing 54 changed files with 820 additions and 787 deletions.
16 changes: 14 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# What's fixed

### [Emarsys SDK](https://github.com/emartech/android-emarsys-sdk)
### [SetPushToken](https://github.com/emartech/android-emarsys-sdk/wiki#21-setpushtoken)

* Fix a crash on Android 12 during the initialization of the SDK
* Fix an issue where the push token was not tracked automatically by the SDK.

* ### [Inapp](https://github.com/emartech/android-emarsys-sdk/wiki#3-inapp)

* Fix an issue where the content of the inapp message was not reloaded when the activity was recreated from a destroyed state, or after an orientation change.

### [Emarsys SDK](https://github.com/emartech/android-emarsys-sdk/wiki)

* Fix an issue where on certain devices encryption-decryption failed due to a KeyStore error.

### [Emarsys SDK](https://github.com/emartech/android-emarsys-sdk/wiki)

* Fix an issue where on initialization the current activity was null and caused a crash.
4 changes: 2 additions & 2 deletions core/src/androidTest/java/com/emarsys/core/DeviceInfoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ class DeviceInfoTest {
val resources = getTargetContext().resources
Locale.setDefault(locale)
val config = resources.configuration
config.locale = locale
resources.updateConfiguration(config, resources.displayMetrics)
config.setLocale(locale)
context.createConfigurationContext(config)
Locale.setDefault(previous)
"+0900" shouldBe deviceInfo.timezone
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import android.security.keystore.KeyGenParameterSpec
import android.util.Base64
import com.emarsys.core.crypto.SharedPreferenceCrypto
package com.emarsys.core.crypto

import android.security.keystore.KeyProperties
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
Expand All @@ -17,146 +14,119 @@ import java.security.GeneralSecurityException
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class SharedPreferenceCryptoTest {
private companion object {
const val encryptedBase64 = "Base64EncryptedBase64IV123123"
class SharedPreferenceCryptoTest {
private lateinit var keyStore: KeyStore

@Before
fun setup() {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore.deleteEntry("emarsys_sdk_key_shared_pref_key_v3")
}

private lateinit var sharedPreferenceCrypto: SharedPreferenceCrypto
private lateinit var mockKeyStore: KeyStore
private lateinit var mockKeyGenerator: KeyGenerator
private lateinit var mockSecretKey: SecretKey
private lateinit var mockCipher: Cipher
@After
fun tearDown() {
unmockkAll()
}

@Before
fun setup() {
mockkStatic(KeyStore::class)
@Test
fun init_shouldGenerateKey_ifNotPresent_inKeyStore() {
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
mockkStatic(Base64::class)

mockKeyStore = mockk()
mockKeyGenerator = mockk()
mockSecretKey = mockk()
mockCipher = mockk()
SharedPreferenceCrypto()

every { KeyStore.getInstance(any()) } returns mockKeyStore
every { KeyGenerator.getInstance(any(), any<String>()) } returns mockKeyGenerator
every { Cipher.getInstance(any()) } returns mockCipher

sharedPreferenceCrypto = SharedPreferenceCrypto()
verify { KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) }
}

@After
fun tearDown() {
unmockkAll()
@Test
fun init_shouldNotGenerateKey_ifPresent_inKeyStore() {
mockkStatic(KeyGenerator::class)

SharedPreferenceCrypto()
SharedPreferenceCrypto()

verify(exactly = 1) { KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) }
}

@Test
fun testGetOrCreateSecretKey_KeyExists() {
every { mockKeyStore.load(null) } just Runs
every { mockKeyStore.containsAlias(any()) } returns true
every { mockKeyStore.getKey(any(), null) } returns mockSecretKey
fun encrypt_decrypt_shouldWork() {
val testValue = "testValue"

val result = sharedPreferenceCrypto.getOrCreateSecretKey()
val testCrypto = SharedPreferenceCrypto()
val encrypted = testCrypto.encrypt(testValue)

result shouldBe mockSecretKey
verify { mockKeyStore.getKey(any(), null) }
testCrypto.decrypt(encrypted) shouldBe testValue
}

@Test
fun testGetOrCreateSecretKey_KeyDoesNotExist() {
every { mockKeyStore.load(null) } just Runs
every { mockKeyStore.containsAlias(any()) } returns false
every { mockKeyGenerator.init(any<KeyGenParameterSpec>()) } just Runs
every { mockKeyGenerator.generateKey() } returns mockSecretKey
fun encrypt_shouldGenerateNewSecretKey_andRetryEncrypting_once_andReturnEncryptedValue_ifSucceeds() {
val testValue = "testValue"
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws GeneralSecurityException("Test exception") andThenAnswer { callOriginal() }

val testCrypto = SharedPreferenceCrypto()

val result = sharedPreferenceCrypto.getOrCreateSecretKey()
val result = testCrypto.encrypt(testValue)

result shouldBe mockSecretKey
verify { mockKeyGenerator.generateKey() }
verify(exactly = 2) { KeyGenerator.getInstance(any()) }

result shouldNotBe testValue
}

@Test
fun testEncrypt_Success() {
val value = "test_value"
val encryptedBytes = byteArrayOf(1, 2, 3, 4)
val iv = byteArrayOf(5, 6, 7, 8)
fun encrypt_shouldGenerateNewSecretKey_andRetryEncrypting_once_andReturnInitialValue_ifFails() {
val testValue = "testValue"
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws GeneralSecurityException("Test exception")

every { mockCipher.init(Cipher.ENCRYPT_MODE, mockSecretKey) } just Runs
every { mockCipher.doFinal(any<ByteArray>()) } returns encryptedBytes
every { mockCipher.iv } returns iv
every { Base64.encodeToString(any(), Base64.DEFAULT) } returns "encodedString"
val testCrypto = SharedPreferenceCrypto()

val result = sharedPreferenceCrypto.encrypt(value, mockSecretKey)
val result = testCrypto.encrypt(testValue)

result shouldNotBe value
result shouldBe "encodedStringencodedString"
verify(exactly = 2) { KeyGenerator.getInstance(any()) }
result shouldBe testValue
}

@Test
fun testEncrypt_Exception() {
val value = "test_value"
fun decrypt_shouldReturn_null_andGenerateNewSecretKey_ifGeneralSecurityExceptionHappens() {
val testValue = "dGVzdFZhbHVlU2hvdWxkQmVTaXh0ZWVuQ2hhcnNMb25n"
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws GeneralSecurityException("Test exception")

val testCrypto = SharedPreferenceCrypto()

every {
mockCipher.init(
Cipher.ENCRYPT_MODE,
mockSecretKey
)
} throws GeneralSecurityException("Encryption failed")
testCrypto.decrypt(testValue) shouldBe null

verify { KeyGenerator.getInstance(any()) }
}

val result = sharedPreferenceCrypto.encrypt(value, mockSecretKey)
@Test
fun decrypt_shouldReturn_encryptedValue_ifIllegalArgumentException_withBase64ErrorHappens() {
val testValue = "testValueShouldBeSixteenCharsLong"

result shouldBe value
val testCrypto = SharedPreferenceCrypto()
testCrypto.decrypt(testValue) shouldBe testValue
}

@Test
fun testDecrypt_Success() {
val ivBytes = byteArrayOf(1, 2, 3, 4)
val encryptedBytes = byteArrayOf(5, 6, 7, 8)
val decryptedBytes = "decrypted".toByteArray()

every { Base64.decode(any<String>(), Base64.DEFAULT) } returnsMany listOf(
ivBytes,
encryptedBytes
)
every {
mockCipher.init(
Cipher.DECRYPT_MODE,
mockSecretKey,
any<GCMParameterSpec>()
)
} just Runs
every { mockCipher.doFinal(encryptedBytes) } returns decryptedBytes

val result = sharedPreferenceCrypto.decrypt(encryptedBase64, mockSecretKey)

result shouldBe "decrypted"
fun decrypt_shouldReturn_null_ifIllegalArgumentExceptionHappens() {
val testValue = "dGVzdFZhbHVlU2hvdWxkQmVTaXh0ZWVuQ2hhcnNMb25n"
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws IllegalArgumentException("Test exception")

val testCrypto = SharedPreferenceCrypto()
testCrypto.decrypt(testValue) shouldBe null
}

@Test
fun testDecrypt_Exception() {
val IVValue = "Base64EncryptedBase64IV123"
val decryptedBytes = encryptedBase64.toByteArray()
every {
mockCipher.init(any(), mockSecretKey, any<GCMParameterSpec>())
} just Runs
every {
mockCipher.doFinal(any())
} returns decryptedBytes
every {
Base64.decode(
IVValue,
Base64.DEFAULT
)
} throws GeneralSecurityException("Decryption failed")

val result = sharedPreferenceCrypto.decrypt(encryptedBase64, mockSecretKey)

result shouldBe encryptedBase64
fun decrypt_shouldReturn_null_ifExceptionHappens() {
val testValue = "testValue"

val testCrypto = SharedPreferenceCrypto()
testCrypto.decrypt(testValue) shouldBe null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ class EmarsysEncryptedSharedPreferencesV3Test {
@Before
fun setup() {
mockContext = mockk()
mockSharedPreferenceCrypto = mockk()
mockSharedPreferenceCrypto = mockk(relaxed = true)
mockRealPreferences = mockk(relaxed = true)
mockSecretKey = mockk()

every { mockContext.getSharedPreferences(any(), any()) } returns mockRealPreferences
every { mockSharedPreferenceCrypto.getOrCreateSecretKey() } returns mockSecretKey
mockInternalEditor = mockk<SharedPreferences.Editor>(relaxed = true)

every { mockRealPreferences.edit() } returns mockInternalEditor
every { mockSharedPreferenceCrypto.encrypt(any(), any()) } returns "encryptedValue"
every { mockSharedPreferenceCrypto.encrypt(any()) } returns "encryptedValue"

emarsysEncryptedSharedPreferencesV3 = EmarsysEncryptedSharedPreferencesV3(
mockContext,
Expand All @@ -54,7 +53,6 @@ class EmarsysEncryptedSharedPreferencesV3Test {
every { mockRealPreferences.all } returns encryptedMap
every {
mockSharedPreferenceCrypto.decrypt(
any(),
any()
)
} returnsMany listOf("decryptedValue1", "decryptedValue2", "decryptedValue3")
Expand All @@ -76,8 +74,7 @@ class EmarsysEncryptedSharedPreferencesV3Test {
every { mockRealPreferences.getString("testKey", null) } returns "encryptedValue"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue",
mockSecretKey
"encryptedValue"
)
} returns "decryptedValue"

Expand All @@ -86,20 +83,32 @@ class EmarsysEncryptedSharedPreferencesV3Test {
result shouldBe "decryptedValue"
}

@Test
fun testGetString_shouldReturnDefaultValue_whenNull() {
every { mockRealPreferences.getString("testKey", null) } returns "encryptedValue"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue"
)
} returns null

val result = emarsysEncryptedSharedPreferencesV3.getString("testKey", "defaultValue")

result shouldBe "defaultValue"
}

@Test
fun testGetStringSet() {
val encryptedSet = setOf("encryptedValue1", "encryptedValue2")
every { mockRealPreferences.getStringSet("testKey", null) } returns encryptedSet
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue1",
mockSecretKey
"encryptedValue1"
)
} returns "decryptedValue1"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue2",
mockSecretKey
"encryptedValue2"
)
} returns "decryptedValue2"

Expand Down Expand Up @@ -181,9 +190,9 @@ class EmarsysEncryptedSharedPreferencesV3Test {

editor.commit()

verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("testValue", mockSecretKey) }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value1", mockSecretKey) }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value2", mockSecretKey) }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("testValue") }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value1") }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value2") }

verify(exactly = 1) { mockInternalEditor.putString("testKey", "encryptedValue") }
verify(exactly = 1) { mockInternalEditor.putInt("testIntKey", 42) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.emarsys.core.storage

import android.content.Context
import android.content.SharedPreferences
import com.emarsys.core.crypto.SharedPreferenceCrypto
import com.emarsys.core.storage.EmarsysEncryptedSharedPreferencesV3
import com.emarsys.core.storage.EncryptedSharedPreferencesToSharedPreferencesMigration
import com.emarsys.core.storage.SharedPreferencesV3Provider
import com.emarsys.testUtil.ReflectionTestUtils
import io.kotest.matchers.shouldBe
import io.mockk.Runs
Expand All @@ -14,7 +13,7 @@ import io.mockk.verify
import org.junit.Before
import org.junit.Test

class SharedPreferencesV3ProviderTest {
class SharedPreferencesV3ProviderTest {

private lateinit var mockContext: Context
private lateinit var mockOldSharedPreferences: SharedPreferences
Expand All @@ -36,14 +35,13 @@ class SharedPreferencesV3ProviderTest {
any()
)
} returns mockRealSharedPrefs
every { mockCrypto.getOrCreateSecretKey() } returns mockk()
every { mockMigration.migrate(any(), any()) } just Runs

}

@Test
fun testInitialization() {
val provider = SharedPreferencesV3Provider(
SharedPreferencesV3Provider(
mockContext,
"test_file",
mockOldSharedPreferences,
Expand Down
Loading

0 comments on commit cbffb02

Please sign in to comment.