diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt index 45d0b05c577a8851b7c938e2c7de45edc36cc89b..42200d5b710b1ddab8e1923a2b5b16f45693aa37 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt @@ -70,6 +70,7 @@ class DebugOptionsFragment : Fragment(R.layout.fragment_test_debugoptions), Auto environmentCdnurlSubmission.text = "Submission CDN:\n${state.urlSubmission}" environmentCdnurlVerification.text = "Verification CDN:\n${state.urlVerification}" environmentUrlDatadonation.text = "DataDonation:\n${state.urlDataDonation}" + environmentUrlQrcodePosterTemplate.text = "QR-Code Poster Template:\n${state.urlQrCodePosterTemplate}" } } vm.environmentChangeEvent.observe2(this) { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt index fb3a8ded80765be7fe43f2487775ff3bfcfa9b2a..3d72f93a074ce0bdeffa9323f695944f54aac039 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt @@ -8,7 +8,9 @@ data class EnvironmentState( val urlSubmission: String, val urlDownload: String, val urlVerification: String, - val urlDataDonation: String + val urlDataDonation: String, + val urlQrCodePosterTemplate: String + ) { companion object { internal fun EnvironmentSetup.toEnvironmentState() = EnvironmentState( @@ -17,7 +19,8 @@ data class EnvironmentState( urlSubmission = submissionCdnUrl, urlDownload = downloadCdnUrl, urlVerification = verificationCdnUrl, - urlDataDonation = dataDonationCdnUrl + urlDataDonation = dataDonationCdnUrl, + urlQrCodePosterTemplate = qrCodePosterTemplateCdnUrl ) } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt index abd5b1c1be9461567ee0dbf1fccc308797c2bb08..0124e0857c06ebe0e76039d8c3cd7bcdefb838f8 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt @@ -80,5 +80,13 @@ class QrCodeCreationTestFragment : Fragment(R.layout.fragment_test_qrcode_creati binding.generateQrCode.setOnClickListener { viewModel.createQrCode(binding.qrCodeText.text.toString()) } + + binding.downloadQrCodePosterTemplate.setOnClickListener { + viewModel.downloadQrCodePosterTemplate() + } + + viewModel.qrCodePosterTemplate.observe2(this) { vectorDrawableBytes -> + binding.downloadedQrCodePoster.text = vectorDrawableBytes.utf8() + } } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt index a1eca9f8b1fbed87ee6f90a0a55725edd05ca75b..336f7fc486d6159c2e1e61d17920851a7e55a3aa 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt @@ -6,7 +6,7 @@ import android.graphics.pdf.PdfDocument import android.view.View import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeGenerator import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.di.AppContext @@ -14,6 +14,8 @@ import de.rki.coronawarnapp.util.files.FileSharing import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import okio.ByteString +import okio.ByteString.Companion.toByteString import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -23,12 +25,13 @@ class QrCodeCreationTestViewModel @AssistedInject constructor( private val fileSharing: FileSharing, private val qrCodeGenerator: QrCodeGenerator, @AppContext private val context: Context, - private val appConfigProvider: AppConfigProvider, + private val posterTemplateServer: QrCodePosterTemplateServer ) : CWAViewModel(dispatcher) { val qrCodeBitmap = SingleLiveEvent<Bitmap>() val errorMessage = SingleLiveEvent<String>() val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>() + val qrCodePosterTemplate = SingleLiveEvent<ByteString>() /** * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap] @@ -85,6 +88,17 @@ class QrCodeCreationTestViewModel @AssistedInject constructor( return File(dir, "CoronaWarnApp-Event.pdf") } + fun downloadQrCodePosterTemplate() { + launch { + try { + val posterTemplate = posterTemplateServer.downloadQrCodePosterTemplate() + qrCodePosterTemplate.postValue(posterTemplate.template.toByteArray().toByteString()) + } catch (exception: Exception) { + errorMessage.postValue("Downloading Poster Template failed: ${exception.message}") + } + } + } + @AssistedFactory interface Factory : SimpleCWAViewModelFactory<QrCodeCreationTestViewModel> } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml index 5a3c1da5e950e6675de4ca1f8964c91f3c9deff7..224249114efbd69384ab29ac449aa49cae7f585c 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml @@ -115,6 +115,17 @@ app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_verification" tools:text="DataDonation: ?" /> + <TextView + android:id="@+id/environment_url_qrcode_poster_template" + style="@style/body2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/environment_url_datadonation" + tools:text="QR-Code poster template: ?" /> + <RadioGroup android:id="@+id/environment_toggle_group" android:layout_width="match_parent" @@ -124,7 +135,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_url_datadonation" /> + app:layout_constraintTop_toBottomOf="@+id/environment_url_qrcode_poster_template" /> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml index 35f279062f405b8ae168310b3da39c55130e4fb4..ad94c03c70d2a560903ab07561c0b82051a45b8d 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -22,14 +22,14 @@ <TextView android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="QRCode, PDF event registration" /> + android:text="QRCode Creation, Poster Template download, PDF creation/printing/sharing" /> <com.google.android.material.button.MaterialButton android:id="@+id/testQrCodeCreation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_tiny" - android:text="QR Code Creation" /> + android:text="QR Code and Poster Creation" /> </LinearLayout> diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml index 6ae5f29730ea651f8a8e8bc0d61f010427562518..7f8e4b10c71683b4cc812dcf7835de7076e9f037 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml @@ -46,6 +46,16 @@ app:layout_constraintStart_toEndOf="@+id/sharePDF" app:layout_constraintTop_toTopOf="parent" /> + <com.google.android.material.button.MaterialButton + android:id="@+id/downloadQrCodePosterTemplate" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Download Poster Template (result below qr code)" + app:layout_constraintStart_toStartOf="@id/generateQrCode" + app:layout_constraintEnd_toEndOf="@id/printPDF" + app:layout_constraintTop_toBottomOf="@id/generateQrCode" /> + <com.google.android.material.textfield.TextInputLayout android:id="@+id/qrCodeTextLayout" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" @@ -54,7 +64,7 @@ android:layout_margin="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/generateQrCode"> + app:layout_constraintTop_toBottomOf="@id/downloadQrCodePosterTemplate"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/qrCodeText" android:layout_width="match_parent" @@ -91,5 +101,15 @@ app:layout_constraintTop_toTopOf="@id/pdfTemplateImageView" app:layout_constraintVertical_bias="0.25" /> </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + android:id="@+id/downloadedQrCodePoster" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/pdfPage" /> + </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt index 1476022658f27fc6711345379d875e055c8098d3..f5504b7792dd600c4d2afa2a927967240c61d948 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt @@ -34,6 +34,7 @@ class EnvironmentSetup @Inject constructor( DOWNLOAD("DOWNLOAD_CDN_URL"), VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"), DATA_DONATION("DATA_DONATION_CDN_URL"), + QRCODE_POSTER_TEMPLATE("QRCODE_POSTER_TEMPLATE_URL"), LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"), SAFETYNET_API_KEY("SAFETYNET_API_KEY") } @@ -115,6 +116,8 @@ class EnvironmentSetup @Inject constructor( get() = getEnvironmentValue(DOWNLOAD).asString val dataDonationCdnUrl: String get() = getEnvironmentValue(DATA_DONATION).asString + val qrCodePosterTemplateCdnUrl: String + get() = getEnvironmentValue(EnvKey.QRCODE_POSTER_TEMPLATE).asString val appConfigVerificationKey: String get() = getEnvironmentValue(VERIFICATION_KEYS).asString diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d7a0d8a163fecdda2d4b87c66453551bc88cc68 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt @@ -0,0 +1,69 @@ +package de.rki.coronawarnapp.environment.eventregistration.qrcodeposter + +import android.content.Context +import dagger.Module +import dagger.Provides +import de.rki.coronawarnapp.environment.BaseEnvironmentModule +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient +import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateApiV1 +import de.rki.coronawarnapp.util.di.AppContext +import okhttp3.Cache +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import java.io.File +import javax.inject.Qualifier +import javax.inject.Singleton + +@Module +class QrCodePosterTemplateModule : BaseEnvironmentModule() { + + @Singleton + @Provides + @QrCodePosterTemplate + fun cacheDir( + @AppContext context: Context + ): File = File(context.cacheDir, "qrCodePoster") + + @Singleton + @Provides + @QrCodePosterTemplate + fun httpCache( + @QrCodePosterTemplate cacheDir: File + ): Cache = Cache(File(cacheDir, "cache_http"), CACHE_SIZE_5MB) + + @Singleton + @QrCodePosterTemplate + @Provides + fun provideQrCodePosterTemplateCDNServerUrl(environment: EnvironmentSetup): String { + val url = environment.qrCodePosterTemplateCdnUrl + return requireValidUrl(url) + } + + @Singleton + @Provides + fun api( + @DownloadCDNHttpClient client: OkHttpClient, + @QrCodePosterTemplate url: String, + @QrCodePosterTemplate cache: Cache + ): QrCodePosterTemplateApiV1 { + val httpClient = client.newBuilder().apply { + cache(cache) + }.build() + + return Retrofit.Builder() + .client(httpClient) + .baseUrl(url) + .build() + .create(QrCodePosterTemplateApiV1::class.java) + } + + companion object { + private const val CACHE_SIZE_5MB = 5 * 1024 * 1024L + } +} + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class QrCodePosterTemplate diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt index 5b0bf95533d46f9c6b9aafd3a76e10f539abd342..2a1be8176e660b0e3b8c24a2b6af2739037812b3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -2,12 +2,17 @@ package de.rki.coronawarnapp.eventregistration import dagger.Binds import dagger.Module +import de.rki.coronawarnapp.environment.eventregistration.qrcodeposter.QrCodePosterTemplateModule import de.rki.coronawarnapp.eventregistration.checkins.download.FakeTraceTimeIntervalWarningRepository import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -@Module +@Module( + includes = [ + QrCodePosterTemplateModule::class + ] +) abstract class EventRegistrationModule { @Binds diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..32b9d12cbeef24397a3f8c4a66987c6fc8eaec77 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateApiV1.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET + +interface QrCodePosterTemplateApiV1 { + + @GET("/version/v1/qr_code_poster_template_android") + suspend fun getQrCodePosterTemplate(): Response<ResponseBody> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateInvalidResponseException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateInvalidResponseException.kt new file mode 100644 index 0000000000000000000000000000000000000000..12e67da2278110ed089cbf53106f66c9861eb9c9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateInvalidResponseException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +class QrCodePosterTemplateInvalidResponseException( + message: String, + cause: Exception? = null +) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt new file mode 100644 index 0000000000000000000000000000000000000000..dccb0432de5badcba1c0a4277a4ddd161284f24d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt @@ -0,0 +1,65 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +import com.google.protobuf.InvalidProtocolBufferException +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate +import de.rki.coronawarnapp.util.ZipHelper.readIntoMap +import de.rki.coronawarnapp.util.ZipHelper.unzip +import de.rki.coronawarnapp.util.security.SignatureValidation +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class QrCodePosterTemplateServer @Inject constructor( + private val api: QrCodePosterTemplateApiV1, + private val signatureValidation: SignatureValidation +) { + suspend fun downloadQrCodePosterTemplate(): QrCodePosterTemplate.QRCodePosterTemplateAndroid { + + Timber.d("Start download of QR-Code poster template.") + + val response = api.getQrCodePosterTemplate() + Timber.d("Received: %s", response) + + if (!response.isSuccessful) { + // TODO return cached or default response + throw HttpException(response) + } + if (response.body() == null) { + throw IllegalStateException("Response is successful, but body is empty.") + } + + val fileMap = response.body()!!.byteStream().unzip().readIntoMap() + + val exportBinary = fileMap[EXPORT_BINARY_FILE_NAME] + val exportSignature = fileMap[EXPORT_SIGNATURE_FILE_NAME] + + if (exportBinary == null || exportSignature == null) { + throw QrCodePosterTemplateInvalidResponseException(message = "Unknown files: ${fileMap.keys}") + } + + val hasValidSignature = signatureValidation.hasValidSignature( + exportBinary, + SignatureValidation.parseTEKStyleSignature(exportSignature) + ) + + if (!hasValidSignature) { + throw QrCodePosterTemplateInvalidResponseException(message = "Invalid Signature!") + } + + return try { + QrCodePosterTemplate.QRCodePosterTemplateAndroid.parseFrom(exportBinary) + } catch (exception: InvalidProtocolBufferException) { + throw QrCodePosterTemplateInvalidResponseException( + message = "QR Code poster template could not be parsed", + cause = exception + ) + } + } + + companion object { + private const val EXPORT_BINARY_FILE_NAME = "export.bin" + private const val EXPORT_SIGNATURE_FILE_NAME = "export.sig" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt index 67dd22ff90fa82d7ff5ed8e4d7ab394770484ad1..44b3cabcf715d247d626b61b54f2dca00daa747d 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt @@ -67,6 +67,7 @@ class EnvironmentSetupTest : BaseTest() { safetyNetApiKey shouldBe "placeholder-${env.rawKey}" dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}" logUploadServerUrl shouldBe "https://logupload-${env.rawKey}" + qrCodePosterTemplateCdnUrl shouldBe "https://qrcodepostertemplate-${env.rawKey}" } } } @@ -124,7 +125,8 @@ class EnvironmentSetupTest : BaseTest() { EnvironmentSetup.EnvKey.DATA_DONATION.rawKey shouldBe "DATA_DONATION_CDN_URL" EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL" EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY" - EnvironmentSetup.EnvKey.values().size shouldBe 8 + EnvironmentSetup.EnvKey.QRCODE_POSTER_TEMPLATE.rawKey shouldBe "QRCODE_POSTER_TEMPLATE_URL" + EnvironmentSetup.EnvKey.values().size shouldBe 9 } companion object { @@ -145,6 +147,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-PROD", "DATA_DONATION_CDN_URL": "https://datadonation-PROD", "LOG_UPLOAD_SERVER_URL": "https://logupload-PROD", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-PROD", "SAFETYNET_API_KEY": "placeholder-PROD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD" }, @@ -155,6 +158,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-DEV", "DATA_DONATION_CDN_URL": "https://datadonation-DEV", "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-DEV", "SAFETYNET_API_KEY": "placeholder-DEV", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV" }, @@ -165,6 +169,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-INT", "DATA_DONATION_CDN_URL": "https://datadonation-INT", "LOG_UPLOAD_SERVER_URL": "https://logupload-INT", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-INT", "SAFETYNET_API_KEY": "placeholder-INT", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT" }, @@ -175,6 +180,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU", "DATA_DONATION_CDN_URL": "https://datadonation-WRU", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU", "SAFETYNET_API_KEY": "placeholder-WRU", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU", "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU" @@ -186,6 +192,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU-XD", "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XD", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XD", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU-XD", "SAFETYNET_API_KEY": "placeholder-WRU-XD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD" }, @@ -196,6 +203,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU-XA", "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XA", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XA", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU-XA", "SAFETYNET_API_KEY": "placeholder-WRU-XA", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA" }, @@ -206,6 +214,7 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-LOCAL", "DATA_DONATION_CDN_URL": "https://datadonation-LOCAL", "LOG_UPLOAD_SERVER_URL": "https://logupload-LOCAL", + "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-LOCAL", "SAFETYNET_API_KEY": "placeholder-LOCAL", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL" } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateApiV1Test.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateApiV1Test.kt new file mode 100644 index 0000000000000000000000000000000000000000..fe715bcdc2a27941e2f4c7206a274a39039a0637 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateApiV1Test.kt @@ -0,0 +1,117 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +import de.rki.coronawarnapp.environment.eventregistration.qrcodeposter.QrCodePosterTemplateModule +import de.rki.coronawarnapp.http.HttpModule +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File +import java.util.concurrent.TimeUnit + +class QrCodePosterTemplateApiV1Test : BaseIOTest() { + + private lateinit var webServer: MockWebServer + private lateinit var serverAddress: String + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + private val cacheDir = File(testDir, "cache") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + webServer = MockWebServer().apply { start() } + serverAddress = "http://${webServer.hostName}:${webServer.port}" + } + + @AfterEach + fun teardown() { + webServer.shutdown() + cacheDir.deleteRecursively() + } + + private fun createAPI(): QrCodePosterTemplateApiV1 { + val defaultHttpClient = HttpModule().defaultHttpClient() + val templateModule = QrCodePosterTemplateModule() + return templateModule.api( + defaultHttpClient, + url = serverAddress, + cache = templateModule.httpCache(cacheDir) + ) + } + + @Test + fun `should perform request as specified`() { + + webServer.enqueue(MockResponse().setBody("QR-Code Poster Template")) + + runBlocking { + createAPI().getQrCodePosterTemplate().apply { + body()!!.string() shouldBe "QR-Code Poster Template" + } + } + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + method shouldBe "GET" + path shouldBe "/version/v1/qr_code_poster_template_android" + } + } + + @Test + fun `should set ETag header of previously received response and return cached response`() { + + // first mocked response returns a body and ETag + webServer.enqueue( + MockResponse() + .setBody("Poster Template") + .setResponseCode(200) + .setHeader("ETag", "ETAG_OF_MOCKED_RESPONSE") + ) + + runBlocking { + createAPI().getQrCodePosterTemplate().apply { + // we should receive the body and ETag + code() shouldBe 200 + body()!!.string() shouldBe "Poster Template" + headers()["ETag"] shouldBe "ETAG_OF_MOCKED_RESPONSE" + } + } + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + // Our first request should not contain any ETag in the 'If-None-Match' header + headers["If-None-Match"] shouldBe null + } + + // second mocked response returns 304 and no body (client already has latest poster) + webServer.enqueue( + MockResponse() + .setResponseCode(304) + .setHeader("ETag", "ETAG_OF_MOCKED_RESPONSE") + ) + + runBlocking { + createAPI().getQrCodePosterTemplate().apply { + code() shouldBe 200 + raw().cacheResponse shouldNotBe null + raw().networkResponse!!.code shouldBe 304 + // cached poster template should be returned + body()!!.string() shouldBe "Poster Template" + headers()["ETag"] shouldBe "ETAG_OF_MOCKED_RESPONSE" + } + } + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + method shouldBe "GET" + path shouldBe "/version/v1/qr_code_poster_template_android" + // Our first request should not contain an ETag + headers["If-None-Match"] shouldBe "ETAG_OF_MOCKED_RESPONSE" + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6f2a1b4b719753cf830a3f455dcfb938cc6a9b3d --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt @@ -0,0 +1,146 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +import de.rki.coronawarnapp.util.security.SignatureValidation +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.test.runBlockingTest +import okhttp3.ResponseBody.Companion.toResponseBody +import okio.ByteString.Companion.decodeHex +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import retrofit2.Response +import testhelpers.BaseTest + +internal class QrCodePosterTemplateServerTest : BaseTest() { + + @MockK lateinit var api: QrCodePosterTemplateApiV1 + @MockK lateinit var signatureValidation: SignatureValidation + + /** + * Info: [QrCodePosterTemplateApiV1Test] is testing if the ETag is set correctly + */ + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { signatureValidation.hasValidSignature(any(), any()) } returns true + } + + private fun createInstance() = QrCodePosterTemplateServer( + api = api, + signatureValidation = signatureValidation + ) + + @Test + fun `should return poster template when response is successful`() = runBlockingTest { + coEvery { + api.getQrCodePosterTemplate() + } returns Response.success(POSTER_BUNDLE.toResponseBody()) + + createInstance().downloadQrCodePosterTemplate().apply { + template.toStringUtf8().substring(0, 22) shouldBe "<vector xmlns:android=" + offsetX shouldBe 10 + offsetY shouldBe 10 + qrCodeSideLength shouldBe 100 + with(descriptionTextBox) { + offsetX shouldBe 10 + offsetY shouldBe 50 + width shouldBe 100 + height shouldBe 20 + fontSize shouldBe 10 + fontColor shouldBe "#000000" + } + } + + verify(exactly = 1) { signatureValidation.hasValidSignature(any(), any()) } + } + + @Test + fun `should throw exception if signature is invalid`() = runBlockingTest { + every { signatureValidation.hasValidSignature(any(), any()) } returns false + + coEvery { + api.getQrCodePosterTemplate() + } returns Response.success(POSTER_BUNDLE.toResponseBody()) + + shouldThrow<QrCodePosterTemplateInvalidResponseException> { + createInstance().downloadQrCodePosterTemplate() + } + } + + @Test + fun `should throw exception if response contains invalid data`() = runBlockingTest { + coEvery { + api.getQrCodePosterTemplate() + } returns Response.success("ABC123".decodeHex().toResponseBody()) + + shouldThrow<QrCodePosterTemplateInvalidResponseException> { + createInstance().downloadQrCodePosterTemplate() + } + } + + @Test + fun `should return default poster template when response is not successful`() = runBlockingTest { + // TODO + } + + @Test + fun `should return latest cached template when response is not successful`() = runBlockingTest { + // TODO + } + + companion object { + /* + POSTER_BUNDLE below encodes the following protobuf objects: + + private val descriptionTextBox = + QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid.newBuilder() + .setOffsetX(10) + .setOffsetY(50) + .setWidth(100) + .setHeight(20) + .setFontSize(10) + .setFontColor("#000000") + .build() + + private val qrCodePosterTemplate = QrCodePosterTemplate.QRCodePosterTemplateAndroid.newBuilder() + .setOffsetX(10.0f) + .setOffsetY(10.0f) + .setQrCodeSideLength(100) + .setDescriptionTextBox(descriptionTextBox) + .setTemplate( + ByteString.copyFromUtf8("""<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:aapt=\"http://schemas.android.com/aapt\"\n" + + " android:width=\"595.3dp\"\n" + + " android:height=\"841.9dp\"\n" + + " android:viewportWidth=\"595.3\"\n" + + " android:viewportHeight=\"841.9\">\n" + + " <path\n" + + " android:pathData=\"M78.1,665.6v-12.8h1.2l3.6,8.5v-8.5h1.5v12.8h-1.1l-3.7,-8.7v8.7H78.1z\"\n" + + " android:fillColor=\"#404040\"/>\n" + + "</vector>\n""")) + .build()*/ + + private val POSTER_BUNDLE = ( + "504b03040a000000080014867d52008c85fefb000000ab0100000a0000006578706f72742e62" + + "696e7d90cf4bc33014c7071e949c0415bc0825bbc8685f9676edbad216440fbb78f61c9a6a8ae912da9089ff80ffb66957700e" + + "f185bcc3e7fbe3f0d0d7596eebcaa8cefb68e5aecfd88e77aae10516c6e88c90be1275cb7a983854aa254cbf93aeeec9c430f2" + + "dc4c71a6cdff59673804269aed1b6e4481e34d0c11d7bf3551376fc215a62b0a9b53d136f55eabcebc1c15fcedd81ed7e0d279" + + "72cd8c18bd3fee013d31c30afcbc4e81fa49124362031a422a28843282c44f21b6815b0ec47654020a540611ac7dc7d7d6fded" + + "90fec427edaf8d948f4aaaaec0f3d572789894282787eb97e86636f31eee86e5f1c5d505ba0c6fb9777d8fc2f3f9729c6f504b" + + "03040a000000080014867d528a1d0eac8f0000008a0000000a0000006578706f72742e736967018a0075ff0a87010a380a1864" + + "652e726b692e636f726f6e617761726e6170702d6465761a02763122033236322a13312e322e3834302e31303034352e342e33" + + "2e321001180122473045022100c251eb5e62282e5573fdb915edf61115d61d020354a510bed66b7b8ce482a38d02202a793775" + + "0958155c82a17acb6dd4b666afc1566285ef532e6e8c11e1d52e5a75504b01020a000a000000080014867d52008c85fefb0000" + + "00ab0100000a0000000000000000000000a401000000006578706f72742e62696e504b01020a000a000000080014867d528a1d" + + "0eac8f0000008a0000000a0000000000000000000000a401230100006578706f72742e736967504b0506000000000200020070" + + "000000da0100000000" + ).decodeHex() + } +} diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt index 7666b705e092c26da33a45c1f5fe515fa268c3c7..ed0342077389eb08c9590844454473015094eb94 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.test.debugoptions.ui -import android.content.Context import androidx.lifecycle.Observer import de.rki.coronawarnapp.environment.EnvironmentSetup import io.kotest.matchers.shouldBe @@ -24,7 +23,6 @@ import testhelpers.flakyTest class DebugOptionsFragmentViewModelTest : BaseTestInstrumentation() { @MockK private lateinit var environmentSetup: EnvironmentSetup - @MockK private lateinit var context: Context private var currentEnvironment = EnvironmentSetup.Type.DEV @@ -38,10 +36,10 @@ class DebugOptionsFragmentViewModelTest : BaseTestInstrumentation() { every { environmentSetup.downloadCdnUrl } returns "downloadUrl" every { environmentSetup.verificationCdnUrl } returns "verificationUrl" every { environmentSetup.dataDonationCdnUrl } returns "dataDonationUrl" + every { environmentSetup.qrCodePosterTemplateCdnUrl } returns "qrCodePosterTemplateUrl" every { environmentSetup.currentEnvironment = any() } answers { currentEnvironment = arg(0) - Unit } every { environmentSetup.currentEnvironment } answers { currentEnvironment