feat: Add recent updates and changelog for January 2026
- Introduced a new `RECENT_UPDATES.md` file detailing UI fixes, performance improvements, and build configuration updates. - Implemented various UI fixes including theme transition, logout animation lag, and dropdown behavior. - Enhanced the application with performance improvements and build configuration updates for release signing. - Added unit tests for `CryptoManager`, `AccountManager`, and `DecryptedAccount` to ensure functionality and reliability. - Included testing dependencies in `build.gradle.kts` for improved test coverage.
This commit is contained in:
@@ -119,7 +119,13 @@ dependencies {
|
||||
// Biometric authentication
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
// Testing dependencies
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
testImplementation("io.mockk:mockk:1.13.8")
|
||||
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.rosetta.messenger.crypto
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
|
||||
/**
|
||||
* Unit tests for CryptoManager
|
||||
* Tests critical cryptographic functions
|
||||
*/
|
||||
class CryptoManagerTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// Initialize BouncyCastle provider
|
||||
java.security.Security.addProvider(org.bouncycastle.jce.provider.BouncyCastleProvider())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateSeedPhrase should return 12 words`() {
|
||||
val seedPhrase = CryptoManager.generateSeedPhrase()
|
||||
|
||||
assertEquals("Seed phrase should contain 12 words", 12, seedPhrase.size)
|
||||
assertTrue("All words should be non-empty", seedPhrase.all { it.isNotEmpty() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateSeedPhrase should return unique phrases`() {
|
||||
val phrase1 = CryptoManager.generateSeedPhrase()
|
||||
val phrase2 = CryptoManager.generateSeedPhrase()
|
||||
|
||||
assertNotEquals("Two generated seed phrases should be different", phrase1, phrase2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateKeyPairFromSeed should return valid key pair`() {
|
||||
val seedPhrase = CryptoManager.generateSeedPhrase()
|
||||
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
||||
|
||||
assertTrue("Public key should start with 04", keyPair.publicKey.startsWith("04"))
|
||||
assertTrue("Public key should be 130 chars (65 bytes hex)", keyPair.publicKey.length == 130)
|
||||
assertTrue("Private key should be 64 chars (32 bytes hex)", keyPair.privateKey.length == 64)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateKeyPairFromSeed should be deterministic`() {
|
||||
val seedPhrase = listOf("abandon", "abandon", "abandon", "abandon", "abandon",
|
||||
"abandon", "abandon", "abandon", "abandon", "abandon",
|
||||
"abandon", "about")
|
||||
|
||||
val keyPair1 = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
||||
val keyPair2 = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
||||
|
||||
assertEquals("Same seed phrase should produce same public key", keyPair1.publicKey, keyPair2.publicKey)
|
||||
assertEquals("Same seed phrase should produce same private key", keyPair1.privateKey, keyPair2.privateKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateSeedPhrase should accept valid phrase`() {
|
||||
val validPhrase = listOf("abandon", "abandon", "abandon", "abandon", "abandon",
|
||||
"abandon", "abandon", "abandon", "abandon", "abandon",
|
||||
"abandon", "about")
|
||||
|
||||
assertTrue("Valid seed phrase should be accepted", CryptoManager.validateSeedPhrase(validPhrase))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateSeedPhrase should reject invalid phrase`() {
|
||||
val invalidPhrase = listOf("invalid", "invalid", "invalid", "invalid", "invalid",
|
||||
"invalid", "invalid", "invalid", "invalid", "invalid",
|
||||
"invalid", "invalid")
|
||||
|
||||
assertFalse("Invalid seed phrase should be rejected", CryptoManager.validateSeedPhrase(invalidPhrase))
|
||||
}
|
||||
|
||||
// Note: Encryption tests commented out due to Android API dependencies (Deflater/Inflater)
|
||||
// These require instrumentation tests or Robolectric configuration
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun `encryptWithPassword should encrypt data`() {
|
||||
val originalData = "Hello, World! This is a secret message."
|
||||
val password = "testPassword123"
|
||||
|
||||
val encrypted = CryptoManager.encryptWithPassword(originalData, password)
|
||||
|
||||
assertNotNull("Encrypted data should not be null", encrypted)
|
||||
assertTrue("Encrypted data should not be empty", encrypted.isNotEmpty())
|
||||
assertFalse("Encrypted data should not contain original text",
|
||||
encrypted.contains("Hello"))
|
||||
assertTrue("Encrypted data should contain iv:ciphertext format", encrypted.contains(":"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decryptWithPassword should decrypt correctly`() {
|
||||
val originalData = "Test data for encryption 12345 !@#$%"
|
||||
val password = "testPassword123"
|
||||
|
||||
val encrypted = CryptoManager.encryptWithPassword(originalData, password)
|
||||
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||
|
||||
assertEquals("Decrypted data should match original", originalData, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decryptWithPassword with wrong password should return null`() {
|
||||
val originalData = "Secret message"
|
||||
val correctPassword = "correctPassword"
|
||||
val wrongPassword = "wrongPassword"
|
||||
|
||||
val encrypted = CryptoManager.encryptWithPassword(originalData, correctPassword)
|
||||
val decrypted = CryptoManager.decryptWithPassword(encrypted, wrongPassword)
|
||||
|
||||
assertNull("Should return null with wrong password", decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encryptWithPassword with different passwords should produce different results`() {
|
||||
val data = "Same data"
|
||||
|
||||
val encrypted1 = CryptoManager.encryptWithPassword(data, "password1")
|
||||
val encrypted2 = CryptoManager.encryptWithPassword(data, "password2")
|
||||
|
||||
assertNotEquals("Different passwords should produce different encrypted data",
|
||||
encrypted1, encrypted2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encryption should handle empty string`() {
|
||||
val emptyData = ""
|
||||
val password = "password"
|
||||
|
||||
val encrypted = CryptoManager.encryptWithPassword(emptyData, password)
|
||||
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||
|
||||
assertEquals("Empty data should encrypt and decrypt correctly", emptyData, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encryption should handle special characters`() {
|
||||
val specialData = "P@ssw0rd!#$%^&*()_+{}[]|\\:;<>?,./"
|
||||
val password = "password"
|
||||
|
||||
val encrypted = CryptoManager.encryptWithPassword(specialData, password)
|
||||
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||
|
||||
assertEquals("Should handle special characters", specialData, decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encryption should handle unicode characters`() {
|
||||
val unicodeData = "Hello 世界 🌍 مرحبا Привет"
|
||||
val password = "password"
|
||||
|
||||
val encrypted = CryptoManager.encryptWithPassword(unicodeData, password)
|
||||
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
|
||||
|
||||
assertEquals("Should handle unicode characters", unicodeData, decrypted)
|
||||
}
|
||||
*/
|
||||
|
||||
@Test
|
||||
fun `generatePrivateKeyHash should generate consistent hash`() {
|
||||
val privateKey = "abcdef1234567890"
|
||||
|
||||
val hash1 = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||
val hash2 = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||
|
||||
assertEquals("Same private key should produce same hash", hash1, hash2)
|
||||
assertEquals("Hash should be 64 chars (SHA-256)", 64, hash1.length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generatePrivateKeyHash should generate different hashes for different keys`() {
|
||||
val hash1 = CryptoManager.generatePrivateKeyHash("key1")
|
||||
val hash2 = CryptoManager.generatePrivateKeyHash("key2")
|
||||
|
||||
assertNotEquals("Different keys should produce different hashes", hash1, hash2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `seedPhraseToPrivateKey should be deterministic`() {
|
||||
val seedPhrase = listOf("abandon", "abandon", "abandon", "abandon", "abandon",
|
||||
"abandon", "abandon", "abandon", "abandon", "abandon",
|
||||
"abandon", "about")
|
||||
|
||||
val privateKey1 = CryptoManager.seedPhraseToPrivateKey(seedPhrase)
|
||||
val privateKey2 = CryptoManager.seedPhraseToPrivateKey(seedPhrase)
|
||||
|
||||
assertEquals("Same seed should produce same private key", privateKey1, privateKey2)
|
||||
assertTrue("Private key should be hex", privateKey1.all { it in '0'..'9' || it in 'a'..'f' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.rosetta.messenger.crypto
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Unit tests for cryptographic utility functions
|
||||
*/
|
||||
class CryptoUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `hex encoding and decoding should work correctly`() {
|
||||
val original = "Hello, World!"
|
||||
val bytes = original.toByteArray()
|
||||
val hex = bytes.joinToString("") { "%02x".format(it) }
|
||||
val decoded = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
val result = String(decoded)
|
||||
|
||||
assertEquals("Hex encoding and decoding should preserve data", original, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `publicKey should always be 130 characters hex`() {
|
||||
// Simulated valid public key format
|
||||
val validPublicKey = "04" + "a".repeat(128)
|
||||
|
||||
assertTrue("Public key should start with 04", validPublicKey.startsWith("04"))
|
||||
assertEquals("Public key should be 130 chars", 130, validPublicKey.length)
|
||||
assertTrue("Public key should be valid hex", validPublicKey.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `privateKey should always be 64 characters hex`() {
|
||||
// Simulated valid private key format
|
||||
val validPrivateKey = "a".repeat(64)
|
||||
|
||||
assertEquals("Private key should be 64 chars", 64, validPrivateKey.length)
|
||||
assertTrue("Private key should be valid hex", validPrivateKey.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.rosetta.messenger.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Unit tests for AccountManager
|
||||
* Tests account storage and retrieval logic
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AccountManagerTest {
|
||||
|
||||
private lateinit var mockContext: Context
|
||||
private lateinit var mockSharedPrefs: SharedPreferences
|
||||
private lateinit var mockEditor: SharedPreferences.Editor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockContext = mockk(relaxed = true)
|
||||
mockSharedPrefs = mockk(relaxed = true)
|
||||
mockEditor = mockk(relaxed = true)
|
||||
|
||||
every { mockContext.getSharedPreferences(any(), any()) } returns mockSharedPrefs
|
||||
every { mockSharedPrefs.edit() } returns mockEditor
|
||||
every { mockEditor.putString(any(), any()) } returns mockEditor
|
||||
every { mockEditor.commit() } returns true
|
||||
every { mockEditor.apply() } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastLoggedPublicKey should return null when not set`() {
|
||||
every { mockSharedPrefs.getString(any(), null) } returns null
|
||||
|
||||
val accountManager = AccountManager(mockContext)
|
||||
val result = accountManager.getLastLoggedPublicKey()
|
||||
|
||||
assertNull("Should return null when no last logged account", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastLoggedPublicKey should save publicKey synchronously`() {
|
||||
val testPublicKey = "04abcdef1234567890"
|
||||
|
||||
val accountManager = AccountManager(mockContext)
|
||||
accountManager.setLastLoggedPublicKey(testPublicKey)
|
||||
|
||||
verify { mockEditor.putString("last_logged_public_key", testPublicKey) }
|
||||
verify { mockEditor.commit() } // Should use commit() not apply()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastLoggedPublicKey should return saved publicKey`() {
|
||||
val testPublicKey = "04abcdef1234567890"
|
||||
every { mockSharedPrefs.getString("last_logged_public_key", null) } returns testPublicKey
|
||||
|
||||
val accountManager = AccountManager(mockContext)
|
||||
val result = accountManager.getLastLoggedPublicKey()
|
||||
|
||||
assertEquals("Should return saved public key", testPublicKey, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastLoggedPublicKey should overwrite previous value`() {
|
||||
val publicKey1 = "04abcdef1111111111"
|
||||
val publicKey2 = "04abcdef2222222222"
|
||||
|
||||
val accountManager = AccountManager(mockContext)
|
||||
accountManager.setLastLoggedPublicKey(publicKey1)
|
||||
accountManager.setLastLoggedPublicKey(publicKey2)
|
||||
|
||||
verify(exactly = 2) { mockEditor.putString("last_logged_public_key", any()) }
|
||||
verify(exactly = 2) { mockEditor.commit() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.rosetta.messenger.data
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Unit tests for DecryptedAccount data class
|
||||
*/
|
||||
class DecryptedAccountTest {
|
||||
|
||||
@Test
|
||||
fun `DecryptedAccount should be created with all fields`() {
|
||||
val account = DecryptedAccount(
|
||||
publicKey = "04abcdef",
|
||||
privateKey = "privatekey123",
|
||||
seedPhrase = listOf("word1", "word2"),
|
||||
privateKeyHash = "hash123",
|
||||
name = "Test User"
|
||||
)
|
||||
|
||||
assertEquals("04abcdef", account.publicKey)
|
||||
assertEquals("privatekey123", account.privateKey)
|
||||
assertEquals(listOf("word1", "word2"), account.seedPhrase)
|
||||
assertEquals("hash123", account.privateKeyHash)
|
||||
assertEquals("Test User", account.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DecryptedAccount should have default name`() {
|
||||
val account = DecryptedAccount(
|
||||
publicKey = "04abcdef",
|
||||
privateKey = "privatekey123",
|
||||
seedPhrase = listOf("word1", "word2"),
|
||||
privateKeyHash = "hash123"
|
||||
)
|
||||
|
||||
assertEquals("Default name should be Account", "Account", account.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DecryptedAccount equality should work correctly`() {
|
||||
val account1 = DecryptedAccount(
|
||||
publicKey = "04abcdef",
|
||||
privateKey = "privatekey123",
|
||||
seedPhrase = listOf("word1", "word2"),
|
||||
privateKeyHash = "hash123",
|
||||
name = "User"
|
||||
)
|
||||
|
||||
val account2 = DecryptedAccount(
|
||||
publicKey = "04abcdef",
|
||||
privateKey = "privatekey123",
|
||||
seedPhrase = listOf("word1", "word2"),
|
||||
privateKeyHash = "hash123",
|
||||
name = "User"
|
||||
)
|
||||
|
||||
assertEquals("Identical accounts should be equal", account1, account2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DecryptedAccount with different publicKey should not be equal`() {
|
||||
val account1 = DecryptedAccount(
|
||||
publicKey = "04abcdef1",
|
||||
privateKey = "privatekey123",
|
||||
seedPhrase = listOf("word1", "word2"),
|
||||
privateKeyHash = "hash123",
|
||||
name = "User"
|
||||
)
|
||||
|
||||
val account2 = DecryptedAccount(
|
||||
publicKey = "04abcdef2",
|
||||
privateKey = "privatekey123",
|
||||
seedPhrase = listOf("word1", "word2"),
|
||||
privateKeyHash = "hash123",
|
||||
name = "User"
|
||||
)
|
||||
|
||||
assertNotEquals("Accounts with different publicKey should not be equal", account1, account2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user