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 328fd06842c5..ad7eee6e260a 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,12 +33,16 @@ 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.ExchangeResult.* import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository.AuthCode +import com.duckduckgo.sync.impl.SyncAuthCode.Connect +import com.duckduckgo.sync.impl.SyncAuthCode.Exchange +import com.duckduckgo.sync.impl.SyncAuthCode.Recovery +import com.duckduckgo.sync.impl.SyncAuthCode.Unknown import com.duckduckgo.sync.impl.pixels.* +import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeUrl import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeUrlWrapper import com.duckduckgo.sync.store.* import com.squareup.anvil.annotations.* @@ -52,11 +56,11 @@ import timber.log.Timber interface SyncAccountRepository { - fun getCodeType(stringCode: String): CodeType + fun parseSyncAuthCode(stringCode: String): SyncAuthCode fun isSyncSupported(): Boolean fun createAccount(): Result fun isSignedIn(): Boolean - fun processCode(stringCode: String): Result + fun processCode(code: SyncAuthCode): Result fun getAccountInfo(): AccountInfo fun logout(deviceId: String): Result fun deleteAccount(): Result @@ -129,58 +133,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: SyncAuthCode): Result { + when (code) { + is Recovery -> { + Timber.d("Sync: code is a recovery code") + return login(code.b64Code) + } - 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.b64Code) + } - 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.b64Code) + } } - 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 { + override fun parseSyncAuthCode(stringCode: String): SyncAuthCode { + // 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") @@ -625,7 +643,8 @@ class AppSyncAccountRepository @Inject constructor( } is Success -> { - val loginResult = processCode(stringCode) + val codeType = parseSyncAuthCode(stringCode) + val loginResult = processCode(codeType) if (loginResult is Error) { syncPixels.fireUserSwitchedLoginError() } @@ -911,11 +930,11 @@ enum class AccountErrorCodes(val code: Int) { EXCHANGE_FAILED(56), } -enum class CodeType { - RECOVERY, - CONNECT, - EXCHANGE, - UNKNOWN, +sealed interface SyncAuthCode { + data class Recovery(val b64Code: RecoveryCode) : SyncAuthCode + data class Connect(val b64Code: ConnectCode) : SyncAuthCode + data class Exchange(val b64Code: InvitationCode) : SyncAuthCode + data class Unknown(val code: String) : SyncAuthCode } 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..519bd476542a 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,6 @@ 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.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -36,6 +35,7 @@ import com.duckduckgo.sync.impl.R import com.duckduckgo.sync.impl.Result import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.SyncAccountRepository +import com.duckduckgo.sync.impl.SyncAuthCode import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.onFailure import com.duckduckgo.sync.impl.onSuccess @@ -100,10 +100,10 @@ class EnterCodeViewModel @Inject constructor( pastedCode: String, ) { val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey - val codeType = syncAccountRepository.getCodeType(pastedCode) - when (val result = syncAccountRepository.processCode(pastedCode)) { + val codeType = syncAccountRepository.parseSyncAuthCode(pastedCode) + when (val result = syncAccountRepository.processCode(codeType)) { is Result.Success -> { - if (codeType == EXCHANGE) { + if (codeType is SyncAuthCode.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 38d82709532d..814eed910e66 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,6 @@ 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.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -39,6 +38,7 @@ import com.duckduckgo.sync.impl.R.dimen import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository +import com.duckduckgo.sync.impl.SyncAuthCode.Exchange import com.duckduckgo.sync.impl.getOrNull import com.duckduckgo.sync.impl.onFailure import com.duckduckgo.sync.impl.onSuccess @@ -173,14 +173,14 @@ class SyncConnectViewModel @Inject constructor( fun onQRCodeScanned(qrCode: String) { viewModelScope.launch(dispatchers.io()) { - val codeType = syncAccountRepository.getCodeType(qrCode) - when (val result = syncAccountRepository.processCode(qrCode)) { + val codeType = syncAccountRepository.parseSyncAuthCode(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 7339f0e0e261..557e2adda26a 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 @@ -210,7 +210,8 @@ constructor( fun onQRScanned(contents: String) { viewModelScope.launch(dispatchers.io()) { - val result = syncAccountRepository.processCode(contents) + val codeType = syncAccountRepository.parseSyncAuthCode(contents) + val result = syncAccountRepository.processCode(codeType) if (result is Error) { command.send(Command.ShowMessage("$result")) } @@ -220,7 +221,8 @@ constructor( fun onConnectQRScanned(contents: String) { viewModelScope.launch(dispatchers.io()) { - val result = syncAccountRepository.processCode(contents) + val codeType = syncAccountRepository.parseSyncAuthCode(contents) + val result = syncAccountRepository.processCode(codeType) when (result) { is Error -> { command.send(Command.ShowMessage("$result")) @@ -289,7 +291,8 @@ constructor( private suspend fun authFlow( pastedCode: String, ) { - val result = syncAccountRepository.processCode(pastedCode) + val codeType = syncAccountRepository.parseSyncAuthCode(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..9f9e950c8f37 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,6 @@ 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.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -35,6 +34,7 @@ import com.duckduckgo.sync.impl.R import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository +import com.duckduckgo.sync.impl.SyncAuthCode import com.duckduckgo.sync.impl.onFailure import com.duckduckgo.sync.impl.onSuccess import com.duckduckgo.sync.impl.pixels.SyncPixels @@ -87,14 +87,14 @@ class SyncLoginViewModel @Inject constructor( fun onQRCodeScanned(qrCode: String) { viewModelScope.launch(dispatchers.io()) { - val codeType = syncAccountRepository.getCodeType(qrCode) - when (val result = syncAccountRepository.processCode(qrCode)) { + val codeType = syncAccountRepository.parseSyncAuthCode(qrCode) + when (val result = syncAccountRepository.processCode(codeType)) { is Error -> { processError(result) } is Success -> { - if (codeType == EXCHANGE) { + if (codeType is SyncAuthCode.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 bf3699a8a1af..ca8f108e732f 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,6 @@ 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.ExchangeResult.AccountSwitchingRequired import com.duckduckgo.sync.impl.ExchangeResult.LoggedIn import com.duckduckgo.sync.impl.ExchangeResult.Pending @@ -41,6 +40,7 @@ import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository import com.duckduckgo.sync.impl.SyncAccountRepository.AuthCode +import com.duckduckgo.sync.impl.SyncAuthCode import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.onFailure import com.duckduckgo.sync.impl.onSuccess @@ -171,15 +171,15 @@ class SyncWithAnotherActivityViewModel @Inject constructor( fun onQRCodeScanned(qrCode: String) { viewModelScope.launch(dispatchers.io()) { val previousPrimaryKey = syncAccountRepository.getAccountInfo().primaryKey - val codeType = syncAccountRepository.getCodeType(qrCode) - when (val result = syncAccountRepository.processCode(qrCode)) { + val codeType = syncAccountRepository.parseSyncAuthCode(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 SyncAuthCode.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 2eb3c0debfcf..a7f2d1debac9 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 @@ -75,6 +75,7 @@ import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository.AuthCode import com.duckduckgo.sync.impl.pixels.SyncPixels +import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeUrl import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeUrlWrapper import com.duckduckgo.sync.store.SyncStore import com.squareup.moshi.Moshi @@ -254,7 +255,7 @@ class AppSyncAccountRepositoryTest { fun whenProcessJsonRecoveryCodeSucceedsThenAccountPersisted() { prepareForLoginSuccess() - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + val result = syncRepo.processCode(syncRepo.parseSyncAuthCode(jsonRecoveryKeyEncoded)) assertEquals(Success(true), result) verify(syncStore).storeCredentials( @@ -274,7 +275,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) - val result = syncRepo.processCode(exchangeCode.encodeB64()) + val result = syncRepo.processCode(syncRepo.parseSyncAuthCode(exchangeCode.encodeB64())) assertTrue(result is Error) } @@ -285,7 +286,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.parseSyncAuthCode(exchangeCode.encodeB64())) assertTrue(result is Success) verify(syncApi).sendEncryptedMessage(eq(primaryDeviceKeyId), eq(encryptedExchangeCode)) @@ -300,7 +301,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.parseSyncAuthCode(exchangeCode.encodeB64())) assertTrue(result is Error) } @@ -344,7 +345,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.parseSyncAuthCode(exchangeCode.encodeB64())) whenever(syncApi.getEncryptedMessage(otherDeviceKeyId)).thenReturn(Success("encryptedExchangeResponse")) whenever(nativeLib.sealOpen("encryptedExchangeResponse", primaryKey, secretKey)).thenReturn("invalid response") @@ -358,7 +359,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.parseSyncAuthCode(exchangeCode.encodeB64())) configureExchangeResultRecoveryReceived() @@ -373,7 +374,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.parseSyncAuthCode(exchangeCode.encodeB64())) configureExchangeResultRecoveryReceived() prepareForLoginSuccess() @@ -389,7 +390,7 @@ class AppSyncAccountRepositoryTest { val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) whenever(deviceKeyGenerator.generate()).thenReturn(otherDeviceKeyId) - syncRepo.processCode(exchangeCode.encodeB64()) + syncRepo.processCode(syncRepo.parseSyncAuthCode(exchangeCode.encodeB64())) configureExchangeResultRecoveryReceived() prepareForLoginSuccess() @@ -421,30 +422,58 @@ class AppSyncAccountRepositoryTest { @Test fun whenCodeIsEmptyThenCodeTypeIsUnknown() { - val type = syncRepo.getCodeType("") - assertTrue(type == CodeType.UNKNOWN) + val type = syncRepo.parseSyncAuthCode("") + assertTrue(type is SyncAuthCode.Unknown) } @Test fun whenCodeIsRecoveryThenCodeTypeIsIdentified() { val code = recoveryCodeAdapter.toJson((LinkCode(recovery = RecoveryCode(primaryKey, "userId")))) - val type = syncRepo.getCodeType(code.encodeB64()) - assertTrue(type == CodeType.RECOVERY) + val type = syncRepo.parseSyncAuthCode(code.encodeB64()) + assertTrue(type is SyncAuthCode.Recovery) } @Test fun whenCodeIsConnectThenCodeTypeIsIdentified() { val code = recoveryCodeAdapter.toJson((LinkCode(connect = ConnectCode(deviceId, secretKey)))) - val type = syncRepo.getCodeType(code.encodeB64()) - assertTrue(type == CodeType.CONNECT) + val type = syncRepo.parseSyncAuthCode(code.encodeB64()) + assertTrue(type is SyncAuthCode.Connect) } @Test fun whenCodeIsExchangeThenCodeTypeIsIdentified() { 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) + val type = syncRepo.parseSyncAuthCode(code.encodeB64()) + assertTrue(type is SyncAuthCode.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.parseSyncAuthCode(url) + assertTrue(type is SyncAuthCode.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.parseSyncAuthCode(url) + assertTrue(type is SyncAuthCode.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.parseSyncAuthCode(url) + assertTrue(type is SyncAuthCode.Unknown) } @Test @@ -457,7 +486,7 @@ class AppSyncAccountRepositoryTest { }.`when`(syncApi).logout(token, deviceId) prepareForLoginSuccess() - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + val result = syncRepo.processCode(syncRepo.parseSyncAuthCode(jsonRecoveryKeyEncoded)) verify(syncApi).logout(token, deviceId) verify(syncApi).login(userId, hashedPassword, deviceId, deviceName, deviceFactor) @@ -475,7 +504,7 @@ class AppSyncAccountRepositoryTest { }.`when`(syncApi).logout(token, deviceId) prepareForLoginSuccess() - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) + val result = syncRepo.processCode(syncRepo.parseSyncAuthCode(jsonRecoveryKeyEncoded)) assertEquals((result as Error).code, ALREADY_SIGNED_IN.code) } @@ -508,7 +537,7 @@ class AppSyncAccountRepositoryTest { prepareToProvideDeviceIds() whenever(nativeLib.prepareForLogin(primaryKey = primaryKey)).thenReturn(failedLoginKeys) - val result = syncRepo.processCode(jsonRecoveryKeyEncoded) as Error + val result = syncRepo.processCode(syncRepo.parseSyncAuthCode(jsonRecoveryKeyEncoded)) as Error assertEquals(LOGIN_FAILED.code, result.code) } @@ -520,7 +549,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.parseSyncAuthCode(jsonRecoveryKeyEncoded)) as Error assertEquals(LOGIN_FAILED.code, result.code) } @@ -533,14 +562,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.parseSyncAuthCode(jsonRecoveryKeyEncoded)) as Error assertEquals(LOGIN_FAILED.code, result.code) } @Test fun whenProcessInvalidCodeThenReturnInvalidCodeError() { - val result = syncRepo.processCode("invalidCode") as Error + val result = syncRepo.processCode(syncRepo.parseSyncAuthCode("invalidCode")) as Error assertEquals(INVALID_CODE.code, result.code) } @@ -635,7 +664,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.parseSyncAuthCode(jsonConnectKeyEncoded)) verify(syncApi).connect(token, deviceId, encryptedRecoveryCode) assertTrue(result is Success) @@ -653,7 +682,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.parseSyncAuthCode(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..89dc83fc5a05 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,9 +36,12 @@ 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.RecoveryCode import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository +import com.duckduckgo.sync.impl.SyncAuthCode.Recovery +import com.duckduckgo.sync.impl.SyncAuthCode.Unknown import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.pixels.SyncPixels import com.duckduckgo.sync.impl.ui.EnterCodeViewModel.AuthState @@ -49,8 +55,8 @@ 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 import org.mockito.kotlin.whenever @@ -98,7 +104,8 @@ internal class EnterCodeViewModelTest { fun whenUserClicksOnPasteCodeWithRecoveryCodeThenProcessCode() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonRecoveryKeyEncoded) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) Success(true) } @@ -116,7 +123,8 @@ internal class EnterCodeViewModelTest { fun whenUserClicksOnPasteCodeWithConnectCodeThenProcessCode() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) whenever(clipboard.pasteFromClipboard()).thenReturn(jsonConnectKeyEncoded) - whenever(syncAccountRepository.processCode(jsonConnectKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.parseSyncAuthCode(jsonConnectKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonConnectKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) Success(true) } @@ -134,7 +142,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.parseSyncAuthCode("invalid code")).thenReturn(Unknown("invalid code")) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = INVALID_CODE.code)) testee.onPasteCodeClicked() @@ -150,7 +159,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.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = ALREADY_SIGNED_IN.code)) testee.onPasteCodeClicked() @@ -165,7 +175,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.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = ALREADY_SIGNED_IN.code)) testee.onPasteCodeClicked() @@ -196,7 +207,8 @@ internal class EnterCodeViewModelTest { @Test fun whenSignedInUserProcessCodeSucceedsAndAccountChangedThenReturnSwitchAccount() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountB) Success(true) } @@ -213,7 +225,8 @@ internal class EnterCodeViewModelTest { @Test fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest { whenever(syncAccountRepository.getAccountInfo()).thenReturn(noAccount) - whenever(syncAccountRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncAccountRepository.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenAnswer { whenever(syncAccountRepository.getAccountInfo()).thenReturn(accountA) Success(true) } @@ -231,7 +244,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.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = LOGIN_FAILED.code)) testee.onPasteCodeClicked() @@ -246,7 +260,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.parseSyncAuthCode(jsonConnectKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonConnectKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = CONNECT_FAILED.code)) testee.onPasteCodeClicked() @@ -261,7 +276,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.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncAccountRepository.processCode(any())).thenReturn(Error(code = CREATE_ACCOUNT_FAILED.code)) testee.onPasteCodeClicked() @@ -276,7 +292,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.parseSyncAuthCode(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 e3768d200005..3e07dafa4c57 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,16 +20,23 @@ 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.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.SyncAccountRepository.AuthCode +import com.duckduckgo.sync.impl.SyncAuthCode.Connect +import com.duckduckgo.sync.impl.SyncAuthCode.Recovery import com.duckduckgo.sync.impl.pixels.SyncPixels import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command.LoginSuccess @@ -166,7 +173,8 @@ class SyncConnectViewModelTest { @Test fun whenUserScansConnectQRCodeAndConnectDeviceSucceedsThenCommandIsLoginSuccess() = runTest { - whenever(syncRepository.processCode(jsonConnectKeyEncoded)).thenReturn(Result.Success(true)) + whenever(syncRepository.parseSyncAuthCode(jsonConnectKeyEncoded)).thenReturn(Connect(ConnectCode(jsonConnectKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenReturn(Result.Success(true)) testee.commands().test { testee.onQRCodeScanned(jsonConnectKeyEncoded) val command = awaitItem() @@ -178,7 +186,8 @@ class SyncConnectViewModelTest { @Test fun whenUserScansRecoveryCodeButSignedInThenCommandIsError() = runTest { - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenReturn(Result.Error(code = ALREADY_SIGNED_IN.code)) + whenever(syncRepository.parseSyncAuthCode(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() @@ -190,7 +199,9 @@ class SyncConnectViewModelTest { @Test fun whenUserScansConnectQRCodeAndConnectDeviceFailsThenCommandIsError() = runTest { - whenever(syncRepository.processCode(jsonConnectKeyEncoded)).thenReturn(Result.Error(code = CONNECT_FAILED.code)) + whenever(syncRepository.parseSyncAuthCode(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..0aa06e34c754 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,9 +19,13 @@ 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.RecoveryCode import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.SyncAccountRepository +import com.duckduckgo.sync.impl.SyncAuthCode.Recovery import com.duckduckgo.sync.impl.pixels.SyncPixels import com.duckduckgo.sync.impl.ui.SyncLoginViewModel.Command import kotlinx.coroutines.test.runTest @@ -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.parseSyncAuthCode(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 8176f2fa1043..3c2eccf59c6f 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,20 +29,25 @@ 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.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 import com.duckduckgo.sync.impl.SyncAccountRepository.AuthCode +import com.duckduckgo.sync.impl.SyncAuthCode.Exchange +import com.duckduckgo.sync.impl.SyncAuthCode.Recovery import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.encodeB64 import com.duckduckgo.sync.impl.pixels.SyncPixels @@ -215,7 +220,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.parseSyncAuthCode(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() @@ -230,7 +236,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.parseSyncAuthCode(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() @@ -243,7 +250,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.parseSyncAuthCode(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() @@ -257,7 +265,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.parseSyncAuthCode(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() @@ -335,7 +344,8 @@ class SyncWithAnotherDeviceViewModelTest { @Test fun whenSignedInUserScansRecoveryCodeAndLoginSucceedsThenReturnSwitchAccount() = runTest { whenever(syncRepository.getAccountInfo()).thenReturn(accountA) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncRepository.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenAnswer { whenever(syncRepository.getAccountInfo()).thenReturn(accountB) Success(true) } @@ -352,7 +362,8 @@ class SyncWithAnotherDeviceViewModelTest { @Test fun whenSignedOutUserScansRecoveryCodeAndLoginSucceedsThenReturnLoginSuccess() = runTest { whenever(syncRepository.getAccountInfo()).thenReturn(noAccount) - whenever(syncRepository.processCode(jsonRecoveryKeyEncoded)).thenAnswer { + whenever(syncRepository.parseSyncAuthCode(jsonRecoveryKeyEncoded)).thenReturn(Recovery(RecoveryCode(jsonRecoveryKey, primaryKey))) + whenever(syncRepository.processCode(any())).thenAnswer { whenever(syncRepository.getAccountInfo()).thenReturn(accountB) Success(true) } @@ -369,7 +380,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.parseSyncAuthCode(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() @@ -393,7 +405,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.parseSyncAuthCode(any())).thenReturn(Exchange(InvitationCode("", ""))) whenever(syncRepository.getAccountInfo()).thenReturn(accountA) val bitmap = TestSyncFixtures.qrBitmap() val jsonExchangeKey = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey)