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 0f5df5b3a9d9..526d34358b58 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 @@ -33,11 +33,15 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.EXCHANGE_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.GENERIC_ERROR import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED -import com.duckduckgo.sync.impl.CodeType.UNKNOWN +import com.duckduckgo.sync.impl.CodeType.Connect +import com.duckduckgo.sync.impl.CodeType.Exchange +import com.duckduckgo.sync.impl.CodeType.Recovery +import com.duckduckgo.sync.impl.CodeType.Unknown import com.duckduckgo.sync.impl.ExchangeResult.* 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.ui.qrcode.SyncBarcodeUrl import com.duckduckgo.sync.store.* import com.squareup.anvil.annotations.* import com.squareup.moshi.* @@ -54,7 +58,7 @@ interface SyncAccountRepository { fun isSyncSupported(): Boolean fun createAccount(): Result fun isSignedIn(): Boolean - fun processCode(stringCode: String): Result + fun processCode(code: CodeType): Result fun getAccountInfo(): AccountInfo fun logout(deviceId: String): Result fun deleteAccount(): Result @@ -114,58 +118,72 @@ class AppSyncAccountRepository @Inject constructor( } } - override fun processCode(stringCode: String): Result { - val decodedCode: String? = kotlin.runCatching { - return@runCatching stringCode.decodeB64() - }.getOrNull() - if (decodedCode == null) { - Timber.w("Failed while b64 decoding barcode; barcode is unusable") - return Error(code = INVALID_CODE.code, reason = "Failed to decode code") - } - - kotlin.runCatching { - Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery - }.getOrNull()?.let { - Timber.d("Sync: code is a recovery code") - return login(it) - } + override fun processCode(code: CodeType): Result { + when (code) { + is Recovery -> { + Timber.d("Sync: code is a recovery code") + return login(code.code) + } - kotlin.runCatching { - Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect - }.getOrNull()?.let { - Timber.d("Sync: code is a connect code") - return connectDevice(it) - } + is Connect -> { + Timber.d("Sync: code is a connect code") + return connectDevice(code.code) + } - kotlin.runCatching { - Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey - }.getOrNull()?.let { - if (!syncFeature.exchangeKeysToSyncWithAnotherDevice().isEnabled()) { - Timber.w("Sync: Scanned exchange code type but exchanging keys to sync with another device is disabled") - return@let null + is Exchange -> { + if (!syncFeature.exchangeKeysToSyncWithAnotherDevice().isEnabled()) { + Timber.w("Sync: Scanned exchange code type but exchanging keys to sync with another device is disabled") + } else { + return onInvitationCodeReceived(code.code) + } } - return onInvitationCodeReceived(it) + else -> { + Timber.d("Sync: code type unknown") + } } - - Timber.e("Sync: code is not supported") + Timber.e("Sync: code type (${code.javaClass.simpleName}) is not supported") return Error(code = INVALID_CODE.code, reason = "Failed to decode code") } override fun getCodeType(stringCode: String): CodeType { + // check first if it's a URL which contains the code + val (code, wasInUrl) = kotlin.runCatching { + SyncBarcodeUrl.parseUrl(stringCode)?.webSafeB64EncodedCode?.removeUrlSafetyToRestoreB64() + ?.let { Pair(it, true) } + ?: Pair(stringCode, false) + }.getOrDefault(Pair(stringCode, false)) + + if (wasInUrl && syncFeature.canScanUrlBasedSyncSetupBarcodes().isEnabled().not()) { + Timber.e("Feature to allow scanning URL-based sync setup codes is disabled") + return Unknown(code) + } + return kotlin.runCatching { - val decodedCode = stringCode.decodeB64() - when { - Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery != null -> CodeType.RECOVERY - Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect != null -> CodeType.CONNECT - Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey != null -> CodeType.EXCHANGE - else -> UNKNOWN - } - }.onFailure { + val decodedCode = code.decodeB64() + + canParseAsRecoveryCode(decodedCode)?.let { + if (wasInUrl) { + throw IllegalArgumentException("Sync: Recovery code found inside a URL which is not acceptable") + } else { + Recovery(it) + } + } + ?: canParseAsExchangeCode(decodedCode)?.let { Exchange(it) } + ?: canParseAsConnectCode(decodedCode)?.let { Connect(it) } + ?: Unknown(code) + }.onSuccess { + Timber.i("Sync: code type is ${it.javaClass.simpleName}. was inside url: $wasInUrl") + }.getOrElse { Timber.e(it, "Failed to decode code") - }.getOrDefault(UNKNOWN) + Unknown(code) + } } + private fun canParseAsRecoveryCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.recovery + private fun canParseAsExchangeCode(decodedCode: String) = Adapters.invitationCodeAdapter.fromJson(decodedCode)?.exchangeKey + private fun canParseAsConnectCode(decodedCode: String) = Adapters.recoveryCodeAdapter.fromJson(decodedCode)?.connect + private fun onInvitationCodeReceived(invitationCode: InvitationCode): Result { // Sync: InviteFlow - B (https://app.asana.com/0/72649045549333/1209571867429615) Timber.d("Sync-exchange: InviteFlow - B. code is an exchange code $invitationCode") @@ -596,7 +614,8 @@ class AppSyncAccountRepository @Inject constructor( } is Success -> { - val loginResult = processCode(stringCode) + val codeType = getCodeType(stringCode) + val loginResult = processCode(codeType) if (loginResult is Error) { syncPixels.fireUserSwitchedLoginError() } @@ -882,11 +901,11 @@ enum class AccountErrorCodes(val code: Int) { EXCHANGE_FAILED(56), } -enum class CodeType { - RECOVERY, - CONNECT, - EXCHANGE, - UNKNOWN, +sealed interface CodeType { + data class Recovery(val code: RecoveryCode) : CodeType + data class Connect(val code: ConnectCode) : CodeType + data class Exchange(val code: InvitationCode) : CodeType + data class Unknown(val code: String) : CodeType } sealed class Result { 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 9946c5617101..d3e9a76fab84 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 @@ -56,4 +56,7 @@ interface SyncFeature { @Toggle.DefaultValue(DefaultFeatureValue.FALSE) fun syncSetupBarcodeIsUrlBased(): Toggle + + @Toggle.DefaultValue(DefaultFeatureValue.TRUE) + fun canScanUrlBasedSyncSetupBarcodes(): Toggle } 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 3b67fa4fd98f..190abafed6e9 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 @@ -28,7 +28,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Clipboard -import com.duckduckgo.sync.impl.CodeType.EXCHANGE +import com.duckduckgo.sync.impl.CodeType import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -101,9 +101,9 @@ class EnterCodeViewModel @Inject constructor( ) { val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey val codeType = syncAccountRepository.getCodeType(pastedCode) - when (val result = syncAccountRepository.processCode(pastedCode)) { + when (val result = syncAccountRepository.processCode(codeType)) { is Result.Success -> { - if (codeType == EXCHANGE) { + if (codeType is CodeType.Exchange) { pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, code = pastedCode) } else { onLoginSuccess(previousPrimaryKey) diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModel.kt index 6fa7f4d85517..18e362eaf67d 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModel.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModel.kt @@ -29,7 +29,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Clipboard -import com.duckduckgo.sync.impl.CodeType.EXCHANGE +import com.duckduckgo.sync.impl.CodeType.Exchange import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -178,13 +178,13 @@ class SyncConnectViewModel @Inject constructor( fun onQRCodeScanned(qrCode: String) { viewModelScope.launch(dispatchers.io()) { val codeType = syncAccountRepository.getCodeType(qrCode) - when (val result = syncAccountRepository.processCode(qrCode)) { + when (val result = syncAccountRepository.processCode(codeType)) { is Error -> { processError(result) } is Success -> { - if (codeType == EXCHANGE) { + if (codeType is Exchange) { pollForRecoveryKey() } else { syncPixels.fireLoginPixel() diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncInternalSettingsViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncInternalSettingsViewModel.kt index 0e18c0fd99df..21f774fbe8ac 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncInternalSettingsViewModel.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncInternalSettingsViewModel.kt @@ -212,7 +212,8 @@ constructor( fun onQRScanned(contents: String) { viewModelScope.launch(dispatchers.io()) { - val result = syncAccountRepository.processCode(contents) + val codeType = syncAccountRepository.getCodeType(contents) + val result = syncAccountRepository.processCode(codeType) if (result is Error) { command.send(Command.ShowMessage("$result")) } @@ -222,7 +223,8 @@ constructor( fun onConnectQRScanned(contents: String) { viewModelScope.launch(dispatchers.io()) { - val result = syncAccountRepository.processCode(contents) + val codeType = syncAccountRepository.getCodeType(contents) + val result = syncAccountRepository.processCode(codeType) when (result) { is Error -> { command.send(Command.ShowMessage("$result")) @@ -291,7 +293,8 @@ constructor( private suspend fun authFlow( pastedCode: String, ) { - val result = syncAccountRepository.processCode(pastedCode) + val codeType = syncAccountRepository.getCodeType(pastedCode) + val result = syncAccountRepository.processCode(codeType) when (result) { is Result.Success -> command.send(Command.LoginSuccess) is Result.Error -> { diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModel.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModel.kt index 4b418022570c..3a604d3c29bf 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModel.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModel.kt @@ -27,7 +27,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CONNECT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED -import com.duckduckgo.sync.impl.CodeType.EXCHANGE +import com.duckduckgo.sync.impl.CodeType import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -88,13 +88,13 @@ class SyncLoginViewModel @Inject constructor( fun onQRCodeScanned(qrCode: String) { viewModelScope.launch(dispatchers.io()) { val codeType = syncAccountRepository.getCodeType(qrCode) - when (val result = syncAccountRepository.processCode(qrCode)) { + when (val result = syncAccountRepository.processCode(codeType)) { is Error -> { processError(result) } is Success -> { - if (codeType == EXCHANGE) { + if (codeType is CodeType.Exchange) { pollForRecoveryKey() } else { syncPixels.fireLoginPixel() 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 58c7ba03f34f..85972207e453 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 @@ -29,7 +29,7 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Clipboard -import com.duckduckgo.sync.impl.CodeType.EXCHANGE +import com.duckduckgo.sync.impl.CodeType import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -178,14 +178,14 @@ class SyncWithAnotherActivityViewModel @Inject constructor( viewModelScope.launch(dispatchers.io()) { val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey val codeType = syncAccountRepository.getCodeType(qrCode) - when (val result = syncAccountRepository.processCode(qrCode)) { + when (val result = syncAccountRepository.processCode(codeType)) { is Error -> { Timber.w("Sync: error processing code ${result.reason}") emitError(result, qrCode) } is Success -> { - if (codeType == EXCHANGE) { + if (codeType is CodeType.Exchange) { pollForRecoveryKey(previousPrimaryKey = previousPrimaryKey, qrCode = qrCode) } else { onLoginSuccess(previousPrimaryKey) 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 e010aee5e27f..4e8090a95c3b 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 @@ -74,6 +74,7 @@ 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.SyncPixels +import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeUrl import com.duckduckgo.sync.store.SyncStore import com.squareup.moshi.Moshi import kotlinx.coroutines.test.TestScope @@ -246,7 +247,7 @@ class AppSyncAccountRepositoryTest { fun whenProcessJsonRecoveryCodeSucceedsThenAccountPersisted() { prepareForLoginSuccess() - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + val result = syncRepo.processCode(syncRepo.getCodeType(jsonRecoveryKeyEncoded)) assertEquals(Success(true), result) verify(syncStore).storeCredentials( @@ -266,7 +267,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) - val result = syncRepo.processCode(exchangeCode.encodeB64()) + val result = syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) assertTrue(result is Error) } @@ -277,7 +278,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - val result = syncRepo.processCode(exchangeCode.encodeB64()) + val result = syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) assertTrue(result is Success) verify(syncApi).sendEncryptedMessage(eq(primaryDeviceKeyId), eq(encryptedExchangeCode)) @@ -292,7 +293,7 @@ class AppSyncAccountRepositoryTest { whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) - val result = syncRepo.processCode(exchangeCode.encodeB64()) + val result = syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) assertTrue(result is Error) } @@ -336,7 +337,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) whenever(syncApi.getEncryptedMessage(otherDeviceKeyId)).thenReturn(Success("encryptedExchangeResponse")) whenever(nativeLib.sealOpen("encryptedExchangeResponse", primaryKey, secretKey)).thenReturn("invalid response") @@ -350,7 +351,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) configureExchangeResultRecoveryReceived() @@ -365,7 +366,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) configureExchangeResultRecoveryReceived() prepareForLoginSuccess() @@ -381,7 +382,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.getCodeType(exchangeCode.encodeB64())) configureExchangeResultRecoveryReceived() prepareForLoginSuccess() @@ -414,21 +415,21 @@ class AppSyncAccountRepositoryTest { @Test fun whenCodeIsEmptyThenCodeTypeIsUnknown() { val type = syncRepo.getCodeType("") - assertTrue(type == CodeType.UNKNOWN) + assertTrue(type is CodeType.Unknown) } @Test fun whenCodeIsRecoveryThenCodeTypeIsIdentified() { val code = recoveryCodeAdapter.toJson((LinkCode(recovery = RecoveryCode(primaryKey, "userId")))) val type = syncRepo.getCodeType(code.encodeB64()) - assertTrue(type == CodeType.RECOVERY) + assertTrue(type is CodeType.Recovery) } @Test fun whenCodeIsConnectThenCodeTypeIsIdentified() { val code = recoveryCodeAdapter.toJson((LinkCode(connect = ConnectCode(deviceId, secretKey)))) val type = syncRepo.getCodeType(code.encodeB64()) - assertTrue(type == CodeType.CONNECT) + assertTrue(type is CodeType.Connect) } @Test @@ -436,7 +437,35 @@ class AppSyncAccountRepositoryTest { val invitationCode = InvitationCode(keyId = primaryDeviceKeyId, publicKey = validLoginKeys.primaryKey) val code = invitationCodeWrapperAdapter.toJson(InvitationCodeWrapper(exchangeKey = invitationCode)) val type = syncRepo.getCodeType(code.encodeB64()) - assertTrue(type == CodeType.EXCHANGE) + assertTrue(type is CodeType.Exchange) + } + + @Test + fun whenCodeIsUrlWithConnectInsideThenCodeTypeIsIdentified() { + val code = recoveryCodeAdapter.toJson((LinkCode(connect = ConnectCode(deviceId, secretKey)))) + val webSafeCode = code.encodeB64().applyUrlSafetyFromB64() + val url = SyncBarcodeUrl(webSafeCode).asUrl() + val type = syncRepo.getCodeType(url) + assertTrue(type is CodeType.Connect) + } + + @Test + fun whenCodeIsUrlWithExchangeInsideThenCodeTypeIsIdentified() { + val invitationCode = InvitationCode(keyId = primaryDeviceKeyId, publicKey = validLoginKeys.primaryKey) + val code = invitationCodeWrapperAdapter.toJson(InvitationCodeWrapper(exchangeKey = invitationCode)) + val webSafeCode = code.encodeB64().applyUrlSafetyFromB64() + val url = SyncBarcodeUrl(webSafeCode).asUrl() + val type = syncRepo.getCodeType(url) + assertTrue(type is CodeType.Exchange) + } + + @Test + fun whenCodeIsUrlWithRecoveryInsideThenCodeTypeIsNotIdentified() { + val code = recoveryCodeAdapter.toJson((LinkCode(recovery = RecoveryCode(deviceId, secretKey)))) + val webSafeCode = code.encodeB64().applyUrlSafetyFromB64() + val url = SyncBarcodeUrl(webSafeCode).asUrl() + val type = syncRepo.getCodeType(url) + assertTrue(type is CodeType.Unknown) } @Test @@ -449,7 +478,7 @@ class AppSyncAccountRepositoryTest { }.`when`(syncApi).logout(token, deviceId) prepareForLoginSuccess() - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + val result = syncRepo.processCode(syncRepo.getCodeType(jsonRecoveryKeyEncoded)) verify(syncApi).logout(token, deviceId) verify(syncApi).login(userId, hashedPassword, deviceId, deviceName, deviceFactor) @@ -467,7 +496,7 @@ class AppSyncAccountRepositoryTest { }.`when`(syncApi).logout(token, deviceId) prepareForLoginSuccess() - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + val result = syncRepo.processCode(syncRepo.getCodeType(jsonRecoveryKeyEncoded)) assertEquals((result as Error).code, ALREADY_SIGNED_IN.code) } @@ -500,7 +529,7 @@ class AppSyncAccountRepositoryTest { prepareToProvideDeviceIds() whenever(nativeLib.prepareForLogin(primaryKey = primaryKey)).thenReturn(failedLoginKeys) - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) as Error + val result = syncRepo.processCode(syncRepo.getCodeType(jsonRecoveryKeyEncoded)) as Error assertEquals(LOGIN_FAILED.code, result.code) } @@ -512,7 +541,7 @@ class AppSyncAccountRepositoryTest { whenever(nativeLib.prepareForLogin(primaryKey = primaryKey)).thenReturn(validLoginKeys) whenever(syncApi.login(userId, hashedPassword, deviceId, deviceName, deviceFactor)).thenReturn(loginFailed) - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) as Error + val result = syncRepo.processCode(syncRepo.getCodeType(jsonRecoveryKeyEncoded)) as Error assertEquals(LOGIN_FAILED.code, result.code) } @@ -525,14 +554,14 @@ class AppSyncAccountRepositoryTest { whenever(nativeLib.decrypt(encryptedData = protectedEncryptionKey, secretKey = stretchedPrimaryKey)).thenReturn(invalidDecryptedSecretKey) whenever(syncApi.login(userId, hashedPassword, deviceId, deviceName, deviceFactor)).thenReturn(loginSuccess) - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) as Error + val result = syncRepo.processCode(syncRepo.getCodeType(jsonRecoveryKeyEncoded)) as Error assertEquals(LOGIN_FAILED.code, result.code) } @Test fun whenProcessInvalidCodeThenReturnInvalidCodeError() { - val result = syncRepo.processCode("invalidCode") as Error + val result = syncRepo.processCode(syncRepo.getCodeType("invalidCode")) as Error assertEquals(INVALID_CODE.code, result.code) } @@ -627,7 +656,7 @@ class AppSyncAccountRepositoryTest { whenever(nativeLib.seal(jsonRecoveryKey, primaryKey)).thenReturn(encryptedRecoveryCode) whenever(syncApi.connect(token, deviceId, encryptedRecoveryCode)).thenReturn(Success(true)) - val result = syncRepo.processCode(jsonConnectKeyEncoded) + val result = syncRepo.processCode(syncRepo.getCodeType(jsonConnectKeyEncoded)) verify(syncApi).connect(token, deviceId, encryptedRecoveryCode) assertTrue(result is Success) @@ -645,7 +674,7 @@ class AppSyncAccountRepositoryTest { whenever(nativeLib.seal(jsonRecoveryKey, primaryKey)).thenReturn(encryptedRecoveryCode) whenever(syncApi.connect(token, deviceId, encryptedRecoveryCode)).thenReturn(Success(true)) - val result = syncRepo.processCode(jsonConnectKeyEncoded) + val result = syncRepo.processCode(syncRepo.getCodeType(jsonConnectKeyEncoded)) verify(syncApi).connect(token, deviceId, encryptedRecoveryCode) assertTrue(result is 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 bab80c41f5cc..07ac168aba6f 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 @@ -24,8 +24,11 @@ import com.duckduckgo.feature.toggles.api.Toggle.State import com.duckduckgo.sync.SyncAccountFixtures.accountA import com.duckduckgo.sync.SyncAccountFixtures.accountB import com.duckduckgo.sync.SyncAccountFixtures.noAccount +import com.duckduckgo.sync.TestSyncFixtures.jsonConnectKey import com.duckduckgo.sync.TestSyncFixtures.jsonConnectKeyEncoded +import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKey import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKeyEncoded +import com.duckduckgo.sync.TestSyncFixtures.primaryKey import com.duckduckgo.sync.impl.AccountErrorCodes.ALREADY_SIGNED_IN import com.duckduckgo.sync.impl.AccountErrorCodes.CONNECT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.CREATE_ACCOUNT_FAILED @@ -33,6 +36,9 @@ import com.duckduckgo.sync.impl.AccountErrorCodes.GENERIC_ERROR import com.duckduckgo.sync.impl.AccountErrorCodes.INVALID_CODE import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Clipboard +import com.duckduckgo.sync.impl.CodeType.Recovery +import com.duckduckgo.sync.impl.CodeType.Unknown +import com.duckduckgo.sync.impl.RecoveryCode import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository @@ -49,6 +55,7 @@ import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -98,7 +105,8 @@ internal class EnterCodeViewModelTest { fun whenUserClicksOnPasteCodeWithRecoveryCodeThenProcessCode() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) Success(true) } @@ -116,7 +124,8 @@ internal class EnterCodeViewModelTest { fun whenUserClicksOnPasteCodeWithConnectCodeThenProcessCode() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonConnectKeyEncoded) - whenever(syncAccountRepository.processCode(jsonConnectKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.getCodeType(jsonConnectKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonConnectKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) Success(true) } @@ -134,7 +143,8 @@ internal class EnterCodeViewModelTest { fun whenPastedInvalidCodeThenAuthStateError() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn("invalid code") - whenever(syncAccountRepository.processCode("invalid code")).thenReturn(Error(code = INVALID_CODE.code)) + whenever(syncAccountRepository.getCodeType("invalid code")).thenReturn(Unknown("invalid code")) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = INVALID_CODE.code)) testee.onPasteCodeClicked() @@ -150,7 +160,8 @@ internal class EnterCodeViewModelTest { syncFeature.seamlessAccountSwitching().setRawStoredState(State(false)) whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = ALREADY_SIGNED_IN.code)) testee.onPasteCodeClicked() @@ -165,7 +176,8 @@ internal class EnterCodeViewModelTest { fun whenProcessCodeButUserSignedInThenOfferToSwitchAccount() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = ALREADY_SIGNED_IN.code)) testee.onPasteCodeClicked() @@ -196,7 +208,8 @@ internal class EnterCodeViewModelTest { @Test fun whenSignedInUserProcessCodeSucceedsAndAccountChangedThenReturnSwitchAccount() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountB) Success(true) } @@ -213,7 +226,8 @@ internal class EnterCodeViewModelTest { @Test fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) Success(true) } @@ -231,7 +245,8 @@ internal class EnterCodeViewModelTest { fun whenProcessCodeAndLoginFailsThenShowError() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Error(code = LOGIN_FAILED.code)) + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = LOGIN_FAILED.code)) testee.onPasteCodeClicked() @@ -246,7 +261,8 @@ internal class EnterCodeViewModelTest { fun whenProcessCodeAndConnectFailsThenShowError() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonConnectKeyEncoded) - whenever(syncAccountRepository.processCode(jsonConnectKeyEncoded)).thenReturn(Error(code = CONNECT_FAILED.code)) + whenever(syncAccountRepository.getCodeType(jsonConnectKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonConnectKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = CONNECT_FAILED.code)) testee.onPasteCodeClicked() @@ -261,7 +277,8 @@ internal class EnterCodeViewModelTest { fun whenProcessCodeAndCreateAccountFailsThenShowError() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Error(code = CREATE_ACCOUNT_FAILED.code)) + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = CREATE_ACCOUNT_FAILED.code)) testee.onPasteCodeClicked() @@ -276,7 +293,8 @@ internal class EnterCodeViewModelTest { fun whenProcessCodeAndGenericErrorThenDoNothing() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Error(code = GENERIC_ERROR.code)) + whenever(syncAccountRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = GENERIC_ERROR.code)) testee.onPasteCodeClicked() diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModelTest.kt index c583590dfe32..de7b2d79d75f 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModelTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncConnectViewModelTest.kt @@ -20,13 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.sync.TestSyncFixtures +import com.duckduckgo.sync.TestSyncFixtures.jsonConnectKey import com.duckduckgo.sync.TestSyncFixtures.jsonConnectKeyEncoded +import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKey import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKeyEncoded +import com.duckduckgo.sync.TestSyncFixtures.primaryKey import com.duckduckgo.sync.impl.AccountErrorCodes.ALREADY_SIGNED_IN import com.duckduckgo.sync.impl.AccountErrorCodes.CONNECT_FAILED import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Clipboard +import com.duckduckgo.sync.impl.CodeType.Connect +import com.duckduckgo.sync.impl.CodeType.Recovery +import com.duckduckgo.sync.impl.ConnectCode import com.duckduckgo.sync.impl.QREncoder +import com.duckduckgo.sync.impl.RecoveryCode import com.duckduckgo.sync.impl.Result import com.duckduckgo.sync.impl.SyncAccountRepository import com.duckduckgo.sync.impl.pixels.SyncPixels @@ -171,7 +178,8 @@ class SyncConnectViewModelTest { @Test fun whenUserScansConnectQRCodeAndConnectDeviceSucceedsThenCommandIsLoginSuccess() = runTest { - whenever(syncRepository.processCode(jsonConnectKeyEncoded)).thenReturn(Result.Success(true)) + whenever(syncRepository.getCodeType(jsonConnectKeyEncoded)).thenReturn(Connect(ConnectCode(jsonConnectKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Success(true)) testee.commands().test { testee.onQRCodeScanned(jsonConnectKeyEncoded) val command = awaitItem() @@ -183,7 +191,8 @@ class SyncConnectViewModelTest { @Test fun whenUserScansRecoveryCodeButSignedInThenCommandIsError() = runTest { - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) testee.commands().test { testee.onQRCodeScanned(jsonRecoveryKeyEncoded) val command = awaitItem() @@ -195,7 +204,9 @@ class SyncConnectViewModelTest { @Test fun whenUserScansConnectQRCodeAndConnectDeviceFailsThenCommandIsError() = runTest { - whenever(syncRepository.processCode(jsonConnectKeyEncoded)).thenReturn(Result.Error(code = CONNECT_FAILED.code)) + whenever(syncRepository.getCodeType(jsonConnectKeyEncoded)).thenReturn(Connect(ConnectCode(jsonConnectKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = CONNECT_FAILED.code)) + testee.commands().test { testee.onQRCodeScanned(jsonConnectKeyEncoded) val command = awaitItem() diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModelTest.kt index 90ea44713608..d76e60a243b1 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModelTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncLoginViewModelTest.kt @@ -19,7 +19,11 @@ package com.duckduckgo.sync.impl.ui import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKey import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKeyEncoded +import com.duckduckgo.sync.TestSyncFixtures.primaryKey +import com.duckduckgo.sync.impl.CodeType.Recovery +import com.duckduckgo.sync.impl.RecoveryCode import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository import com.duckduckgo.sync.impl.pixels.SyncPixels @@ -29,6 +33,7 @@ import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -60,7 +65,8 @@ class SyncLoginViewModelTest { @Test fun whenProcessRecoveryCodeThenPerformLoginAndEmitResult() = runTest { - whenever(syncRepostitory.processCode(jsonRecoveryKeyEncoded)).thenReturn(Success(true)) + whenever(syncRepostitory.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepostitory.processCode(any())).thenReturn(Success(true)) testee.commands().test { testee.onQRCodeScanned(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 9f75d98cb725..9fe33fd78496 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 @@ -29,16 +29,21 @@ import com.duckduckgo.sync.SyncAccountFixtures.noAccount import com.duckduckgo.sync.TestSyncFixtures import com.duckduckgo.sync.TestSyncFixtures.encryptedRecoveryCode import com.duckduckgo.sync.TestSyncFixtures.jsonExchangeKey +import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKey import com.duckduckgo.sync.TestSyncFixtures.jsonRecoveryKeyEncoded import com.duckduckgo.sync.TestSyncFixtures.primaryDeviceKeyId +import com.duckduckgo.sync.TestSyncFixtures.primaryKey import com.duckduckgo.sync.TestSyncFixtures.validLoginKeys import com.duckduckgo.sync.impl.AccountErrorCodes.ALREADY_SIGNED_IN import com.duckduckgo.sync.impl.AccountErrorCodes.LOGIN_FAILED import com.duckduckgo.sync.impl.Clipboard -import com.duckduckgo.sync.impl.CodeType.EXCHANGE +import com.duckduckgo.sync.impl.CodeType.Exchange +import com.duckduckgo.sync.impl.CodeType.Recovery import com.duckduckgo.sync.impl.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn +import com.duckduckgo.sync.impl.InvitationCode import com.duckduckgo.sync.impl.QREncoder +import com.duckduckgo.sync.impl.RecoveryCode import com.duckduckgo.sync.impl.Result import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository @@ -232,7 +237,8 @@ class SyncWithAnotherDeviceViewModelTest { fun whenUserScansRecoveryCodeButSignedInThenCommandIsError() = runTest { syncFeature.seamlessAccountSwitching().setRawStoredState(State(false)) whenever(syncRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) testee.commands().test { testee.onQRCodeScanned(jsonRecoveryKeyEncoded) val command = awaitItem() @@ -247,7 +253,8 @@ class SyncWithAnotherDeviceViewModelTest { configureExchangeKeysSupported() syncFeature.seamlessAccountSwitching().setRawStoredState(State(false)) whenever(syncRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) testee.commands().test { testee.onQRCodeScanned(jsonRecoveryKeyEncoded) val command = awaitItem() @@ -260,7 +267,8 @@ class SyncWithAnotherDeviceViewModelTest { @Test fun whenUserScansRecoveryCodeButSignedInThenCommandIsAskToSwitchAccount() = runTest { whenever(syncRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) testee.commands().test { testee.onQRCodeScanned(jsonRecoveryKeyEncoded) val command = awaitItem() @@ -274,7 +282,8 @@ class SyncWithAnotherDeviceViewModelTest { fun whenUserScansRecoveryCodeAndExchangingKeysEnabledButSignedInThenCommandIsAskToSwitchAccount() = runTest { configureExchangeKeysSupported() whenever(syncRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) testee.commands().test { testee.onQRCodeScanned(jsonRecoveryKeyEncoded) val command = awaitItem() @@ -352,7 +361,8 @@ class SyncWithAnotherDeviceViewModelTest { @Test fun whenSignedInUserScansRecoveryCodeAndLoginSucceedsThenReturnSwitchAccount() = runTest { whenever(syncRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenAnswer { whenever(syncRepository.getAccountInfo()).thenReturn(accountB) Success(true) } @@ -369,7 +379,8 @@ class SyncWithAnotherDeviceViewModelTest { @Test fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest { whenever(syncRepository.getAccountInfo()).thenReturn(noAccount) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenAnswer { whenever(syncRepository.getAccountInfo()).thenReturn(accountB) Success(true) } @@ -386,7 +397,8 @@ class SyncWithAnotherDeviceViewModelTest { @Test fun whenUserScansRecoveryQRCodeAndConnectDeviceFailsThenCommandIsError() = runTest { whenever(syncRepository.getAccountInfo()).thenReturn(noAccount) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = LOGIN_FAILED.code)) + whenever(syncRepository.getCodeType(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Error(code = LOGIN_FAILED.code)) testee.commands().test { testee.onQRCodeScanned(jsonRecoveryKeyEncoded) val command = awaitItem() @@ -410,7 +422,7 @@ class SyncWithAnotherDeviceViewModelTest { private fun configureExchangeKeysSupported(): Pair { syncFeature.exchangeKeysToSyncWithAnotherDevice().setRawStoredState(State(true)) whenever(syncRepository.pollSecondDeviceExchangeAcknowledgement()).thenReturn(Success(true)) - whenever(syncRepository.getCodeType(any())).thenReturn(EXCHANGE) + whenever(syncRepository.getCodeType(any())).thenReturn(Exchange(InvitationCode("", ""))) whenever(syncRepository.getAccountInfo()).thenReturn(accountA) val bitmap = TestSyncFixtures.qrBitmap() val jsonExchangeKey = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey).also {