-
Notifications
You must be signed in to change notification settings - Fork 0
π§ͺ [testing improvement] Add unit tests for SessionStore #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| package com.clhs.score.data | ||
|
|
||
| import android.content.Context | ||
| import android.util.Base64 | ||
| import androidx.test.core.app.ApplicationProvider | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.assertNull | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Assert.assertFalse | ||
| import org.junit.Assert.assertNotNull | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import org.junit.Before | ||
| import org.robolectric.annotation.Config | ||
| import org.robolectric.annotation.Implementation | ||
| import org.robolectric.annotation.Implements | ||
| import javax.crypto.Cipher | ||
| import javax.crypto.KeyGenerator | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| @Config(sdk = [34], shadows = [ShadowMasterKey::class, ShadowEncryptedSharedPreferences::class, ShadowBiometricHelper::class]) | ||
| class SessionStoreTest { | ||
|
|
||
| @Before | ||
| fun setup() { | ||
| System.setProperty("javax.net.ssl.trustStoreType", "JKS") | ||
| } | ||
|
|
||
| @Test | ||
| fun testSaveAndLoadSession() { | ||
| val context = ApplicationProvider.getApplicationContext<Context>() | ||
| val store = SessionStore(context) | ||
|
|
||
| val session = AuthenticatedSession("123", "token", mapOf("cookie1" to "val1")) | ||
| store.saveSession(session) | ||
|
|
||
| val loaded = store.loadSession() | ||
| assertEquals(session, loaded) | ||
| } | ||
|
|
||
| @Test | ||
| fun testClearSession() { | ||
| val context = ApplicationProvider.getApplicationContext<Context>() | ||
| val store = SessionStore(context) | ||
|
|
||
| val session = AuthenticatedSession("123", "token", mapOf("cookie1" to "val1")) | ||
| store.saveSession(session) | ||
|
|
||
| store.clearNormalSession() | ||
| assertNull(store.loadSession()) | ||
| } | ||
|
|
||
| @Test | ||
| fun testReminderSession() { | ||
| val context = ApplicationProvider.getApplicationContext<Context>() | ||
| val store = SessionStore(context) | ||
|
|
||
| val session = AuthenticatedSession("123", "token", mapOf("cookie1" to "val1")) | ||
| store.saveReminderSession(session, 1000) | ||
|
|
||
| val loaded = store.loadReminderSession(500) | ||
| assertEquals(session, loaded) | ||
|
|
||
| val expired = store.loadReminderSession(2000) | ||
| assertNull(expired) | ||
| assertNull(store.loadReminderSession(500)) | ||
| } | ||
|
|
||
| @Test | ||
| fun testClearAll() { | ||
| val context = ApplicationProvider.getApplicationContext<Context>() | ||
| val store = SessionStore(context) | ||
|
|
||
| val session = AuthenticatedSession("123", "token", mapOf("cookie1" to "val1")) | ||
| store.saveSession(session) | ||
| store.saveReminderSession(session, 1000) | ||
|
|
||
| store.clear() | ||
|
|
||
| assertNull(store.loadSession()) | ||
| assertNull(store.loadReminderSession(500)) | ||
| } | ||
|
|
||
|
Comment on lines
+71
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The @Test
fun testClearAll() {
val context = ApplicationProvider.getApplicationContext<Context>()
val store = SessionStore(context)
val session = AuthenticatedSession("123", "token", mapOf("cookie1" to "val1"))
store.saveSession(session)
store.saveReminderSession(session, 1000)
val keyGen = KeyGenerator.getInstance("AES")
keyGen.init(256)
val key = keyGen.generateKey()
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
store.saveBiometricSession(session, "1234", cipher)
store.clear()
assertNull(store.loadSession())
assertNull(store.loadReminderSession(500))
assertFalse(store.hasBiometricSession())
} |
||
| @Test | ||
| fun testBiometricSession() { | ||
| val context = ApplicationProvider.getApplicationContext<Context>() | ||
| val store = SessionStore(context) | ||
|
|
||
| val session = AuthenticatedSession("123", "token", mapOf("cookie1" to "val1")) | ||
| val keyGen = KeyGenerator.getInstance("AES") | ||
| keyGen.init(256) | ||
| val key = keyGen.generateKey() | ||
| val cipher = Cipher.getInstance("AES/GCM/NoPadding") | ||
| cipher.init(Cipher.ENCRYPT_MODE, key) | ||
|
|
||
| store.saveBiometricSession(session, "1234", cipher) | ||
|
|
||
| assertTrue(store.hasBiometricSession()) | ||
| assertNotNull(store.getBiometricIv()) | ||
|
|
||
| val cipherDecrypt = Cipher.getInstance("AES/GCM/NoPadding") | ||
| cipherDecrypt.init(Cipher.DECRYPT_MODE, key, cipher.parameters) | ||
|
|
||
| val loaded = store.loadBiometricSession(cipherDecrypt) | ||
| assertEquals(session, loaded) | ||
|
|
||
| store.clearBiometricSession() | ||
| assertFalse(store.hasBiometricSession()) | ||
| } | ||
| } | ||
|
|
||
| @Implements(androidx.security.crypto.MasterKey.Builder::class) | ||
| class ShadowMasterKey { | ||
| @Implementation | ||
| fun build(): androidx.security.crypto.MasterKey { | ||
| return org.mockito.Mockito.mock(androidx.security.crypto.MasterKey::class.java) | ||
| } | ||
| } | ||
|
|
||
| @Implements(androidx.security.crypto.EncryptedSharedPreferences::class) | ||
| class ShadowEncryptedSharedPreferences { | ||
| companion object { | ||
| @JvmStatic | ||
| @Implementation | ||
| fun create( | ||
| context: Context, | ||
| fileName: String, | ||
| masterKey: androidx.security.crypto.MasterKey, | ||
| prefKeyEncryptionScheme: androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme, | ||
| prefValueEncryptionScheme: androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme | ||
| ): android.content.SharedPreferences { | ||
| return context.getSharedPreferences(fileName, Context.MODE_PRIVATE) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Implements(BiometricHelper::class) | ||
| class ShadowBiometricHelper { | ||
| companion object { | ||
| @JvmStatic | ||
| @Implementation | ||
| fun encryptWithPin(session: AuthenticatedSession, pin: String, salt: ByteArray): com.clhs.score.data.BiometricHelper.EncryptedData { | ||
| return com.clhs.score.data.BiometricHelper.EncryptedData("cipher", Base64.encodeToString("iv".toByteArray(), Base64.NO_WRAP)) | ||
| } | ||
|
|
||
| @JvmStatic | ||
| @Implementation | ||
| fun decryptWithPin(cipherText: String, ivBase64: String, pin: String, salt: ByteArray): AuthenticatedSession { | ||
| return AuthenticatedSession("123", "token", mapOf("cookie1" to "val1")) | ||
| } | ||
|
|
||
| @JvmStatic | ||
| @Implementation | ||
| fun encryptPin(pin: String, cipher: Cipher): com.clhs.score.data.BiometricHelper.EncryptedData { | ||
| return com.clhs.score.data.BiometricHelper.EncryptedData("pinCipher", Base64.encodeToString("pinIv".toByteArray(), Base64.NO_WRAP)) | ||
| } | ||
|
|
||
| @JvmStatic | ||
| @Implementation | ||
| fun decryptPin(cipherText: String, cipher: Cipher): String { | ||
| return "1234" | ||
| } | ||
|
|
||
| @JvmStatic | ||
| @Implementation | ||
| fun deleteSecretKey() { | ||
| // Do nothing | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+138
to
+171
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Kotlin, Remove the @Implements(BiometricHelper::class)
class ShadowBiometricHelper {
@Implementation
fun encryptWithPin(session: AuthenticatedSession, pin: String, salt: ByteArray): com.clhs.score.data.BiometricHelper.EncryptedData {
return com.clhs.score.data.BiometricHelper.EncryptedData("cipher", Base64.encodeToString("iv".toByteArray(), Base64.NO_WRAP))
}
@Implementation
fun decryptWithPin(cipherText: String, ivBase64: String, pin: String, salt: ByteArray): AuthenticatedSession {
return AuthenticatedSession("123", "token", mapOf("cookie1" to "val1"))
}
@Implementation
fun encryptPin(pin: String, cipher: Cipher): com.clhs.score.data.BiometricHelper.EncryptedData {
return com.clhs.score.data.BiometricHelper.EncryptedData("pinCipher", Base64.encodeToString("pinIv".toByteArray(), Base64.NO_WRAP))
}
@Implementation
fun decryptPin(cipherText: String, cipher: Cipher): String {
return "1234"
}
@Implementation
fun deleteSecretKey() {
// Do nothing
}
} |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This project sets
targetSdk = 37, but the newly added JVM test runner depends on Robolectric 4.11.1, whose supported SDK ceiling is much older; when./gradlew testreachesSessionStoreTest, Robolectric validates the generated manifest target SDK before running the@Config(sdk = [34])test and rejects apps whosetargetSdkVersionis above its max SDK. That makes the release workflow's existing./gradlew --no-daemon test assembleReleasestep fail as soon as this test is included, so the dependency needs to be updated to a Robolectric release that supports the app target SDK or the test should avoid Robolectric.Useful? React with πΒ / π.