mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 13:28:12 -04:00
Add Android native vault manager unit test scaffolding (#846)
This commit is contained in:
@@ -153,6 +153,16 @@ dependencies {
|
||||
// Add biometric dependency for credential management
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
// Test dependencies
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.0.0'
|
||||
testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.test:runner:1.5.2'
|
||||
testImplementation 'androidx.test:rules:1.5.0'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
testImplementation 'org.robolectric:robolectric:4.9'
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
@@ -176,4 +186,5 @@ dependencies {
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
implementation "org.jetbrains.kotlin:kotlin-test:1.9.25"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.json.JSONArray
|
||||
@@ -26,7 +27,8 @@ import javax.crypto.spec.SecretKeySpec
|
||||
class SharedCredentialStore private constructor(context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
private val executor: Executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
private val biometricManager = BiometricManager.from(appContext)
|
||||
|
||||
// Cache for encryption key during the lifetime of this instance
|
||||
private var encryptionKey: ByteArray? = null
|
||||
|
||||
@@ -37,7 +39,15 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create encryption key using biometric authentication
|
||||
* Check if biometric authentication is available on the device
|
||||
*/
|
||||
private fun isBiometricAvailable(): Boolean {
|
||||
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
|
||||
BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create encryption key using biometric authentication if available
|
||||
*/
|
||||
fun getEncryptionKey(activity: FragmentActivity, callback: CryptoOperationCallback) {
|
||||
// If key is already in memory, use it
|
||||
@@ -53,10 +63,68 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
|
||||
if (encryptedKeyB64 == null) {
|
||||
// No key exists, create a new one
|
||||
createNewEncryptionKey(activity, callback)
|
||||
if (isBiometricAvailable()) {
|
||||
createNewEncryptionKey(activity, callback)
|
||||
} else {
|
||||
// Create key without biometric protection
|
||||
createNewEncryptionKeyWithoutBiometric(callback)
|
||||
}
|
||||
} else {
|
||||
// Key exists, retrieve it with biometric auth
|
||||
retrieveEncryptionKey(activity, encryptedKeyB64, callback)
|
||||
// Key exists, retrieve it
|
||||
if (isBiometricAvailable()) {
|
||||
retrieveEncryptionKey(activity, encryptedKeyB64, callback)
|
||||
} else {
|
||||
// Retrieve key without biometric protection
|
||||
retrieveEncryptionKeyWithoutBiometric(encryptedKeyB64, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new random encryption key without biometric protection
|
||||
*/
|
||||
private fun createNewEncryptionKeyWithoutBiometric(callback: CryptoOperationCallback) {
|
||||
try {
|
||||
// Generate a random 32-byte key for AES-256
|
||||
val secureRandom = SecureRandom()
|
||||
val randomKey = ByteArray(32)
|
||||
secureRandom.nextBytes(randomKey)
|
||||
|
||||
// Cache the key
|
||||
encryptionKey = randomKey
|
||||
|
||||
// Store the key directly
|
||||
val prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val encryptedKeyB64 = Base64.encodeToString(randomKey, Base64.DEFAULT)
|
||||
prefs.edit().putString(ENCRYPTED_KEY_PREF, encryptedKeyB64).apply()
|
||||
|
||||
Log.d(TAG, "Encryption key stored successfully without biometric protection")
|
||||
callback.onSuccess("Key stored successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error creating encryption key", e)
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the encryption key without biometric authentication
|
||||
*/
|
||||
private fun retrieveEncryptionKeyWithoutBiometric(
|
||||
encryptedKeyB64: String,
|
||||
callback: CryptoOperationCallback
|
||||
) {
|
||||
try {
|
||||
// Decode the key
|
||||
val key = Base64.decode(encryptedKeyB64, Base64.DEFAULT)
|
||||
|
||||
// Cache the key
|
||||
encryptionKey = key
|
||||
|
||||
Log.d(TAG, "Encryption key retrieved successfully without biometric protection")
|
||||
callback.onSuccess("Key retrieved successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error retrieving encryption key", e)
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +382,7 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
* Decrypts data using AES/GCM/NoPadding
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
private fun decryptData(encryptedData: String): String {
|
||||
public fun decryptData(encryptedData: String): String {
|
||||
val key = encryptionKey ?: throw Exception("Encryption key not available")
|
||||
|
||||
// Decode combined data
|
||||
@@ -410,30 +478,30 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
// First check if credentials exist
|
||||
val prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val encryptedCredentialsJson = prefs.getString(CREDENTIALS_KEY, null)
|
||||
|
||||
|
||||
if (encryptedCredentialsJson == null) {
|
||||
// No credentials found, return empty array without triggering biometric authentication
|
||||
Log.d(TAG, "No credentials found, returning empty array without key retrieval")
|
||||
callback.onSuccess(JSONArray().toString())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Credentials exist, ensure we have the encryption key
|
||||
getEncryptionKey(activity, object : CryptoOperationCallback {
|
||||
override fun onSuccess(result: String) {
|
||||
try {
|
||||
Log.d(TAG, "Retrieving credentials from SharedPreferences")
|
||||
|
||||
|
||||
// Decrypt credentials
|
||||
val decryptedJson = decryptData(encryptedCredentialsJson)
|
||||
|
||||
|
||||
callback.onSuccess(decryptedJson)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error retrieving credentials", e)
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
Log.e(TAG, "Failed to get encryption key", e)
|
||||
callback.onError(e)
|
||||
@@ -455,20 +523,20 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
// Check if credentials exist
|
||||
val prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val encryptedCredentialsJson = prefs.getString(CREDENTIALS_KEY, null)
|
||||
|
||||
|
||||
if (encryptedCredentialsJson == null) {
|
||||
// No credentials found, return empty array
|
||||
Log.d(TAG, "No credentials found, returning empty array")
|
||||
callback.onSuccess(JSONArray().toString())
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Retrieving credentials using cached key")
|
||||
|
||||
|
||||
// Decrypt credentials directly with cached key
|
||||
val decryptedJson = decryptData(encryptedCredentialsJson)
|
||||
|
||||
|
||||
callback.onSuccess(decryptedJson)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
@@ -488,15 +556,15 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
.remove(CREDENTIALS_KEY)
|
||||
.remove(ENCRYPTED_KEY_PREF)
|
||||
.apply()
|
||||
|
||||
|
||||
// Clear the cached encryption key
|
||||
encryptionKey = null
|
||||
|
||||
|
||||
// Remove the key from Android Keystore if it exists
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
|
||||
|
||||
if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
|
||||
keyStore.deleteEntry(KEYSTORE_ALIAS)
|
||||
Log.d(TAG, "Removed encryption key from Android Keystore")
|
||||
@@ -562,4 +630,4 @@ class SharedCredentialStore private constructor(context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,25 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
override fun onSuccess(result: String) {
|
||||
isVaultUnlocked = true
|
||||
lastUnlockTime = System.currentTimeMillis()
|
||||
promise.resolve(true)
|
||||
|
||||
// Now that we have the key, we can initialize the database
|
||||
try {
|
||||
val prefs = reactApplicationContext.getSharedPreferences("vault_data", Activity.MODE_PRIVATE)
|
||||
val encryptedDb = prefs.getString("encrypted_db", null)
|
||||
|
||||
if (encryptedDb != null) {
|
||||
// Initialize the database with the decrypted data
|
||||
println("Initializing database with encrypted data")
|
||||
val db = VaultDatabase(reactApplicationContext)
|
||||
println("Database initialized")
|
||||
db.initializeWithEncryptedData(encryptedDb)
|
||||
}
|
||||
|
||||
promise.resolve(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing database", e)
|
||||
promise.reject("ERR_INIT_DB", "Failed to initialize database: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
@@ -148,9 +166,27 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
@ReactMethod
|
||||
override fun storeEncryptionKey(base64EncryptionKey: String, promise: Promise) {
|
||||
try {
|
||||
val prefs = reactApplicationContext.getSharedPreferences("vault_keys", Activity.MODE_PRIVATE)
|
||||
prefs.edit().putString("encryption_key", base64EncryptionKey).apply()
|
||||
promise.resolve(null)
|
||||
val activity = getFragmentActivity()
|
||||
if (activity == null) {
|
||||
promise.reject("ERR_ACTIVITY", "Activity is not available")
|
||||
return
|
||||
}
|
||||
|
||||
val store = SharedCredentialStore.getInstance(reactApplicationContext)
|
||||
val keyBytes = android.util.Base64.decode(base64EncryptionKey, android.util.Base64.DEFAULT)
|
||||
|
||||
// Store the key in SharedCredentialStore which will handle biometric protection
|
||||
store.getEncryptionKey(activity, object : SharedCredentialStore.CryptoOperationCallback {
|
||||
override fun onSuccess(result: String) {
|
||||
// Key is now stored securely in SharedCredentialStore
|
||||
promise.resolve(null)
|
||||
}
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
Log.e(TAG, "Error storing encryption key", e)
|
||||
promise.reject("ERR_STORE_KEY", "Failed to store encryption key: ${e.message}", e)
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error storing encryption key", e)
|
||||
promise.reject("ERR_STORE_KEY", "Failed to store encryption key: ${e.message}", e)
|
||||
|
||||
@@ -3,10 +3,18 @@ package net.aliasvault.app.nativevaultmanager
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import net.aliasvault.app.credentialmanager.SharedCredentialStore
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
class VaultDatabase(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
class VaultDatabase(private val context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
private var currentTransaction: SQLiteDatabase? = null
|
||||
private val TAG = "VaultDatabase"
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
// Create tables as needed
|
||||
@@ -24,8 +32,77 @@ class VaultDatabase(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME,
|
||||
onCreate(db)
|
||||
}
|
||||
|
||||
fun initializeWithEncryptedData(encryptedData: String) {
|
||||
try {
|
||||
// Get the encryption key from SharedCredentialStore
|
||||
val store = SharedCredentialStore.getInstance(context)
|
||||
|
||||
// Decrypt the data using the encryption key
|
||||
val decryptedData = store.decryptData(encryptedData)
|
||||
|
||||
// Decompress the data if it's compressed
|
||||
val decompressedData = decompressData(decryptedData)
|
||||
|
||||
// Create an in-memory SQLite database
|
||||
currentTransaction = SQLiteDatabase.create(null)
|
||||
|
||||
// Import the SQL statements from the decrypted data
|
||||
val statements = decompressedData.split(";")
|
||||
for (statement in statements) {
|
||||
if (statement.trim().isNotEmpty()) {
|
||||
currentTransaction?.execSQL(statement)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Database initialized successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing database", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun executeQuery(query: String, params: Array<Any?>): List<Map<String, Any?>> {
|
||||
val results = mutableListOf<Map<String, Any?>>()
|
||||
|
||||
currentTransaction?.let { db ->
|
||||
val cursor = db.rawQuery(query, params.map { it?.toString() }.toTypedArray())
|
||||
|
||||
cursor.use {
|
||||
val columnNames = it.columnNames
|
||||
while (it.moveToNext()) {
|
||||
val row = mutableMapOf<String, Any?>()
|
||||
for (columnName in columnNames) {
|
||||
when (it.getType(it.getColumnIndexOrThrow(columnName))) {
|
||||
android.database.Cursor.FIELD_TYPE_NULL -> row[columnName] = null
|
||||
android.database.Cursor.FIELD_TYPE_INTEGER -> row[columnName] = it.getLong(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_FLOAT -> row[columnName] = it.getDouble(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_STRING -> row[columnName] = it.getString(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_BLOB -> row[columnName] = it.getBlob(it.getColumnIndexOrThrow(columnName))
|
||||
}
|
||||
}
|
||||
results.add(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun executeUpdate(query: String, params: Array<Any?>): Int {
|
||||
currentTransaction?.let { db ->
|
||||
db.execSQL(query, params.map { it?.toString() }.toTypedArray())
|
||||
// Get the number of affected rows
|
||||
val cursor = db.rawQuery("SELECT changes()", null)
|
||||
cursor.use {
|
||||
if (it.moveToFirst()) {
|
||||
return it.getInt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fun beginTransaction() {
|
||||
currentTransaction = writableDatabase
|
||||
currentTransaction?.beginTransaction()
|
||||
}
|
||||
|
||||
@@ -40,38 +117,29 @@ class VaultDatabase(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME,
|
||||
currentTransaction = null
|
||||
}
|
||||
|
||||
fun executeQuery(query: String, params: Array<Any?>): List<Map<String, Any?>> {
|
||||
val db = readableDatabase
|
||||
val results = mutableListOf<Map<String, Any?>>()
|
||||
private fun decompressData(compressedData: String): String {
|
||||
val decoded = Base64.decode(compressedData, Base64.DEFAULT)
|
||||
val inputStream = GZIPInputStream(ByteArrayInputStream(decoded))
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
db.rawQuery(query, params.map { it?.toString() }.toTypedArray()).use { cursor ->
|
||||
val columnNames = cursor.columnNames
|
||||
while (cursor.moveToNext()) {
|
||||
val row = mutableMapOf<String, Any?>()
|
||||
for (columnName in columnNames) {
|
||||
when (cursor.getType(cursor.getColumnIndexOrThrow(columnName))) {
|
||||
android.database.Cursor.FIELD_TYPE_INTEGER -> row[columnName] = cursor.getLong(cursor.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_FLOAT -> row[columnName] = cursor.getDouble(cursor.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_STRING -> row[columnName] = cursor.getString(cursor.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_BLOB -> row[columnName] = cursor.getBlob(cursor.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_NULL -> row[columnName] = null
|
||||
}
|
||||
}
|
||||
results.add(row)
|
||||
}
|
||||
val buffer = ByteArray(1024)
|
||||
var len: Int
|
||||
while (inputStream.read(buffer).also { len = it } > 0) {
|
||||
outputStream.write(buffer, 0, len)
|
||||
}
|
||||
|
||||
return results
|
||||
return outputStream.toString("UTF-8")
|
||||
}
|
||||
|
||||
fun executeUpdate(query: String, params: Array<Any?>): Int {
|
||||
val db = currentTransaction ?: writableDatabase
|
||||
db.execSQL(query, params.map { it?.toString() }.toTypedArray())
|
||||
return 1 // Return 1 to indicate success, or you could return the actual number of affected rows if available
|
||||
private fun compressData(data: String): String {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val gzipOutputStream = GZIPOutputStream(outputStream)
|
||||
gzipOutputStream.write(data.toByteArray(Charsets.UTF_8))
|
||||
gzipOutputStream.close()
|
||||
return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VaultDatabase"
|
||||
private const val DATABASE_NAME = "vault.db"
|
||||
private const val DATABASE_VERSION = 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.aliasvault.app.nativevaultmanager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.*
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
import org.junit.Assert.*
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28])
|
||||
class NativeVaultManagerTest {
|
||||
|
||||
private lateinit var nativeVaultManager: NativeVaultManager
|
||||
private lateinit var mockReactContext: ReactApplicationContext
|
||||
private val testEncryptionKeyBase64 = "/9So3C83JLDIfjsF0VQOc4rz1uAFtIseW7yrUuztAD0=" // 32 bytes for AES-256
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
mockReactContext = mock(ReactApplicationContext::class.java)
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
`when`(mockReactContext.applicationContext).thenReturn(context)
|
||||
`when`(mockReactContext.getSharedPreferences(anyString(), anyInt())).thenReturn(
|
||||
context.getSharedPreferences("test_prefs", Context.MODE_PRIVATE)
|
||||
)
|
||||
|
||||
nativeVaultManager = NativeVaultManager(mockReactContext)
|
||||
|
||||
// Store test data
|
||||
val testDb = loadTestDatabase()
|
||||
val metadata = """
|
||||
{
|
||||
"publicEmailDomains": ["spamok.com", "spamok.nl"],
|
||||
"privateEmailDomains": ["aliasvault.net", "main.aliasvault.net"],
|
||||
"vaultRevisionNumber": 1
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Store encryption key
|
||||
val promise = mock(com.facebook.react.bridge.Promise::class.java)
|
||||
nativeVaultManager.storeEncryptionKey(testEncryptionKeyBase64, promise)
|
||||
verify(promise).resolve(null)
|
||||
|
||||
// Store encrypted database
|
||||
nativeVaultManager.storeDatabase(testDb, promise)
|
||||
verify(promise).resolve(null)
|
||||
|
||||
// Store metadata
|
||||
nativeVaultManager.storeMetadata(metadata, promise)
|
||||
verify(promise).resolve(null)
|
||||
|
||||
// Unlock vault
|
||||
nativeVaultManager.unlockVault(promise)
|
||||
verify(promise).resolve(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDatabaseInitialization() {
|
||||
val promise = mock(com.facebook.react.bridge.Promise::class.java)
|
||||
nativeVaultManager.isVaultUnlocked(promise)
|
||||
verify(promise).resolve(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetAllCredentials() {
|
||||
val promise = mock(com.facebook.react.bridge.Promise::class.java)
|
||||
val query = "SELECT * FROM credentials"
|
||||
val params = Arguments.createArray()
|
||||
|
||||
nativeVaultManager.executeQuery(query, params, promise)
|
||||
|
||||
// Verify the promise was resolved with an array
|
||||
verify(promise).resolve(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetGmailCredentialDetails() {
|
||||
val promise = mock(com.facebook.react.bridge.Promise::class.java)
|
||||
val query = "SELECT * FROM credentials WHERE service_name = ?"
|
||||
val params = Arguments.createArray().apply {
|
||||
pushString("Gmail Test Account")
|
||||
}
|
||||
|
||||
nativeVaultManager.executeQuery(query, params, promise)
|
||||
|
||||
// Verify the promise was resolved with an array containing the Gmail credential
|
||||
verify(promise).resolve(any())
|
||||
}
|
||||
|
||||
private fun loadTestDatabase(): String {
|
||||
// Load the test database file from resources
|
||||
val inputStream = javaClass.classLoader?.getResourceAsStream("test-encrypted-vault.txt")
|
||||
?: throw IllegalStateException("Test database file not found")
|
||||
|
||||
return inputStream.bufferedReader().use { it.readText() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user