From d59e67844462d8193eb73a2b35675f72d3ed2256 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Tue, 1 Jun 2021 09:46:40 +0200 Subject: [PATCH] DOB hash calculation & wiring (EXPOSUREAPP-7488, EXPOSUREAPP-7509) (#3317) * Extend corona test data structures with digital covid certificate related properties. +Some additional wiring, plumbing and tests for future PRs. * LINTs * Adjust TestRegistrationRequest to supply dcc consent and DOB on test registration. * Remove explicit assignment, defaults are sufficient. * A few additional unit tests to check defaults. * DateOfBirthKey calculation, draft 1 * Fix date parser pattern. * wip * Adjust padding calculation to take the new dobHash into account. Some refactoring to make it less complicated to adjust for future changes. * klint, ofc. Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com> --- .../coronatest/server/RegistrationData.kt | 6 + .../coronatest/server/RegistrationRequest.kt | 9 + .../coronatest/server/VerificationApiV1.kt | 15 +- .../coronatest/server/VerificationKeyType.kt | 8 +- .../coronatest/server/VerificationServer.kt | 167 ++++++++++++++---- .../coronatest/type/CoronaTestService.kt | 33 +--- .../coronatest/type/common/DateOfBirthKey.kt | 25 +++ .../coronatest/type/pcr/PCRProcessor.kt | 28 ++- .../type/rapidantigen/RAProcessor.kt | 17 +- .../coronawarnapp/playbook/DefaultPlaybook.kt | 29 +-- .../de/rki/coronawarnapp/playbook/Playbook.kt | 12 +- .../server/VerificationApiV1Test.kt | 50 +++++- .../server/VerificationServerTest.kt | 145 ++++++++++----- .../type/common/DateOfBirthKeyTest.kt | 34 ++++ .../coronatest/type/pcr/PCRProcessorTest.kt | 39 ++-- .../rapidantigen/RapidAntigenProcessorTest.kt | 40 ++--- .../http/playbook/DefaultPlaybookTest.kt | 38 ++-- ...erviceTest.kt => CoronaTestServiceTest.kt} | 64 ++++--- 18 files changed, 520 insertions(+), 239 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/{SubmissionServiceTest.kt => CoronaTestServiceTest.kt} (64%) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt new file mode 100644 index 000000000..b004502d8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.coronatest.server + +data class RegistrationData( + val registrationToken: String, + val testResultResponse: CoronaTestResultResponse +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt new file mode 100644 index 000000000..08de195d1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.coronatest.server + +import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey + +data class RegistrationRequest( + val key: String, + val type: VerificationKeyType, + val dateOfBirthKey: DateOfBirthKey? = null, +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt index d40f59b3b..4f4662767 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt @@ -8,9 +8,10 @@ import retrofit2.http.POST interface VerificationApiV1 { data class RegistrationTokenRequest( - @SerializedName("keyType") val keyType: String? = null, - @SerializedName("key") val key: String? = null, - @SerializedName("requestPadding") val requestPadding: String? = null + @SerializedName("keyType") val keyType: VerificationKeyType, + @SerializedName("key") val key: String, + @SerializedName("keyDob") val dateOfBirthKey: String? = null, + @SerializedName("requestPadding") val requestPadding: String? = null, ) data class RegistrationTokenResponse( @@ -25,8 +26,8 @@ interface VerificationApiV1 { ): RegistrationTokenResponse data class RegistrationRequest( - @SerializedName("registrationToken") val registrationToken: String? = null, - @SerializedName("requestPadding") val requestPadding: String? = null + @SerializedName("registrationToken") val registrationToken: String, + @SerializedName("requestPadding") val requestPadding: String ) data class TestResultResponse( @@ -42,8 +43,8 @@ interface VerificationApiV1 { ): TestResultResponse data class TanRequestBody( - @SerializedName("registrationToken") val registrationToken: String? = null, - @SerializedName("requestPadding") val requestPadding: String? = null + @SerializedName("registrationToken") val registrationToken: String, + @SerializedName("requestPadding") val requestPadding: String ) data class TanResponse( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt index c64cfe628..0f982bd2b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt @@ -1,5 +1,11 @@ package de.rki.coronawarnapp.coronatest.server +import com.google.gson.annotations.SerializedName + enum class VerificationKeyType { - GUID, TELETAN; + @SerializedName("GUID") + GUID, + + @SerializedName("TELETAN") + TELETAN; } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt index 30d580cf9..ab80df04a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt @@ -20,32 +20,59 @@ class VerificationServer @Inject constructor( get() = verificationAPI.get() suspend fun retrieveRegistrationToken( - key: String, - keyType: VerificationKeyType + request: RegistrationRequest ): RegistrationToken = withContext(Dispatchers.IO) { - Timber.tag(TAG).v("retrieveRegistrationToken(key=%s, keyType=%s)", key, keyType) - val keyStr = if (keyType == VerificationKeyType.GUID) { - HashHelper.hash256(key) - } else { - key - } + Timber.tag(TAG).v("retrieveRegistrationToken(request=%s)", request) + + val requiredHeaderPadding = run { + var size = HEADER_SIZE_OUR_DATA + size -= HEADER_SIZE_OVERHEAD - val paddingLength = when (keyType) { - VerificationKeyType.GUID -> PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID - VerificationKeyType.TELETAN -> PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN + // `POST /version/v1/registrationToken` + size -= 34 + + size + } + val requiredBodyPadding = run { + var size = BODY_SIZE_EXPECTED + size -= BODY_SIZE_OVERHEAD + + size -= when (request.type) { + VerificationKeyType.GUID -> 17 // `"keyType":"GUID",` + VerificationKeyType.TELETAN -> 20 // `"keyType":"TELETAN",` + } + + size -= when (request.type) { + VerificationKeyType.GUID -> { + 73 // `"key":"75552e6e1dae7a520bad64e92b7569447d0f5ca3c539335e0418a7695606147e",` + } + VerificationKeyType.TELETAN -> { + 19 // `"key":"ERYCJMM4DC",` + } + } + + size -= when (request.dateOfBirthKey) { + null -> 0 + else -> 76 // `"keyDob":"x9acafb78b330522e32b4bf4c90a3ebb7a4d20d8af8cc32018c550ea86a38cc1",` + } + size } val response = api.getRegistrationToken( fake = "0", - headerPadding = requestPadding(PADDING_LENGTH_HEADER_REGISTRATION_TOKEN), + headerPadding = requestPadding(requiredHeaderPadding), requestBody = VerificationApiV1.RegistrationTokenRequest( - keyType = keyType.name, - key = keyStr, - requestPadding = requestPadding(paddingLength) + keyType = request.type, + key = when (request.type) { + VerificationKeyType.GUID -> HashHelper.hash256(request.key) + else -> request.key + }, + dateOfBirthKey = request.dateOfBirthKey?.key, + requestPadding = requestPadding(requiredBodyPadding), ) ) - Timber.tag(TAG).d("retrieveRegistrationToken(key=%s, keyType=%s) -> %s", key, keyType, response) + Timber.tag(TAG).d("retrieveRegistrationToken(request=%s) -> %s", request, response) response.registrationToken } @@ -53,12 +80,32 @@ class VerificationServer @Inject constructor( token: RegistrationToken ): CoronaTestResultResponse = withContext(Dispatchers.IO) { Timber.tag(TAG).v("retrieveTestResults(token=%s)", token) + + val requiredHeaderPadding = run { + var size = HEADER_SIZE_OUR_DATA + size -= HEADER_SIZE_OVERHEAD + + // `POST /version/v1/testresult` + size -= 27 + + size + } + val requiredBodyPadding = run { + var size = BODY_SIZE_EXPECTED + size -= BODY_SIZE_OVERHEAD + + // `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",` + size -= 59 + + size + } + val response = api.getTestResult( fake = "0", - headerPadding = requestPadding(PADDING_LENGTH_HEADER_TEST_RESULT), + headerPadding = requestPadding(requiredHeaderPadding), request = VerificationApiV1.RegistrationRequest( token, - requestPadding(PADDING_LENGTH_BODY_TEST_RESULT) + requestPadding(requiredBodyPadding) ) ) @@ -71,12 +118,31 @@ class VerificationServer @Inject constructor( registrationToken: RegistrationToken ): String = withContext(Dispatchers.IO) { Timber.tag(TAG).v("retrieveTan(registrationToken=%s)", registrationToken) + val requiredHeaderPadding = run { + var size = HEADER_SIZE_OUR_DATA + size -= HEADER_SIZE_OVERHEAD + + // `POST /version/v1/tan` + size -= 20 + + size + } + val requiredBodyPadding = run { + var size = BODY_SIZE_EXPECTED + size -= BODY_SIZE_OVERHEAD + + // `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",` + size -= 59 + + size + } + val response = api.getTAN( fake = "0", - headerPadding = requestPadding(PADDING_LENGTH_HEADER_TAN), + headerPadding = requestPadding(requiredHeaderPadding), requestBody = VerificationApiV1.TanRequestBody( registrationToken, - requestPadding(PADDING_LENGTH_BODY_TAN) + requestPadding(requiredBodyPadding) ) ) @@ -86,12 +152,31 @@ class VerificationServer @Inject constructor( suspend fun retrieveTanFake() = withContext(Dispatchers.IO) { Timber.tag(TAG).v("retrieveTanFake()") + val requiredHeaderPadding = run { + var size = HEADER_SIZE_OUR_DATA + size -= HEADER_SIZE_OVERHEAD + + // `POST /version/v1/tan` + size -= 20 + + size + } + val requiredBodyPadding = run { + var size = BODY_SIZE_EXPECTED + size -= BODY_SIZE_OVERHEAD + + // `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",` + size -= 59 + + size + } + val response = api.getTAN( fake = "1", - headerPadding = requestPadding(PADDING_LENGTH_HEADER_TAN), + headerPadding = requestPadding(requiredHeaderPadding), requestBody = VerificationApiV1.TanRequestBody( registrationToken = DUMMY_REGISTRATION_TOKEN, - requestPadding = requestPadding(PADDING_LENGTH_BODY_TAN_FAKE) + requestPadding = requestPadding(requiredBodyPadding) ) ) Timber.tag(TAG).v("retrieveTanFake() -> %s", response) @@ -99,28 +184,40 @@ class VerificationServer @Inject constructor( } companion object { - // padding registration token - private const val VERIFICATION_BODY_FILL = 139 + /** + * The specific sizes are not important, but all requests should be padded up to the same size. + * Pick a total size that is guaranteed to be above or equal to the maximum size a request can be. + */ + // `"requestPadding":""` + private const val BODY_SIZE_PADDING_OVERHEAD = 19 // + + // `{}` json brackets + private const val BODY_SIZE_OVERHEAD = BODY_SIZE_PADDING_OVERHEAD + 2 + private const val BODY_SIZE_EXPECTED = 250 + + /** + * The header itself is larger. + * We care about the header fields we set that are request specific. + * We don't need to pad for device specific fields set by OK http. + */ + // `POST /version/v1/registrationToken` -> 34 (longest method + url atm) use 64 to have a buffer + private const val HEADER_SIZE_LONGEST_METHOD = 34 - const val PADDING_LENGTH_HEADER_REGISTRATION_TOKEN = 0 - const val PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN = 51 + VERIFICATION_BODY_FILL - const val PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID = 0 + VERIFICATION_BODY_FILL + // `cwa-fake 0\n` -> 12 + private const val HEADER_SIZE_VAL_FAKE = 12 - // padding test result - const val PADDING_LENGTH_HEADER_TEST_RESULT = 7 - const val PADDING_LENGTH_BODY_TEST_RESULT = 31 + VERIFICATION_BODY_FILL + // `cwa-header-padding\n` -> 22 + private const val HEADER_SIZE_VAL_PADDING = 22 + private const val HEADER_SIZE_OVERHEAD = HEADER_SIZE_VAL_FAKE + HEADER_SIZE_VAL_PADDING + private const val HEADER_SIZE_OUR_DATA = HEADER_SIZE_LONGEST_METHOD + HEADER_SIZE_OVERHEAD - // padding tan - const val PADDING_LENGTH_HEADER_TAN = 14 - const val PADDING_LENGTH_BODY_TAN = 31 + VERIFICATION_BODY_FILL - const val PADDING_LENGTH_BODY_TAN_FAKE = 31 + VERIFICATION_BODY_FILL const val DUMMY_REGISTRATION_TOKEN = "11111111-2222-4444-8888-161616161616" /** * Test is available for this long on the server. * After this period the server will delete it and return PENDING if the regtoken is polled again. */ - val TEST_AVAILABLBILITY = Duration.standardDays(60) + val TEST_AVAILABLBILITY: Duration = Duration.standardDays(60) private const val TAG = "VerificationServer" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt index dcc41c608..009fa8deb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt @@ -1,7 +1,8 @@ package de.rki.coronawarnapp.coronatest.type import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse -import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.deniability.NoiseScheduler import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.worker.BackgroundConstants @@ -13,34 +14,17 @@ class CoronaTestService @Inject constructor( private val noiseScheduler: NoiseScheduler, ) { - suspend fun asyncRequestTestResult(registrationToken: String): CoronaTestResultResponse { + suspend fun checkTestResult(registrationToken: String): CoronaTestResultResponse { return playbook.testResult(registrationToken) } - suspend fun asyncRegisterDeviceViaGUID(guid: String): RegistrationData { - val (registrationToken, testResult) = - playbook.initialRegistration( - guid, - VerificationKeyType.GUID - ) + suspend fun registerTest(tokenRequest: RegistrationRequest): RegistrationData { + val response = playbook.initialRegistration(tokenRequest) Timber.d("Scheduling background noise.") scheduleDummyPattern() - return RegistrationData(registrationToken, testResult) - } - - suspend fun asyncRegisterDeviceViaTAN(tan: String): RegistrationData { - val (registrationToken, testResult) = - playbook.initialRegistration( - tan, - VerificationKeyType.TELETAN - ) - - Timber.d("Scheduling background noise.") - scheduleDummyPattern() - - return RegistrationData(registrationToken, testResult) + return response } private fun scheduleDummyPattern() { @@ -48,9 +32,4 @@ class CoronaTestService @Inject constructor( noiseScheduler.setPeriodicNoise(enabled = true) } } - - data class RegistrationData( - val registrationToken: String, - val testResultResponse: CoronaTestResultResponse - ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt new file mode 100644 index 000000000..efc8a828e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.coronatest.type.common + +import de.rki.coronawarnapp.util.HashExtensions.toSHA256 +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat + +data class DateOfBirthKey constructor( + private val testGuid: String, + private val dateOfBirth: LocalDate, +) { + + init { + require(testGuid.isNotEmpty()) { "GUID can't be empty." } + } + + val key by lazy { + val dobFormatted = dateOfBirth.toString(DOB_FORMATTER) + val keyHash = "${testGuid}$dobFormatted".toSHA256() + "x${keyHash.substring(1)}" + } + + companion object { + private val DOB_FORMATTER = DateTimeFormat.forPattern("ddMMYYYY") + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt index 8ee04a424..75f83e9dd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt @@ -14,11 +14,15 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_NEGATIVE import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_PENDING import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType import de.rki.coronawarnapp.coronatest.server.VerificationServer import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor import de.rki.coronawarnapp.coronatest.type.CoronaTestService +import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey import de.rki.coronawarnapp.coronatest.type.isOlderThan21Days import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.testresult.AnalyticsTestResultCollector @@ -51,7 +55,17 @@ class PCRProcessor @Inject constructor( private suspend fun createQR(request: CoronaTestQRCode.PCR): PCRCoronaTest { Timber.tag(TAG).d("createQR(data=%s)", request) - val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.qrCodeGUID).also { + val dateOfBirthKey = if (request.isDccConsentGiven && request.dateOfBirth != null) { + DateOfBirthKey(request.qrCodeGUID, request.dateOfBirth) + } else null + + val serverRequest = RegistrationRequest( + key = request.qrCodeGUID, + dateOfBirthKey = dateOfBirthKey, + type = VerificationKeyType.GUID, + ) + + val registrationData = submissionService.registerTest(serverRequest).also { Timber.tag(TAG).d("Request %s gave us %s", request, it) } @@ -64,7 +78,13 @@ class PCRProcessor @Inject constructor( private suspend fun createTAN(request: CoronaTestTAN.PCR): CoronaTest { Timber.tag(TAG).d("createTAN(data=%s)", request) - val registrationData = submissionService.asyncRegisterDeviceViaTAN(request.tan) + val serverRequest = RegistrationRequest( + key = request.tan, + dateOfBirthKey = null, + type = VerificationKeyType.TELETAN, + ) + + val registrationData = submissionService.registerTest(serverRequest) analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() @@ -75,7 +95,7 @@ class PCRProcessor @Inject constructor( private suspend fun createCoronaTest( request: TestRegistrationRequest, - response: CoronaTestService.RegistrationData + response: RegistrationData ): PCRCoronaTest { analyticsKeySubmissionCollector.reset(type) @@ -126,7 +146,7 @@ class PCRProcessor @Inject constructor( } val newTestResult = try { - submissionService.asyncRequestTestResult(test.registrationToken).coronaTestResult.let { + submissionService.checkTestResult(test.registrationToken).coronaTestResult.let { Timber.tag(TAG).d("Raw test result was %s", it) analyticsTestResultCollector.updatePendingTestResultReceivedTime(it, type) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt index c28253d5f..3dcd82592 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt @@ -15,10 +15,13 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_PENDING import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType import de.rki.coronawarnapp.coronatest.server.VerificationServer import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor import de.rki.coronawarnapp.coronatest.type.CoronaTestService +import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey import de.rki.coronawarnapp.coronatest.type.isOlderThan21Days import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.testresult.AnalyticsTestResultCollector @@ -53,7 +56,17 @@ class RAProcessor @Inject constructor( analyticsKeySubmissionCollector.reset(type) analyticsTestResultCollector.clear(type) - val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.registrationIdentifier).also { + val dateOfBirthKey = if (request.isDccConsentGiven && request.dateOfBirth != null) { + DateOfBirthKey(request.registrationIdentifier, request.dateOfBirth) + } else null + + val serverRequest = RegistrationRequest( + key = request.registrationIdentifier, + dateOfBirthKey = dateOfBirthKey, + type = VerificationKeyType.GUID + ) + + val registrationData = submissionService.registerTest(serverRequest).also { Timber.tag(TAG).d("Request %s gave us %s", request, it) } @@ -117,7 +130,7 @@ class RAProcessor @Inject constructor( } val newTestResult = try { - submissionService.asyncRequestTestResult(test.registrationToken).let { + submissionService.checkTestResult(test.registrationToken).let { Timber.tag(TAG).v("Raw test result was %s", it) it.copy( coronaTestResult = it.coronaTestResult.toValidatedResult() 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 3e2070c78..959410f6a 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,7 +1,8 @@ package de.rki.coronawarnapp.playbook import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse -import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.coronatest.server.VerificationServer import de.rki.coronawarnapp.exception.TanPairingException import de.rki.coronawarnapp.exception.http.BadRequestException @@ -26,23 +27,18 @@ class DefaultPlaybook @Inject constructor( private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) override suspend fun initialRegistration( - key: String, - keyType: VerificationKeyType - ): Pair<String, CoronaTestResultResponse> { + request: RegistrationRequest + ): RegistrationData { Timber.i("[$uid] New Initial Registration Playbook") // real registration - val (registrationToken, registrationException) = - executeCapturingExceptions { - verificationServer.retrieveRegistrationToken( - key, - keyType - ) - } + val (registrationToken, registrationException) = executeCapturingExceptions { + verificationServer.retrieveRegistrationToken(request) + } // if the registration succeeded continue with the real test result retrieval // if it failed, execute a dummy request to satisfy the required playbook pattern - val (testResult, testResultException) = if (registrationToken != null) { + val (testResultResponse, testResultException) = if (registrationToken != null) { executeCapturingExceptions { verificationServer.pollTestResult(registrationToken) } } else { ignoreExceptions { verificationServer.retrieveTanFake() } @@ -55,8 +51,13 @@ class DefaultPlaybook @Inject constructor( coroutineScope.launch { followUpPlaybooks() } // if registration and test result retrieval succeeded, return the result - if (registrationToken != null && testResult != null) - return registrationToken to testResult + if (registrationToken != null && testResultResponse != null) { + + return RegistrationData( + registrationToken = registrationToken, + testResultResponse = testResultResponse, + ) + } // else propagate the exception of either the first or the second step propagateException(registrationException, testResultException) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt index b631e93a1..5e72abca8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.playbook -import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse -import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass import de.rki.coronawarnapp.server.protocols.internal.SubmissionPayloadOuterClass.SubmissionPayload import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass @@ -21,13 +21,7 @@ import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass */ interface Playbook { - /** - * @return pair of Registration token [String] & [CoronaTestResult] - */ - suspend fun initialRegistration( - key: String, - keyType: VerificationKeyType - ): Pair<String, CoronaTestResultResponse> + suspend fun initialRegistration(tokenRequest: RegistrationRequest): RegistrationData suspend fun testResult(registrationToken: String): CoronaTestResultResponse diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt index efa159300..9076ba3b7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.coronatest.server import android.content.Context +import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey import de.rki.coronawarnapp.http.HttpModule import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -9,6 +10,7 @@ import io.mockk.impl.annotations.MockK import kotlinx.coroutines.runBlocking import okhttp3.ConnectionSpec import okhttp3.mockwebserver.MockWebServer +import org.joda.time.LocalDate import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -65,7 +67,7 @@ class VerificationApiV1Test : BaseIOTest() { } @Test - fun `test getRegistrationToken`(): Unit = runBlocking { + fun `test getRegistrationToken - GUID`(): Unit = runBlocking { val api = createAPI() """ @@ -75,8 +77,9 @@ class VerificationApiV1Test : BaseIOTest() { """.toJsonResponse().apply { webServer.enqueue(this) } val requestBody = VerificationApiV1.RegistrationTokenRequest( - keyType = "testKeyType", + keyType = VerificationKeyType.GUID, key = "testKey", + dateOfBirthKey = DateOfBirthKey("testKeyGuid", LocalDate.parse("2020-09-11")).key, requestPadding = "testRequestPadding" ) @@ -94,7 +97,48 @@ class VerificationApiV1Test : BaseIOTest() { path shouldBe "/version/v1/registrationToken" body.readUtf8() shouldBe """ { - "keyType": "testKeyType", + "keyType": "GUID", + "key": "testKey", + "keyDob": "x9acafb78b330522e32b4bf4c90a3ebb7a4d20d8af8cc32018c550ea86a38cc1", + "requestPadding": "testRequestPadding" + } + """.toComparableJson() + } + + httpCacheDir.exists() shouldBe true + } + + @Test + fun `test getRegistrationToken - TAN`(): Unit = runBlocking { + val api = createAPI() + + """ + { + "registrationToken": "testRegistrationToken" + } + """.toJsonResponse().apply { webServer.enqueue(this) } + + val requestBody = VerificationApiV1.RegistrationTokenRequest( + keyType = VerificationKeyType.TELETAN, + key = "testKey", + requestPadding = "testRequestPadding", + ) + + api.getRegistrationToken( + fake = "0", + headerPadding = "testPadding", + requestBody + ) shouldBe VerificationApiV1.RegistrationTokenResponse( + registrationToken = "testRegistrationToken" + ) + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + headers["cwa-fake"] shouldBe "0" + headers["cwa-header-padding"] shouldBe "testPadding" + path shouldBe "/version/v1/registrationToken" + body.readUtf8() shouldBe """ + { + "keyType": "TELETAN", "key": "testKey", "requestPadding": "testRequestPadding" } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt index 6ac91f60f..07f8cbc45 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt @@ -1,8 +1,10 @@ package de.rki.coronawarnapp.coronatest.server import android.content.Context +import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey import de.rki.coronawarnapp.http.HttpModule import de.rki.coronawarnapp.util.headerSizeIgnoringContentLength +import de.rki.coronawarnapp.util.requestHeaderWithoutContentLength import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -13,11 +15,14 @@ import kotlinx.coroutines.runBlocking import okhttp3.ConnectionSpec import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import org.joda.time.Duration +import org.joda.time.LocalDate import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseIOTest +import timber.log.Timber import java.io.File class VerificationServerTest : BaseIOTest() { @@ -30,7 +35,17 @@ class VerificationServerTest : BaseIOTest() { private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) private val cacheDir = File(testDir, "cache") - private val httpCacheDir = File(cacheDir, "http_verification") + + private val requestTan = RegistrationRequest( + "testKeyTan", + VerificationKeyType.TELETAN + ) + + private val requestGuid = RegistrationRequest( + key = "testKeyGuid", + type = VerificationKeyType.GUID, + dateOfBirthKey = DateOfBirthKey("testKeyGuid", LocalDate.parse("2020-09-11")) + ) @BeforeEach fun setup() { @@ -54,16 +69,58 @@ class VerificationServerTest : BaseIOTest() { customApi: VerificationApiV1 = verificationApi ) = VerificationServer(verificationAPI = { customApi }) + private fun createRealApi(): VerificationApiV1 { + val httpModule = HttpModule() + val defaultHttpClient = httpModule.defaultHttpClient() + val gsonConverterFactory = httpModule.provideGSONConverter() + + return VerificationModule().let { + val downloadHttpClient = it.cdnHttpClient( + defaultHttpClient, + listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS) + ) + it.provideVerificationApi( + context = context, + client = downloadHttpClient, + url = serverAddress, + gsonConverterFactory = gsonConverterFactory + ) + } + } + @Test - fun `get registration token via GUID`(): Unit = runBlocking { + fun `get registration token via GUID - with dobHash`(): Unit = runBlocking { val server = createServer() coEvery { verificationApi.getRegistrationToken(any(), any(), any()) } answers { arg<String>(0) shouldBe "0" arg<String>(1) shouldBe "" arg<VerificationApiV1.RegistrationTokenRequest>(2).apply { - keyType shouldBe VerificationKeyType.GUID.name - key shouldBe "15291f67d99ea7bc578c3544dadfbb991e66fa69cb36ff70fe30e798e111ff5f" + keyType shouldBe VerificationKeyType.GUID + key shouldBe "7620a19f93374e8d5acff090d3c10d0242a32fe140c50bbd40c95edf3c0af5b7" + requestPadding!!.length shouldBe 63 + dateOfBirthKey shouldBe requestGuid.dateOfBirthKey!!.key + } + VerificationApiV1.RegistrationTokenResponse( + registrationToken = "testRegistrationToken" + ) + } + + server.retrieveRegistrationToken(requestGuid) shouldBe "testRegistrationToken" + + coVerify { verificationApi.getRegistrationToken(any(), any(), any()) } + } + + @Test + fun `get registration token via GUID - without dobHash`(): Unit = runBlocking { + val server = createServer() + coEvery { verificationApi.getRegistrationToken(any(), any(), any()) } answers { + arg<String>(0) shouldBe "0" + arg<String>(1) shouldBe "" + arg<VerificationApiV1.RegistrationTokenRequest>(2).apply { + keyType shouldBe VerificationKeyType.GUID + key shouldBe "7620a19f93374e8d5acff090d3c10d0242a32fe140c50bbd40c95edf3c0af5b7" requestPadding!!.length shouldBe 139 + dateOfBirthKey shouldBe null } VerificationApiV1.RegistrationTokenResponse( registrationToken = "testRegistrationToken" @@ -71,8 +128,7 @@ class VerificationServerTest : BaseIOTest() { } server.retrieveRegistrationToken( - "testKey", - VerificationKeyType.GUID + requestGuid.copy(dateOfBirthKey = null) ) shouldBe "testRegistrationToken" coVerify { verificationApi.getRegistrationToken(any(), any(), any()) } @@ -85,19 +141,17 @@ class VerificationServerTest : BaseIOTest() { arg<String>(0) shouldBe "0" arg<String>(1) shouldBe "" arg<VerificationApiV1.RegistrationTokenRequest>(2).apply { - keyType shouldBe VerificationKeyType.TELETAN.name - key shouldBe "testKey" + keyType shouldBe VerificationKeyType.TELETAN + key shouldBe "testKeyTan" requestPadding!!.length shouldBe 190 + dateOfBirthKey shouldBe null } VerificationApiV1.RegistrationTokenResponse( registrationToken = "testRegistrationToken" ) } - server.retrieveRegistrationToken( - "testKey", - VerificationKeyType.TELETAN - ) shouldBe "testRegistrationToken" + server.retrieveRegistrationToken(requestTan) shouldBe "testRegistrationToken" coVerify { verificationApi.getRegistrationToken(any(), any(), any()) } } @@ -110,7 +164,7 @@ class VerificationServerTest : BaseIOTest() { arg<String>(1).length shouldBe 7 // Header-padding arg<VerificationApiV1.RegistrationRequest>(2).apply { registrationToken shouldBe "testRegistrationToken" - requestPadding!!.length shouldBe 170 + requestPadding.length shouldBe 170 } VerificationApiV1.TestResultResponse(testResult = 2, sampleCollectedAt = null) } @@ -131,7 +185,7 @@ class VerificationServerTest : BaseIOTest() { arg<String>(1).length shouldBe 14 // Header-padding arg<VerificationApiV1.TanRequestBody>(2).apply { registrationToken shouldBe "testRegistrationToken" - requestPadding!!.length shouldBe 170 + requestPadding.length shouldBe 170 } VerificationApiV1.TanResponse(tan = "testTan") } @@ -149,7 +203,7 @@ class VerificationServerTest : BaseIOTest() { arg<String>(1).length shouldBe 14 // Header-padding arg<VerificationApiV1.TanRequestBody>(2).apply { registrationToken shouldBe "11111111-2222-4444-8888-161616161616" - requestPadding!!.length shouldBe 170 + requestPadding.length shouldBe 170 } VerificationApiV1.TanResponse(tan = "testTan") } @@ -159,59 +213,52 @@ class VerificationServerTest : BaseIOTest() { coVerify { verificationApi.getTAN(any(), any(), any()) } } - private fun createRealApi(): VerificationApiV1 { - val httpModule = HttpModule() - val defaultHttpClient = httpModule.defaultHttpClient() - val gsonConverterFactory = httpModule.provideGSONConverter() - - return VerificationModule().let { - val downloadHttpClient = it.cdnHttpClient( - defaultHttpClient, - listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS) - ) - it.provideVerificationApi( - context = context, - client = downloadHttpClient, - url = serverAddress, - gsonConverterFactory = gsonConverterFactory - ) - } - } - @Test fun `all requests have the same footprint for pleasible deniability`(): Unit = runBlocking { - val guidExample = "3BF1D4-1C6003DD-733D-41F1-9F30-F85FA7406BF7" - val teletanExample = "9A3B578UMG" val registrationTokenExample = "63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f" + val requests = mutableListOf<RecordedRequest>() + val api = createServer(createRealApi()) + + // Default happy path + webServer.enqueue(MockResponse().setBody("{}")) + api.retrieveRegistrationToken(requestGuid) + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L + + // No dobHash + webServer.enqueue(MockResponse().setBody("{}")) + api.retrieveRegistrationToken(requestGuid) + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L + + // Second happy path try webServer.enqueue(MockResponse().setBody("{}")) - api.retrieveRegistrationToken(guidExample, VerificationKeyType.GUID) + api.retrieveRegistrationToken(requestGuid.copy(key = "3BF1D4-1C6003DD-733D-41F1-9F30-F85FA7406BF7")) + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L + // Via tan webServer.enqueue(MockResponse().setBody("{}")) - api.retrieveRegistrationToken(teletanExample, VerificationKeyType.TELETAN) + api.retrieveRegistrationToken(requestTan.copy(key = "9A3B578UMG")) + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L + // Polling for test result webServer.enqueue(MockResponse().setBody("{}")) api.pollTestResult(registrationTokenExample) + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L + // Submission TAN webServer.enqueue(MockResponse().setBody("{}")) api.retrieveTan(registrationTokenExample) + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L + // Playbook dummy request webServer.enqueue(MockResponse().setBody("{}")) api.retrieveTanFake() - - val requests = listOf( - webServer.takeRequest(), - webServer.takeRequest(), - webServer.takeRequest(), - webServer.takeRequest(), - webServer.takeRequest() - ) - - // ensure all request have same size (header & body) - requests.forEach { it.bodySize shouldBe 250L } + webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L requests.zipWithNext().forEach { (a, b) -> + Timber.i("Header a: %s", a.requestHeaderWithoutContentLength().replace('\n', ' ')) + Timber.i("Header b: %s", b.requestHeaderWithoutContentLength().replace('\n', ' ')) a.headerSizeIgnoringContentLength() shouldBe b.headerSizeIgnoringContentLength() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt new file mode 100644 index 000000000..4ef92cd80 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.coronatest.type.common + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DateOfBirthKeyTest : BaseTest() { + + @Test + fun `empty guids fail early`() { + shouldThrow<IllegalArgumentException> { + DateOfBirthKey("", LocalDate.parse("1990-10-24")) + } + } + + @Test + fun `test case 1`() { + DateOfBirthKey( + testGuid = "E1277F-E1277F24-4AD2-40BC-AFF8-CBCCCD893E4B", + dateOfBirth = LocalDate.parse("2000-01-01", DateTimeFormat.forPattern("YYYY-MM-dd")) + ).key shouldBe "xfa760e171f000ef5a7f863ab180f6f6e8185c4890224550395281d839d85458" + } + + @Test + fun `test case 2`() { + DateOfBirthKey( + testGuid = "F1EE0D-F1EE0D4D-4346-4B63-B9CF-1522D9200915", + dateOfBirth = LocalDate.parse("1995-06-07", DateTimeFormat.forPattern("YYYY-MM-dd")) + ).key shouldBe "x4a7788ef394bc7d52112014b127fe2bf142c51fe1bbb385214280e9db670193" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt index f67ba4c3d..f7d32ec8e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt @@ -14,6 +14,8 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.values import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR import de.rki.coronawarnapp.coronatest.type.CoronaTestService @@ -58,24 +60,21 @@ class PCRProcessorTest : BaseTest() { every { timeStamper.nowUTC } returns nowUTC submissionService.apply { - coEvery { asyncRequestTestResult(any()) } returns CoronaTestResultResponse( + coEvery { checkTestResult(any()) } returns CoronaTestResultResponse( coronaTestResult = PCR_OR_RAT_PENDING, sampleCollectedAt = null, ) - coEvery { asyncRegisterDeviceViaGUID(any()) } returns CoronaTestService.RegistrationData( - registrationToken = "regtoken-qr", - testResultResponse = CoronaTestResultResponse( - coronaTestResult = PCR_OR_RAT_PENDING, - sampleCollectedAt = null, - ), - ) - coEvery { asyncRegisterDeviceViaTAN(any()) } returns CoronaTestService.RegistrationData( - registrationToken = "regtoken-tan", - testResultResponse = CoronaTestResultResponse( - coronaTestResult = PCR_OR_RAT_PENDING, - sampleCollectedAt = null, - ), - ) + coEvery { registerTest(any()) } answers { + val request = arg<RegistrationRequest>(0) + + RegistrationData( + registrationToken = "regtoken-${request.type}", + testResultResponse = CoronaTestResultResponse( + coronaTestResult = PCR_OR_RAT_PENDING, + sampleCollectedAt = null, + ), + ) + } } analyticsKeySubmissionCollector.apply { @@ -118,14 +117,14 @@ class PCRProcessorTest : BaseTest() { @Test fun `registering a new test maps invalid results to INVALID state`() = runBlockingTest { - var registrationData = CoronaTestService.RegistrationData( + var registrationData = RegistrationData( registrationToken = "regtoken", testResultResponse = CoronaTestResultResponse( coronaTestResult = PCR_OR_RAT_PENDING, sampleCollectedAt = null, ) ) - coEvery { submissionService.asyncRegisterDeviceViaGUID(any()) } answers { registrationData } + coEvery { submissionService.registerTest(any()) } answers { registrationData } val instance = createInstance() @@ -158,7 +157,7 @@ class PCRProcessorTest : BaseTest() { @Test fun `polling maps invalid results to INVALID state`() = runBlockingTest { var pollResult: CoronaTestResult = PCR_OR_RAT_PENDING - coEvery { submissionService.asyncRequestTestResult(any()) } answers { + coEvery { submissionService.checkTestResult(any()) } answers { CoronaTestResultResponse( coronaTestResult = pollResult, sampleCollectedAt = null, @@ -210,7 +209,7 @@ class PCRProcessorTest : BaseTest() { @Test fun `polling is skipped if test is older than 21 days and state was already REDEEMED`() = runBlockingTest { - coEvery { submissionService.asyncRequestTestResult(any()) } answers { + coEvery { submissionService.checkTestResult(any()) } answers { CoronaTestResultResponse( coronaTestResult = PCR_POSITIVE, sampleCollectedAt = null, @@ -236,7 +235,7 @@ class PCRProcessorTest : BaseTest() { @Test fun `http 400 errors map to REDEEMED (EXPIRED) state after 21 days`() = runBlockingTest { val ourBadRequest = BadRequestException("Who?") - coEvery { submissionService.asyncRequestTestResult(any()) } throws ourBadRequest + coEvery { submissionService.checkTestResult(any()) } throws ourBadRequest val instance = createInstance() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt index a3180a0da..5222c1be5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt @@ -14,6 +14,8 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.values import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN import de.rki.coronawarnapp.coronatest.type.CoronaTestService import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector @@ -59,24 +61,22 @@ class RapidAntigenProcessorTest : BaseTest() { every { timeStamper.nowUTC } returns nowUTC submissionService.apply { - coEvery { asyncRequestTestResult(any()) } returns CoronaTestResultResponse( + coEvery { checkTestResult(any()) } returns CoronaTestResultResponse( coronaTestResult = PCR_OR_RAT_PENDING, sampleCollectedAt = null, ) - coEvery { asyncRegisterDeviceViaGUID(any()) } returns CoronaTestService.RegistrationData( - registrationToken = "regtoken-qr", - testResultResponse = CoronaTestResultResponse( - coronaTestResult = PCR_OR_RAT_PENDING, - sampleCollectedAt = null, - ) - ) - coEvery { asyncRegisterDeviceViaTAN(any()) } returns CoronaTestService.RegistrationData( - registrationToken = "regtoken-tan", - testResultResponse = CoronaTestResultResponse( - coronaTestResult = PCR_OR_RAT_PENDING, - sampleCollectedAt = null, + + coEvery { registerTest(any()) } answers { + val request = arg<RegistrationRequest>(0) + + RegistrationData( + registrationToken = "regtoken-${request.type}", + testResultResponse = CoronaTestResultResponse( + coronaTestResult = PCR_OR_RAT_PENDING, + sampleCollectedAt = null, + ) ) - ) + } } analyticsKeySubmissionCollector.apply { @@ -115,7 +115,7 @@ class RapidAntigenProcessorTest : BaseTest() { (instance.pollServer(raTest) as RACoronaTest).sampleCollectedAt shouldBe null - coEvery { submissionService.asyncRequestTestResult(any()) } returns CoronaTestResultResponse( + coEvery { submissionService.checkTestResult(any()) } returns CoronaTestResultResponse( coronaTestResult = PCR_OR_RAT_PENDING, sampleCollectedAt = nowUTC, ) @@ -142,14 +142,14 @@ class RapidAntigenProcessorTest : BaseTest() { @Test fun `registering a new test maps invalid results to INVALID state`() = runBlockingTest { - var registrationData = CoronaTestService.RegistrationData( + var registrationData = RegistrationData( registrationToken = "regtoken", testResultResponse = CoronaTestResultResponse( coronaTestResult = PCR_OR_RAT_PENDING, sampleCollectedAt = null, ), ) - coEvery { submissionService.asyncRegisterDeviceViaGUID(any()) } answers { registrationData } + coEvery { submissionService.registerTest(any()) } answers { registrationData } val instance = createInstance() @@ -184,7 +184,7 @@ class RapidAntigenProcessorTest : BaseTest() { @Test fun `polling filters out invalid test result values`() = runBlockingTest { var pollResult: CoronaTestResult = PCR_OR_RAT_PENDING - coEvery { submissionService.asyncRequestTestResult(any()) } answers { + coEvery { submissionService.checkTestResult(any()) } answers { CoronaTestResultResponse( coronaTestResult = pollResult, sampleCollectedAt = null, @@ -217,7 +217,7 @@ class RapidAntigenProcessorTest : BaseTest() { @Test fun `polling is skipped if test is older than 21 days and state was already REDEEMED`() = runBlockingTest { - coEvery { submissionService.asyncRequestTestResult(any()) } answers { + coEvery { submissionService.checkTestResult(any()) } answers { CoronaTestResultResponse( coronaTestResult = RAT_POSITIVE, sampleCollectedAt = null, @@ -243,7 +243,7 @@ class RapidAntigenProcessorTest : BaseTest() { @Test fun `http 400 errors map to REDEEMED (EXPIRED) state after 21 days`() = runBlockingTest { val ourBadRequest = BadRequestException("Who?") - coEvery { submissionService.asyncRequestTestResult(any()) } throws ourBadRequest + coEvery { submissionService.checkTestResult(any()) } throws ourBadRequest val instance = createInstance() 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 8259af24f..dd870ce11 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 @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.http.playbook import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.coronatest.server.VerificationKeyType import de.rki.coronawarnapp.coronatest.server.VerificationServer import de.rki.coronawarnapp.exception.TanPairingException @@ -29,11 +30,20 @@ class DefaultPlaybookTest : BaseTest() { @MockK lateinit var submissionServer: SubmissionServer @MockK lateinit var verificationServer: VerificationServer + private val requestGuid = RegistrationRequest( + key = "guid", + type = VerificationKeyType.GUID + ) + private val requestTan = RegistrationRequest( + key = "9A3B578UMG", + type = VerificationKeyType.TELETAN + ) + @BeforeEach fun setup() { MockKAnnotations.init(this) - coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns "token" + coEvery { verificationServer.retrieveRegistrationToken(any()) } returns "token" coEvery { verificationServer.pollTestResult(any()) } returns CoronaTestResultResponse( coronaTestResult = CoronaTestResult.PCR_OR_RAT_PENDING, sampleCollectedAt = null @@ -52,13 +62,13 @@ class DefaultPlaybookTest : BaseTest() { @Test fun `initial registration pattern matches`(): Unit = runBlocking { - coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns "response" + coEvery { verificationServer.retrieveRegistrationToken(any()) } returns "response" - createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN) + createPlaybook().initialRegistration(requestTan) coVerifySequence { // ensure request order is 2x verification and 1x submission - verificationServer.retrieveRegistrationToken(any(), any()) + verificationServer.retrieveRegistrationToken(any()) verificationServer.pollTestResult(any()) submissionServer.submitFakePayload() } @@ -67,16 +77,16 @@ class DefaultPlaybookTest : BaseTest() { @Test fun ` registration pattern matches despite token failure`(): Unit = runBlocking { coEvery { - verificationServer.retrieveRegistrationToken(any(), any()) + verificationServer.retrieveRegistrationToken(any()) } throws TestException() shouldThrow<TestException> { - createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN) + createPlaybook().initialRegistration(requestTan) } coVerifySequence { // ensure request order is 2x verification and 1x submission - verificationServer.retrieveRegistrationToken(any(), any()) + verificationServer.retrieveRegistrationToken(any()) verificationServer.retrieveTanFake() submissionServer.submitFakePayload() } @@ -200,7 +210,7 @@ class DefaultPlaybookTest : BaseTest() { @Test fun `failures during dummy requests should be ignored`(): Unit = runBlocking { val expectedToken = "token" - coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns expectedToken + coEvery { verificationServer.retrieveRegistrationToken(any()) } returns expectedToken val expectedResult = CoronaTestResult.PCR_OR_RAT_PENDING coEvery { verificationServer.pollTestResult(expectedToken) } returns CoronaTestResultResponse( coronaTestResult = expectedResult, @@ -209,7 +219,7 @@ class DefaultPlaybookTest : BaseTest() { coEvery { submissionServer.submitFakePayload() } throws TestException() val (registrationToken, testResult) = createPlaybook() - .initialRegistration("key", VerificationKeyType.GUID) + .initialRegistration(requestGuid) registrationToken shouldBe expectedToken testResult shouldBe CoronaTestResultResponse( @@ -221,15 +231,15 @@ class DefaultPlaybookTest : BaseTest() { @Test fun `registration pattern matches despire token failure`(): Unit = runBlocking { coEvery { - verificationServer.retrieveRegistrationToken(any(), any()) + verificationServer.retrieveRegistrationToken(any()) } throws TestException() shouldThrow<TestException> { - createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN) + createPlaybook().initialRegistration(requestTan) } coVerifySequence { // ensure request order is 2x verification and 1x submission - verificationServer.retrieveRegistrationToken(any(), any()) + verificationServer.retrieveRegistrationToken(any()) verificationServer.retrieveTanFake() submissionServer.submitFakePayload() } @@ -240,12 +250,12 @@ class DefaultPlaybookTest : BaseTest() { coEvery { verificationServer.pollTestResult(any()) } throws TestException() shouldThrow<TestException> { - createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN) + createPlaybook().initialRegistration(requestTan) } coVerifySequence { // ensure request order is 2x verification and 1x submission - verificationServer.retrieveRegistrationToken(any(), any()) + verificationServer.retrieveRegistrationToken(any()) verificationServer.pollTestResult(any()) submissionServer.submitFakePayload() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/CoronaTestServiceTest.kt similarity index 64% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/CoronaTestServiceTest.kt index 5fa4e49b6..b75849bb4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/CoronaTestServiceTest.kt @@ -2,6 +2,8 @@ package de.rki.coronawarnapp.service.submission import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse +import de.rki.coronawarnapp.coronatest.server.RegistrationData +import de.rki.coronawarnapp.coronatest.server.RegistrationRequest import de.rki.coronawarnapp.coronatest.server.VerificationKeyType import de.rki.coronawarnapp.coronatest.type.CoronaTestService import de.rki.coronawarnapp.deniability.NoiseScheduler @@ -20,7 +22,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -class SubmissionServiceTest : BaseTest() { +class CoronaTestServiceTest : BaseTest() { private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064" private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" @@ -30,8 +32,6 @@ class SubmissionServiceTest : BaseTest() { @MockK lateinit var appComponent: ApplicationComponent @MockK lateinit var noiseScheduler: NoiseScheduler - lateinit var submissionService: CoronaTestService - @BeforeEach fun setUp() { MockKAnnotations.init(this) @@ -39,49 +39,45 @@ class SubmissionServiceTest : BaseTest() { every { AppInjector.component } returns appComponent every { appComponent.playbook } returns mockPlaybook - submissionService = CoronaTestService( - playbook = mockPlaybook, - noiseScheduler = noiseScheduler - ) - } - - @Test - fun registrationWithGUIDSucceeds() { coEvery { - mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID) - } returns ( - registrationToken to CoronaTestResultResponse( + mockPlaybook.initialRegistration(any()) + } returns RegistrationData( + registrationToken = registrationToken, + testResultResponse = CoronaTestResultResponse( coronaTestResult = CoronaTestResult.PCR_OR_RAT_PENDING, sampleCollectedAt = null ) - ) + ) + } - runBlocking { - submissionService.asyncRegisterDeviceViaGUID(guid) - } + private fun createInstance() = CoronaTestService( + playbook = mockPlaybook, + noiseScheduler = noiseScheduler + ) + + @Test + fun registrationWithGUIDSucceeds() = runBlocking { + val request = RegistrationRequest( + key = guid, + type = VerificationKeyType.GUID, + ) + createInstance().registerTest(request) coVerify(exactly = 1) { - mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID) + mockPlaybook.initialRegistration(request) } } @Test - fun registrationWithTeleTANSucceeds() { - coEvery { - mockPlaybook.initialRegistration(any(), VerificationKeyType.TELETAN) - } returns ( - registrationToken to CoronaTestResultResponse( - coronaTestResult = CoronaTestResult.PCR_OR_RAT_PENDING, - sampleCollectedAt = null - ) - ) - - runBlocking { - submissionService.asyncRegisterDeviceViaTAN(tan) - } + fun registrationWithTeleTANSucceeds() = runBlocking { + val request = RegistrationRequest( + key = tan, + type = VerificationKeyType.TELETAN, + ) + createInstance().registerTest(request) coVerify(exactly = 1) { - mockPlaybook.initialRegistration(tan, VerificationKeyType.TELETAN) + mockPlaybook.initialRegistration(request) } } @@ -93,7 +89,7 @@ class SubmissionServiceTest : BaseTest() { ) runBlocking { - submissionService.asyncRequestTestResult(registrationToken) shouldBe CoronaTestResultResponse( + createInstance().checkTestResult(registrationToken) shouldBe CoronaTestResultResponse( coronaTestResult = CoronaTestResult.PCR_NEGATIVE, sampleCollectedAt = null, ) -- GitLab