diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TanPairingException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TanPairingException.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae4de503cf0c38f7b842b4485e1e5784934a1a01 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TanPairingException.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.exception + +import android.content.Context +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.exception.http.CwaClientError +import de.rki.coronawarnapp.util.HasHumanReadableError +import de.rki.coronawarnapp.util.HumanReadableError + +/** + * Specific Exception type to identify an error case happening when TAN is retrieved. + * @see <a href="https://jira-ibs.wbs.net.sap/browse/EXPOSUREAPP-4515">EXPOSUREAPP-4515</a> + */ +class TanPairingException( + override val code: Int, + override val message: String?, + override val cause: Throwable? +) : CwaClientError(code, message, cause), HasHumanReadableError { + override fun toHumanReadableError(context: Context): HumanReadableError { + return HumanReadableError( + title = context.getString(R.string.submission_error_dialog_web_paring_invalid_title), + description = context.getString(R.string.submission_error_dialog_web_paring_invalid_body) + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpModule.kt index a921a828fdc1e36ebdeb51188ed4553e94a64cfd..438e7095274ac7edcb22d4e2febc4824fa424b31 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpModule.kt @@ -28,11 +28,7 @@ class HttpModule { fun defaultHttpClient(): OkHttpClient { val interceptors: List<Interceptor> = listOf( WebSecurityVerificationInterceptor(), - HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { - override fun log(message: String) { - Timber.tag("OkHttp").v(message) - } - }).apply { + HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) }.apply { if (BuildConfig.DEBUG) setLevel(HttpLoggingInterceptor.Level.BODY) }, RetryInterceptor(), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt index 7d981a1c81f08e314fe4456bcce87448dbf686a9..6988043fd5c36d227973eed3d8f2237c55d4a0b0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt @@ -1,5 +1,7 @@ package de.rki.coronawarnapp.playbook +import de.rki.coronawarnapp.exception.TanPairingException +import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.submission.server.SubmissionServer import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.verification.server.VerificationKeyType @@ -90,23 +92,46 @@ class DefaultPlaybook @Inject constructor( // fake verification ignoreExceptions { verificationServer.retrieveTanFake() } - // real submission - if (authCode != null) { - val serverSubmissionData = SubmissionServer.SubmissionData( - authCode = authCode, - keyList = data.temporaryExposureKeys, - consentToFederation = data.consentToFederation, - visistedCountries = data.visistedCountries + // submitKeysToServer could throw BadRequestException too. + try { + // real submission + if (authCode != null) { + val serverSubmissionData = SubmissionServer.SubmissionData( + authCode = authCode, + keyList = data.temporaryExposureKeys, + consentToFederation = data.consentToFederation, + visistedCountries = data.visistedCountries + ) + submissionServer.submitKeysToServer(serverSubmissionData) + coroutineScope.launch { followUpPlaybooks() } + } else { + submissionServer.submitKeysToServerFake() + coroutineScope.launch { followUpPlaybooks() } + propagateException(wrapException(exception)) + } + } catch (exception: BadRequestException) { + propagateException( + TanPairingException( + code = exception.statusCode, + message = "Invalid payload or missing header", + cause = exception + ) ) - submissionServer.submitKeysToServer(serverSubmissionData) - coroutineScope.launch { followUpPlaybooks() } - } else { - submissionServer.submitKeysToServerFake() - coroutineScope.launch { followUpPlaybooks() } - propagateException(exception) } } + /** + * Distinguish BadRequestException to present more insightful message to the end user + */ + private fun wrapException(exception: Exception?) = when (exception) { + is BadRequestException -> TanPairingException( + code = exception.statusCode, + message = "Tan has been retrieved before for this registration token", + cause = exception + ) + else -> exception + } + private suspend fun dummy(launchFollowUp: Boolean) { // fake verification ignoreExceptions { verificationServer.retrieveTanFake() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt index b22bf0f54c88e09130bd24ee19c2d84bdba25f3a..24e670aff5571534d7c60025bda5e30b05fb36b8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt @@ -421,7 +421,7 @@ object LocalData { ) } - fun devicePairingSuccessfulTimestamp(): Long? { + fun devicePairingSuccessfulTimestamp(): Long { return getSharedPreferenceInstance().getLong( CoronaWarnApplication.getAppContext() .getString(R.string.preference_device_pairing_successful_time), diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt index 7a710623431ea78e0b7e1d380754985a0145c980..7f04b80939b8d8953c3ea1601ff9e40b7b0361a0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt @@ -1,5 +1,7 @@ package de.rki.coronawarnapp.http.playbook +import de.rki.coronawarnapp.exception.TanPairingException +import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.playbook.DefaultPlaybook import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.submission.server.SubmissionServer @@ -8,6 +10,7 @@ import de.rki.coronawarnapp.verification.server.VerificationKeyType import de.rki.coronawarnapp.verification.server.VerificationServer import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.MockKAnnotations import io.mockk.clearAllMocks import io.mockk.coEvery @@ -102,6 +105,44 @@ class DefaultPlaybookTest : BaseTest() { } } + @Test + fun `tan retrieval throws human readable exception`(): Unit = runBlocking { + coEvery { verificationServer.retrieveTan(any()) } throws BadRequestException(null) + try { + createPlaybook().submit( + Playbook.SubmissionData( + registrationToken = "token", + temporaryExposureKeys = listOf(), + consentToFederation = true, + visistedCountries = listOf("DE") + ) + ) + } catch (e: Exception) { + e.shouldBeInstanceOf<TanPairingException>() + e.cause.shouldBeInstanceOf<BadRequestException>() + e.message shouldBe "Tan has been retrieved before for this registration token" + } + } + + @Test + fun `keys submission throws human readable exception`(): Unit = runBlocking { + coEvery { submissionServer.submitKeysToServer(any()) } throws BadRequestException(null) + try { + createPlaybook().submit( + Playbook.SubmissionData( + registrationToken = "token", + temporaryExposureKeys = listOf(), + consentToFederation = true, + visistedCountries = listOf("DE") + ) + ) + } catch (e: Exception) { + e.shouldBeInstanceOf<TanPairingException>() + e.cause.shouldBeInstanceOf<BadRequestException>() + e.message shouldBe "Invalid payload or missing header" + } + } + @Test fun `submission matches request pattern despite missing authcode`(): Unit = runBlocking { coEvery { verificationServer.retrieveTan(any()) } throws TestException()