diff --git a/sync/sync-impl/lint-baseline.xml b/sync/sync-impl/lint-baseline.xml index b258727a2108..96f067463a0d 100644 --- a/sync/sync-impl/lint-baseline.xml +++ b/sync/sync-impl/lint-baseline.xml @@ -34,6 +34,17 @@ message="Defined here, included via layout/activity_sync.xml => layout/view_sync_disabled.xml defines @+id/syncSetupOtherPlatforms"/> + + + + @@ -118,7 +129,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt index 6128db48fcf2..c5ac8003d753 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt @@ -74,8 +74,11 @@ class AppSyncAccountRepository @Inject constructor( private val syncPixels: SyncPixels, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, + private val syncFeature: SyncFeature, ) : SyncAccountRepository { + private val connectedDevicesCached: MutableList = mutableListOf() + override fun isSyncSupported(): Boolean { return syncStore.isEncryptionSupported() } @@ -108,9 +111,20 @@ class AppSyncAccountRepository @Inject constructor( } private fun login(recoveryCode: RecoveryCode): Result { + var wasUserLogout = false if (isSignedIn()) { - return Error(code = ALREADY_SIGNED_IN.code, reason = "Already signed in") - .alsoFireAlreadySignedInErrorPixel() + val allowSwitchAccount = syncFeature.seamlessAccountSwitching().isEnabled() + val error = Error(code = ALREADY_SIGNED_IN.code, reason = "Already signed in").alsoFireAlreadySignedInErrorPixel() + if (allowSwitchAccount && connectedDevicesCached.size == 1) { + val thisDeviceId = syncStore.deviceId.orEmpty() + val result = logout(thisDeviceId) + if (result is Error) { + return result + } + wasUserLogout = true + } else { + return error + } } val primaryKey = recoveryCode.primaryKey @@ -120,6 +134,9 @@ class AppSyncAccountRepository @Inject constructor( return performLogin(userId, deviceId, deviceName, primaryKey).onFailure { it.alsoFireLoginErrorPixel() + if (wasUserLogout) { + syncPixels.fireUserSwitchedLoginError() + } return it.copy(code = LOGIN_FAILED.code) } } @@ -288,6 +305,7 @@ class AppSyncAccountRepository @Inject constructor( return when (val result = syncApi.getDevices(token)) { is Error -> { + connectedDevicesCached.clear() result.alsoFireAccountErrorPixel().copy(code = GENERIC_ERROR.code) } @@ -315,6 +333,11 @@ class AppSyncAccountRepository @Inject constructor( } }.sortedWith { a, b -> if (a.thisDevice) -1 else 1 + }.also { + connectedDevicesCached.apply { + clear() + addAll(it) + } }, ) } @@ -326,8 +349,17 @@ class AppSyncAccountRepository @Inject constructor( override fun logoutAndJoinNewAccount(stringCode: String): Result { val thisDeviceId = syncStore.deviceId.orEmpty() return when (val result = logout(thisDeviceId)) { - is Error -> result - is Result.Success -> processCode(stringCode) + is Error -> { + syncPixels.fireUserSwitchedLogoutError() + result + } + is Result.Success -> { + val loginResult = processCode(stringCode) + if (loginResult is Error) { + syncPixels.fireUserSwitchedLoginError() + } + loginResult + } } } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt index d2e532722e43..f91c261e0083 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt @@ -45,6 +45,6 @@ interface SyncFeature { @Toggle.DefaultValue(true) fun gzipPatchRequests(): Toggle - @Toggle.DefaultValue(false) + @Toggle.DefaultValue(true) fun seamlessAccountSwitching(): Toggle } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt index 7fca1dd76f17..620cf2b0eb88 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixelParamRemovalPlugin.kt @@ -35,6 +35,13 @@ class SyncPixelParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugi SyncPixelName.SYNC_GET_OTHER_DEVICES_SCREEN_SHOWN.pixelName to PixelParameter.removeAtb(), SyncPixelName.SYNC_GET_OTHER_DEVICES_LINK_COPIED.pixelName to PixelParameter.removeAtb(), SyncPixelName.SYNC_GET_OTHER_DEVICES_LINK_SHARED.pixelName to PixelParameter.removeAtb(), + + SyncPixelName.SYNC_ASK_USER_TO_SWITCH_ACCOUNT.pixelName to PixelParameter.removeAtb(), + SyncPixelName.SYNC_USER_ACCEPTED_SWITCHING_ACCOUNT.pixelName to PixelParameter.removeAtb(), + SyncPixelName.SYNC_USER_CANCELLED_SWITCHING_ACCOUNT.pixelName to PixelParameter.removeAtb(), + SyncPixelName.SYNC_USER_SWITCHED_ACCOUNT.pixelName to PixelParameter.removeAtb(), + SyncPixelName.SYNC_USER_SWITCHED_LOGOUT_ERROR.pixelName to PixelParameter.removeAtb(), + SyncPixelName.SYNC_USER_SWITCHED_LOGIN_ERROR.pixelName to PixelParameter.removeAtb(), ) } } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt index 6a9b55c0fb2b..a52493645079 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/pixels/SyncPixels.kt @@ -81,6 +81,13 @@ interface SyncPixels { feature: SyncableType, apiError: Error, ) + + fun fireAskUserToSwitchAccount() + fun fireUserAcceptedSwitchingAccount() + fun fireUserCancelledSwitchingAccount() + fun fireUserSwitchedAccount() + fun fireUserSwitchedLogoutError() + fun fireUserSwitchedLoginError() } @ContributesBinding(AppScope::class) @@ -255,6 +262,30 @@ class RealSyncPixels @Inject constructor( } } + override fun fireUserSwitchedAccount() { + pixel.fire(SyncPixelName.SYNC_USER_SWITCHED_ACCOUNT) + } + + override fun fireAskUserToSwitchAccount() { + pixel.fire(SyncPixelName.SYNC_ASK_USER_TO_SWITCH_ACCOUNT) + } + + override fun fireUserAcceptedSwitchingAccount() { + pixel.fire(SyncPixelName.SYNC_USER_ACCEPTED_SWITCHING_ACCOUNT) + } + + override fun fireUserCancelledSwitchingAccount() { + pixel.fire(SyncPixelName.SYNC_USER_CANCELLED_SWITCHING_ACCOUNT) + } + + override fun fireUserSwitchedLoginError() { + pixel.fire(SyncPixelName.SYNC_USER_SWITCHED_LOGIN_ERROR) + } + + override fun fireUserSwitchedLogoutError() { + pixel.fire(SyncPixelName.SYNC_USER_SWITCHED_LOGOUT_ERROR) + } + companion object { private const val SYNC_PIXELS_PREF_FILE = "com.duckduckgo.sync.pixels.v1" } @@ -302,6 +333,12 @@ enum class SyncPixelName(override val pixelName: String) : Pixel.PixelName { SYNC_GET_OTHER_DEVICES_SCREEN_SHOWN("sync_get_other_devices"), SYNC_GET_OTHER_DEVICES_LINK_COPIED("sync_get_other_devices_copy"), SYNC_GET_OTHER_DEVICES_LINK_SHARED("sync_get_other_devices_share"), + SYNC_ASK_USER_TO_SWITCH_ACCOUNT("sync_ask_user_to_switch_account"), + SYNC_USER_ACCEPTED_SWITCHING_ACCOUNT("sync_user_accepted_switching_account"), + SYNC_USER_CANCELLED_SWITCHING_ACCOUNT("sync_user_cancelled_switching_account"), + SYNC_USER_SWITCHED_ACCOUNT("sync_user_switched_account"), + SYNC_USER_SWITCHED_LOGOUT_ERROR("sync_user_switched_logout_error"), + SYNC_USER_SWITCHED_LOGIN_ERROR("sync_user_switched_login_error"), } object SyncPixelParameters { diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt index c48dc8757a19..63054c345580 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeActivity.kt @@ -131,6 +131,7 @@ class EnterCodeActivity : DuckDuckGoActivity() { } private fun askUserToSwitchAccount(it: AskToSwitchAccount) { + viewModel.onUserAskedToSwitchAccount() TextAlertDialogBuilder(this) .setTitle(R.string.sync_dialog_switch_account_header) .setMessage(R.string.sync_dialog_switch_account_description) @@ -141,6 +142,10 @@ class EnterCodeActivity : DuckDuckGoActivity() { override fun onPositiveButtonClicked() { viewModel.onUserAcceptedJoiningNewAccount(it.encodedStringCode) } + + override fun onNegativeButtonClicked() { + viewModel.onUserCancelledJoiningNewAccount() + } }, ).show() } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt index 7ca9026cadf4..7a505bca3fa2 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModel.kt @@ -35,6 +35,7 @@ import com.duckduckgo.sync.impl.SyncAccountRepository import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.pixels.SyncPixels import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.AskToSwitchAccount +import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.LoginSuccess import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.ShowError import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.Command.SwitchAccountSuccess import javax.inject.* @@ -90,9 +91,17 @@ class EnterCodeViewModel @Inject constructor( private suspend fun authFlow( pastedCode: String, ) { - val result = syncAccountRepository.processCode(pastedCode) - when (result) { - is Result.Success -> command.send(Command.LoginSuccess) + val userSignedIn = syncAccountRepository.isSignedIn() + when (val result = syncAccountRepository.processCode(pastedCode)) { + is Result.Success -> { + val commandSuccess = if (userSignedIn) { + syncPixels.fireUserSwitchedAccount() + SwitchAccountSuccess + } else { + LoginSuccess + } + command.send(commandSuccess) + } is Result.Error -> { processError(result, pastedCode) } @@ -128,6 +137,7 @@ class EnterCodeViewModel @Inject constructor( fun onUserAcceptedJoiningNewAccount(encodedStringCode: String) { viewModelScope.launch(dispatchers.io()) { + syncPixels.fireUserAcceptedSwitchingAccount() val result = syncAccountRepository.logoutAndJoinNewAccount(encodedStringCode) if (result is Error) { when (result.code) { @@ -143,9 +153,17 @@ class EnterCodeViewModel @Inject constructor( ) } } else { - syncPixels.fireLoginPixel() + syncPixels.fireUserSwitchedAccount() command.send(SwitchAccountSuccess) } } } + + fun onUserCancelledJoiningNewAccount() { + syncPixels.fireUserCancelledSwitchingAccount() + } + + fun onUserAskedToSwitchAccount() { + syncPixels.fireAskUserToSwitchAccount() + } } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt index 46ee532a5673..0c186be3ea44 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherActivityViewModel.kt @@ -136,6 +136,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor( fun onQRCodeScanned(qrCode: String) { viewModelScope.launch(dispatchers.io()) { + val userSignedIn = syncAccountRepository.isSignedIn() when (val result = syncAccountRepository.processCode(qrCode)) { is Error -> { emitError(result, qrCode) @@ -143,7 +144,13 @@ class SyncWithAnotherActivityViewModel @Inject constructor( is Success -> { syncPixels.fireLoginPixel() - command.send(LoginSuccess) + val commandSuccess = if (userSignedIn) { + syncPixels.fireUserSwitchedAccount() + SwitchAccountSuccess + } else { + LoginSuccess + } + command.send(commandSuccess) } } } @@ -175,6 +182,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor( fun onUserAcceptedJoiningNewAccount(encodedStringCode: String) { viewModelScope.launch(dispatchers.io()) { + syncPixels.fireUserAcceptedSwitchingAccount() val result = syncAccountRepository.logoutAndJoinNewAccount(encodedStringCode) if (result is Error) { when (result.code) { @@ -191,6 +199,7 @@ class SyncWithAnotherActivityViewModel @Inject constructor( } } else { syncPixels.fireLoginPixel() + syncPixels.fireUserSwitchedAccount() command.send(SwitchAccountSuccess) } } @@ -211,4 +220,12 @@ class SyncWithAnotherActivityViewModel @Inject constructor( } } } + + fun onUserCancelledJoiningNewAccount() { + syncPixels.fireUserCancelledSwitchingAccount() + } + + fun onUserAskedToSwitchAccount() { + syncPixels.fireAskUserToSwitchAccount() + } } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt index 8883523a1e80..3a51582e64f9 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceActivity.kt @@ -133,6 +133,7 @@ class SyncWithAnotherDeviceActivity : DuckDuckGoActivity() { } private fun askUserToSwitchAccount(it: AskToSwitchAccount) { + viewModel.onUserAskedToSwitchAccount() TextAlertDialogBuilder(this) .setTitle(R.string.sync_dialog_switch_account_header) .setMessage(R.string.sync_dialog_switch_account_description) @@ -143,6 +144,10 @@ class SyncWithAnotherDeviceActivity : DuckDuckGoActivity() { override fun onPositiveButtonClicked() { viewModel.onUserAcceptedJoiningNewAccount(it.encodedStringCode) } + + override fun onNegativeButtonClicked() { + viewModel.onUserCancelledJoiningNewAccount() + } }, ).show() } diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt index 9bbdc1f225a7..120d523b784c 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/TestSyncFixtures.kt @@ -140,7 +140,8 @@ object TestSyncFixtures { ) val loginSuccessResponse: Response = Response.success(loginResponseBody) - val listOfDevices = listOf(Device(deviceId = deviceId, deviceName = deviceName, jwIat = "", deviceType = deviceFactor)) + val aDevice = Device(deviceId = deviceId, deviceName = deviceName, jwIat = "", deviceType = deviceFactor) + val listOfDevices = listOf(aDevice) val deviceResponse = DeviceResponse(DeviceEntries(listOfDevices)) val getDevicesBodySuccessResponse: Response = Response.success(deviceResponse) val getDevicesBodyErrorResponse: Response = Response.error( diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt index 9fd80aba10b7..09c5d17cfc96 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt @@ -18,6 +18,9 @@ package com.duckduckgo.sync.impl import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.common.utils.DefaultDispatcherProvider +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.sync.TestSyncFixtures.aDevice import com.duckduckgo.sync.TestSyncFixtures.accountCreatedFailDupUser import com.duckduckgo.sync.TestSyncFixtures.accountCreatedSuccess import com.duckduckgo.sync.TestSyncFixtures.accountKeys @@ -65,9 +68,8 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success -import com.duckduckgo.sync.impl.pixels.* +import com.duckduckgo.sync.impl.pixels.SyncPixels import com.duckduckgo.sync.store.SyncStore -import java.lang.RuntimeException import kotlinx.coroutines.test.TestScope import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -94,13 +96,25 @@ class AppSyncAccountRepositoryTest { private var syncStore: SyncStore = mock() private var syncEngine: SyncEngine = mock() private var syncPixels: SyncPixels = mock() + private val syncFeature = FakeFeatureToggleFactory.create(SyncFeature::class.java).apply { + this.seamlessAccountSwitching().setRawStoredState(State(true)) + } private lateinit var syncRepo: SyncAccountRepository @Before fun before() { - syncRepo = - AppSyncAccountRepository(syncDeviceIds, nativeLib, syncApi, syncStore, syncEngine, syncPixels, TestScope(), DefaultDispatcherProvider()) + syncRepo = AppSyncAccountRepository( + syncDeviceIds, + nativeLib, + syncApi, + syncStore, + syncEngine, + syncPixels, + TestScope(), + DefaultDispatcherProvider(), + syncFeature, + ) } @Test @@ -231,6 +245,39 @@ class AppSyncAccountRepositoryTest { ) } + @Test + fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenSwitchAccountIfOnly1DeviceConnected() { + givenAuthenticatedDevice() + givenAccountWithConnectedDevices(1) + doAnswer { + givenUnauthenticatedDevice() // simulate logout locally + logoutSuccess + }.`when`(syncApi).logout(token, deviceId) + prepareForLoginSuccess() + + val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + + verify(syncApi).logout(token, deviceId) + verify(syncApi).login(userId, hashedPassword, deviceId, deviceName, deviceFactor) + + assertTrue(result is Success) + } + + @Test + fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenReturnErrorIfMultipleDevicesConnected() { + givenAuthenticatedDevice() + givenAccountWithConnectedDevices(2) + doAnswer { + givenUnauthenticatedDevice() // simulate logout locally + logoutSuccess + }.`when`(syncApi).logout(token, deviceId) + prepareForLoginSuccess() + + val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + + assertEquals((result as Error).code, ALREADY_SIGNED_IN.code) + } + @Test fun whenLogoutAndJoinNewAccountSucceedsThenReturnSuccess() { givenAuthenticatedDevice() @@ -573,4 +620,15 @@ class AppSyncAccountRepositoryTest { EncryptResult(0, it.arguments.first() as String) } } + + private fun givenAccountWithConnectedDevices(size: Int) { + prepareForEncryption() + val listOfDevices = mutableListOf() + for (i in 0 until size) { + listOfDevices.add(aDevice.copy(deviceId = "device$i")) + } + whenever(syncApi.getDevices(anyString())).thenReturn(Success(listOfDevices)) + + syncRepo.getConnectedDevices() as Success + } } diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt index eed3a058b789..4979289f791b 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/EnterCodeViewModelTest.kt @@ -47,6 +47,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -173,6 +174,34 @@ internal class EnterCodeViewModelTest { } } + @Test + fun whenSignedInUserScansRecoveryCodeAndLoginSucceedsThenReturnSwitchAccount() = runTest { + whenever(syncAccountRepository.isSignedIn()).thenReturn(true) + whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true)) + whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) + + testee.commands().test { + testee.onPasteCodeClicked() + val command = awaitItem() + assertTrue(command is SwitchAccountSuccess) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest { + whenever(syncAccountRepository.isSignedIn()).thenReturn(false) + whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true)) + whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) + + testee.commands().test { + testee.onPasteCodeClicked() + val command = awaitItem() + assertTrue(command is LoginSuccess) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun whenProcessCodeAndLoginFailsThenShowError() = runTest { whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt index 6b408e780542..9fd327b4edd6 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt @@ -168,6 +168,34 @@ class SyncWithAnotherDeviceViewModelTest { } } + @Test + fun whenSignedInUserScansRecoveryCodeAndLoginSucceedsThenReturnSwitchAccount() = runTest { + whenever(syncRepository.isSignedIn()).thenReturn(true) + whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true)) + + testee.commands().test { + testee.onQRCodeScanned(jsonRecoveryKeyEncoded) + val command = awaitItem() + assertTrue(command is SwitchAccountSuccess) + verify(syncPixels, times(1)).fireLoginPixel() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest { + whenever(syncRepository.isSignedIn()).thenReturn(false) + whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true)) + + testee.commands().test { + testee.onQRCodeScanned(jsonRecoveryKeyEncoded) + val command = awaitItem() + assertTrue(command is LoginSuccess) + verify(syncPixels, times(1)).fireLoginPixel() + cancelAndIgnoreRemainingEvents() + } + } + @Test fun whenUserScansRecoveryQRCodeAndConnectDeviceFailsThenCommandIsError() = runTest { whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = LOGIN_FAILED.code))