diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index fb6ccc436b..5fc777c8a1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -21,4 +21,24 @@ interface AuthDiskSource { * Emits updates that track [userState]. This will replay the last known value, if any. */ val userStateFlow: Flow + + /** + * Retrieves a user key using a [userId]. + */ + fun getUserKey(userId: String): String? + + /** + * Stores a user key using a [userId]. + */ + fun storeUserKey(userId: String, userKey: String?) + + /** + * Retrieves a private key using a [userId]. + */ + fun getPrivateKey(userId: String): String? + + /** + * Stores a private key using a [userId]. + */ + fun storePrivateKey(userId: String, privateKey: String?) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 5940d2872e..0645741f46 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -12,6 +12,8 @@ import kotlinx.serialization.json.Json private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail" private const val STATE_KEY = "$BASE_KEY:state" +private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey" +private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "encPrivateKey" /** * Primary implementation of [AuthDiskSource]. @@ -56,4 +58,24 @@ class AuthDiskSourceImpl( STATE_KEY -> mutableUserStateFlow.tryEmit(userState) } } + + override fun getUserKey(userId: String): String? = + getString(key = "${MASTER_KEY_ENCRYPTION_USER_KEY}_$userId") + + override fun storeUserKey(userId: String, userKey: String?) { + putString( + key = "${MASTER_KEY_ENCRYPTION_USER_KEY}_$userId", + value = userKey, + ) + } + + override fun getPrivateKey(userId: String): String? = + getString(key = "${MASTER_KEY_ENCRYPTION_PRIVATE_KEY}_$userId") + + override fun storePrivateKey(userId: String, privateKey: String?) { + putString( + key = "${MASTER_KEY_ENCRYPTION_PRIVATE_KEY}_$userId", + value = privateKey, + ) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 80814eae11..68521b200e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -112,6 +112,16 @@ class AuthRepositoryImpl @Inject constructor( .toUserState( previousUserState = authDiskSource.userState, ) + .also { userState -> + authDiskSource.storeUserKey( + userId = userState.activeUserId, + userKey = it.key, + ) + authDiskSource.storePrivateKey( + userId = userState.activeUserId, + privateKey = it.privateKey, + ) + } LoginResult.Success } @@ -131,7 +141,8 @@ class AuthRepositoryImpl @Inject constructor( val updatedAccounts = currentUserState .accounts .filterKeys { it != activeUserId } - + authDiskSource.storeUserKey(userId = activeUserId, userKey = null) + authDiskSource.storePrivateKey(userId = activeUserId, privateKey = null) // Check if there is a new active user if (updatedAccounts.isNotEmpty()) { val (updatedActiveUserId, updatedActiveAccount) = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index e56751a3ae..91a9abd534 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.repository +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch class VaultRepositoryImpl constructor( private val syncService: SyncService, private val vaultSdkSource: VaultSdkSource, + private val authDiskSource: AuthDiskSource, dispatcherManager: DispatcherManager, ) : VaultRepository { @@ -29,8 +31,11 @@ class VaultRepositoryImpl constructor( .sync() .fold( onSuccess = { syncResponse -> + storeUserKeyAndPrivateKey( + userKey = syncResponse.profile?.key, + privateKey = syncResponse.profile?.privateKey, + ) // TODO transform into domain object consumable by VaultViewModel BIT-205. - // TODO initialize crypto in BIT-990 syncResponse.ciphers?.let { networkCiphers -> vaultSdkSource.decryptCipherList( @@ -49,4 +54,22 @@ class VaultRepositoryImpl constructor( ) } } + + private fun storeUserKeyAndPrivateKey( + userKey: String?, + privateKey: String?, + ) { + val userId = authDiskSource.userState?.activeUserId ?: return + if (userKey == null || privateKey == null) return + authDiskSource.apply { + storeUserKey( + userId = userId, + userKey = userKey, + ) + storePrivateKey( + userId = userId, + privateKey = privateKey, + ) + } + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index b0b1fbad42..642c91bae0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.repository.di +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource @@ -23,10 +24,12 @@ class VaultRepositoryModule { fun providesVaultRepository( syncService: SyncService, vaultSdkSource: VaultSdkSource, + authDiskSource: AuthDiskSource, dispatcherManager: DispatcherManager, ): VaultRepository = VaultRepositoryImpl( syncService = syncService, vaultSdkSource = vaultSdkSource, + authDiskSource = authDiskSource, dispatcherManager = dispatcherManager, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 2789fb667b..cbcbf2cf5f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -87,6 +87,79 @@ class AuthDiskSourceTest { assertEquals(USER_STATE, awaitItem()) } } + + @Test + fun `getUserKey should pull from SharedPreferences`() { + val mockUserId = "mockUserId" + val mockUserKey = "mockUserKey" + fakeSharedPreferences + .edit() + .putString( + "masterKeyEncryptedUserKey_$mockUserId", + mockUserKey, + ) + .apply() + val actual = authDiskSource.getUserKey(userId = mockUserId) + assertEquals( + mockUserKey, + actual, + ) + } + + @Test + fun `storeUserKey should update SharedPreferences`() { + val mockUserId = "mockUserId" + val mockUserKey = "mockUserKey" + authDiskSource.storeUserKey( + userId = mockUserId, + userKey = mockUserKey, + ) + val actual = fakeSharedPreferences + .getString( + "masterKeyEncryptedUserKey_$mockUserId", + null, + ) + assertEquals( + mockUserKey, + actual, + ) + } + + @Test + fun `getPrivateKey should pull from SharedPreferences`() { + val mockUserId = "mockUserId" + val mockPrivateKey = "mockPrivateKey" + fakeSharedPreferences + .edit() + .putString( + "encPrivateKey_$mockUserId", + mockPrivateKey, + ) + .apply() + val actual = authDiskSource.getPrivateKey(userId = mockUserId) + assertEquals( + mockPrivateKey, + actual, + ) + } + + @Test + fun `storePrivateKey should update SharedPreferences`() { + val mockUserId = "mockUserId" + val mockPrivateKey = "mockPrivateKey" + authDiskSource.storePrivateKey( + userId = mockUserId, + privateKey = mockPrivateKey, + ) + val actual = fakeSharedPreferences.getString( + "encPrivateKey_$mockUserId", + null, + ) + assertEquals( + mockPrivateKey, + actual, + ) + } } private const val USER_STATE_JSON = """ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt new file mode 100644 index 0000000000..7058bd5653 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -0,0 +1,57 @@ +package com.x8bit.bitwarden.data.auth.datasource.disk.util + +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onSubscription +import org.junit.Assert.assertEquals + +class FakeAuthDiskSource : AuthDiskSource { + override var rememberedEmailAddress: String? = null + + override var userState: UserStateJson? = null + set(value) { + field = value + mutableUserStateFlow.tryEmit(value) + } + + override val userStateFlow: Flow + get() = mutableUserStateFlow.onSubscription { emit(userState) } + + override fun getUserKey(userId: String): String? = storedUserKeys[userId] + + override fun storeUserKey(userId: String, userKey: String?) { + storedUserKeys[userId] = userKey + } + + override fun getPrivateKey(userId: String): String? = storedPrivateKeys[userId] + + override fun storePrivateKey(userId: String, privateKey: String?) { + storedPrivateKeys[userId] = privateKey + } + + private val mutableUserStateFlow = + MutableSharedFlow( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + ) + + private val storedUserKeys = mutableMapOf() + + private val storedPrivateKeys = mutableMapOf() + + /** + * Assert that the [userKey] was stored successfully using the [userId]. + */ + fun assertUserKey(userId: String, userKey: String?) { + assertEquals(userKey, storedUserKeys[userId]) + } + + /** + * Assert that the [privateKey] was stored successfully using the [userId]. + */ + fun assertPrivateKey(userId: String, privateKey: String?) { + assertEquals(privateKey, storedPrivateKeys[userId]) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index c7300f39fb..47c814a00b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -4,9 +4,9 @@ import app.cash.turbine.test import com.bitwarden.core.Kdf import com.bitwarden.core.RegisterKeyResponse import com.bitwarden.core.RsaKeyPair -import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson +import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256 @@ -38,9 +38,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals @@ -186,34 +183,43 @@ class AuthRepositoryTest { } @Test - fun `login get token succeeds should return Success and update AuthState`() = runTest { - val successResponse = GET_TOKEN_RESPONSE_SUCCESS - coEvery { - accountsService.preLogin(email = EMAIL) - } returns Result.success(PRE_LOGIN_SUCCESS) - coEvery { - identityService.getToken( - email = EMAIL, - passwordHash = PASSWORD_HASH, - captchaToken = null, + fun `login get token succeeds should return Success and update AuthState and stored keys`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + accountsService.preLogin(email = EMAIL) + } returns Result.success(PRE_LOGIN_SUCCESS) + coEvery { + identityService.getToken( + email = EMAIL, + passwordHash = PASSWORD_HASH, + captchaToken = null, + ) + } + .returns(Result.success(successResponse)) + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null) + } returns SINGLE_USER_STATE_1 + val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.Success, result) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + coVerify { accountsService.preLogin(email = EMAIL) } + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = "privateKey", ) - } - .returns(Result.success(successResponse)) - every { - GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null) - } returns SINGLE_USER_STATE_1 - val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) - assertEquals(LoginResult.Success, result) - assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) - coVerify { accountsService.preLogin(email = EMAIL) } - coVerify { - identityService.getToken( - email = EMAIL, - passwordHash = PASSWORD_HASH, - captchaToken = null, + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = "key", ) + coVerify { + identityService.getToken( + email = EMAIL, + passwordHash = PASSWORD_HASH, + captchaToken = null, + ) + } } - } @Test fun `login get token returns captcha request should return CaptchaRequired`() = runTest { @@ -578,7 +584,7 @@ class AuthRepositoryTest { } @Test - fun `logout for single account should clear the access token`() = runTest { + fun `logout for single account should clear the access toke and stored keys`() = runTest { // First login: val successResponse = GET_TOKEN_RESPONSE_SUCCESS coEvery { @@ -608,45 +614,62 @@ class AuthRepositoryTest { assertEquals(AuthState.Unauthenticated, awaitItem()) assertNull(fakeAuthDiskSource.userState) + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = null, + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = null, + ) } } @Test - fun `logout for multiple accounts should update current access token`() = runTest { - // First populate multiple user accounts - fakeAuthDiskSource.userState = SINGLE_USER_STATE_2 + fun `logout for multiple accounts should update current access token and stored keys`() = + runTest { + // First populate multiple user accounts + fakeAuthDiskSource.userState = SINGLE_USER_STATE_2 - // Then login: - val successResponse = GET_TOKEN_RESPONSE_SUCCESS - coEvery { - accountsService.preLogin(email = EMAIL) - } returns Result.success(PRE_LOGIN_SUCCESS) - coEvery { - identityService.getToken( - email = EMAIL, - passwordHash = PASSWORD_HASH, - captchaToken = null, - ) - } returns Result.success(successResponse) - every { - GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = SINGLE_USER_STATE_2) - } returns MULTI_USER_STATE + // Then login: + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + accountsService.preLogin(email = EMAIL) + } returns Result.success(PRE_LOGIN_SUCCESS) + coEvery { + identityService.getToken( + email = EMAIL, + passwordHash = PASSWORD_HASH, + captchaToken = null, + ) + } returns Result.success(successResponse) + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = SINGLE_USER_STATE_2) + } returns MULTI_USER_STATE - repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) - assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) - assertEquals(MULTI_USER_STATE, fakeAuthDiskSource.userState) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + assertEquals(MULTI_USER_STATE, fakeAuthDiskSource.userState) - // Then call logout: - repository.authStateFlow.test { - assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem()) + // Then call logout: + repository.authStateFlow.test { + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem()) - repository.logout() + repository.logout() - assertEquals(AuthState.Authenticated(ACCESS_TOKEN_2), awaitItem()) - assertEquals(SINGLE_USER_STATE_2, fakeAuthDiskSource.userState) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN_2), awaitItem()) + assertEquals(SINGLE_USER_STATE_2, fakeAuthDiskSource.userState) + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = null, + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = null, + ) + } } - } @Test fun `getPasswordStrength should be based on password length`() = runTest { @@ -774,22 +797,3 @@ class AuthRepositoryTest { ) } } - -private class FakeAuthDiskSource : AuthDiskSource { - override var rememberedEmailAddress: String? = null - - override var userState: UserStateJson? = null - set(value) { - field = value - mutableUserStateFlow.tryEmit(value) - } - - override val userStateFlow: Flow - get() = mutableUserStateFlow.onSubscription { emit(userState) } - - private val mutableUserStateFlow = - MutableSharedFlow( - replay = 1, - extraBufferCapacity = Int.MAX_VALUE, - ) -} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseUtil.kt new file mode 100644 index 0000000000..eb7b21c57d --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseUtil.kt @@ -0,0 +1,12 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.model + +fun createMockSyncResponse(number: Int): SyncResponseJson = + SyncResponseJson( + folders = listOf(createMockFolder(number = number)), + collections = listOf(createMockCollection(number = number)), + profile = createMockProfile(number = number), + ciphers = listOf(createMockCipher(number = number)), + policies = listOf(createMockPolicy(number = number)), + domains = createMockDomains(number = number), + sends = listOf(createMockSend(number = number)), + ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt index ff2bf073cc..004f9ee8d4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt @@ -2,14 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service import com.x8bit.bitwarden.data.platform.base.BaseServiceTest import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi -import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockDomains -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockProfile -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse import org.junit.Test @@ -27,7 +20,7 @@ class SyncServiceTest : BaseServiceTest() { fun `sync should return the correct response`() = runTest { server.enqueue(MockResponse().setBody(SYNC_SUCCESS_JSON)) val result = syncService.sync() - assertEquals(SYNC_SUCCESS, result.getOrThrow()) + assertEquals(createMockSyncResponse(number = 1), result.getOrThrow()) } } @@ -363,13 +356,3 @@ private const val SYNC_SUCCESS_JSON = """ ] } """ - -private val SYNC_SUCCESS = SyncResponseJson( - folders = listOf(createMockFolder(number = 1)), - collections = listOf(createMockCollection(number = 1)), - profile = createMockProfile(number = 1), - ciphers = listOf(createMockCipher(number = 1)), - policies = listOf(createMockPolicy(number = 1)), - domains = createMockDomains(number = 1), - sends = listOf(createMockSend(number = 1)), -) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt new file mode 100644 index 0000000000..6fe19f98ba --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -0,0 +1,84 @@ +package com.x8bit.bitwarden.data.vault.repository + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson +import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse +import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkCipher +import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkFolder +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class VaultRepositoryTest { + + private val dispatcherManager: DispatcherManager = FakeDispatcherManager() + private val fakeAuthDiskSource = FakeAuthDiskSource() + private val syncService: SyncService = mockk() + private val vaultSdkSource: VaultSdkSource = mockk() + private val vaultRepository = VaultRepositoryImpl( + syncService = syncService, + vaultSdkSource = vaultSdkSource, + authDiskSource = fakeAuthDiskSource, + dispatcherManager = dispatcherManager, + ) + + @Test + fun `sync when syncService Success should update AuthDiskSource with keys`() = runTest { + coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns mockk() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns mockk() + fakeAuthDiskSource.userState = MOCK_USER_STATE + + vaultRepository.sync() + + fakeAuthDiskSource.assertUserKey( + userId = "mockUserId", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.assertPrivateKey( + userId = "mockUserId", + privateKey = "mockPrivateKey-1", + ) + } +} + +private val MOCK_USER_STATE = UserStateJson( + activeUserId = "mockUserId", + accounts = mapOf( + "mockUserId" to AccountJson( + profile = AccountJson.Profile( + userId = "activeUserId", + email = "email", + isEmailVerified = true, + name = null, + stamp = null, + organizationId = null, + avatarColorHex = null, + hasPremium = true, + forcePasswordResetReason = null, + kdfType = null, + kdfIterations = null, + kdfMemory = null, + kdfParallelism = null, + userDecryptionOptions = null, + ), + tokens = AccountJson.Tokens( + accessToken = "accessToken", + refreshToken = "refreshToken", + ), + settings = AccountJson.Settings( + environmentUrlData = null, + ), + ), + ), +)