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..5e46e02e16003da416af66fefef52c5e2900f819 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}" + environmentUrlTracelocation.text = "Create TraceLocation:\n${state.urlCreateTraceLocation}" } } 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..de39d889a768a6ff4c5f6b447b33f76b831381e2 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,8 @@ data class EnvironmentState( val urlSubmission: String, val urlDownload: String, val urlVerification: String, - val urlDataDonation: String + val urlDataDonation: String, + val urlCreateTraceLocation: String ) { companion object { internal fun EnvironmentSetup.toEnvironmentState() = EnvironmentState( @@ -17,7 +18,8 @@ data class EnvironmentState( urlSubmission = submissionCdnUrl, urlDownload = downloadCdnUrl, urlVerification = verificationCdnUrl, - urlDataDonation = dataDonationCdnUrl + urlDataDonation = dataDonationCdnUrl, + urlCreateTraceLocation = traceLocationCdnUrl ) } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt index 3e6daef237373951dc50bf59d214b44f3b72232b..87006f2a43461eda2fe0ab2110fa4c7f0b465a0b 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt @@ -49,16 +49,25 @@ class CreateEventTestFragment : Fragment(R.layout.fragment_test_createevent), Au private fun initOnCreateEventClicked() = with(binding) { createEventButton.setOnClickListener { - vm.createEvent( - eventOrLocationSpinner.editText!!.text.toString(), - eventDescription.text.toString(), - eventAddress.text.toString(), - eventStartEditText.text.toString(), - eventEndEditText.text.toString(), - eventDefaultCheckinLengthInMinutes.text.toString() - ) + createEvent() it.hideKeyboard() } + sendToServerButton.setOnClickListener { + createEvent(sendToServer = true) + it.hideKeyboard() + } + } + + private fun FragmentTestCreateeventBinding.createEvent(sendToServer: Boolean = false) { + vm.createEvent( + eventOrLocationSpinner.editText!!.text.toString(), + eventDescription.text.toString(), + eventAddress.text.toString(), + eventStartEditText.text.toString(), + eventEndEditText.text.toString(), + eventDefaultCheckinLengthInMinutes.text.toString(), + sendToServer + ) } private fun initSpinner() { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt index 264bb97c7c25f274b7e3ab1a6cb46668d04035f0..9ba5907a542d5ad910541c9cca9dfe193cb34a38 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt @@ -4,9 +4,11 @@ import androidx.lifecycle.MutableLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.events.TraceLocationCreator +import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass -import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory @@ -18,6 +20,7 @@ import java.util.UUID class CreateEventTestViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, + private val traceLocationCreator: TraceLocationCreator, private val traceLocationRepository: TraceLocationRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -32,7 +35,8 @@ class CreateEventTestViewModel @AssistedInject constructor( address: String, start: String, end: String, - defaultCheckInLengthInMinutes: String + defaultCheckInLengthInMinutes: String, + sendToServer: Boolean = false ) { try { val startDate = @@ -40,31 +44,43 @@ class CreateEventTestViewModel @AssistedInject constructor( val endDate = if (end.isBlank()) null else DateTime.parse(end, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm")) - /* TODO: wait for new protobuf messages 'TraceLocation' and perform network request to get - 'SignedTraceLocation' */ - - // Backend needs UNIX timestamp in Seconds, not milliseconds - val startTimeStampSeconds = startDate?.toInstant()?.seconds ?: 0 - val endTimeStampSeconds = endDate?.toInstant()?.seconds ?: 0 - val traceLocationType = - if (type == "Event") TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER - else TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER + if (type == "Event") LOCATION_TYPE_TEMPORARY_OTHER else LOCATION_TYPE_PERMANENT_OTHER - val traceLocation = TraceLocation( - UUID.randomUUID().toString(), // will be provided by the server when the endpoint is ready - traceLocationType, - description, - address, - startDate?.toInstant(), - endDate?.toInstant(), - defaultCheckInLengthInMinutes.toInt(), - "ByteRepresentation".toByteArray().toByteString(), - "ServerSignature".toByteArray().toByteString() - ) + if (sendToServer) { + val userInput = TraceLocationUserInput( + traceLocationType, + description, + address, + startDate?.toInstant(), + endDate?.toInstant(), + defaultCheckInLengthInMinutes.toInt() + ) - traceLocationRepository.addTraceLocation(traceLocation) - result.postValue(Result.Success(traceLocation)) + launch { + try { + val traceLocation = traceLocationCreator.createTraceLocation(userInput) + result.postValue(Result.Success(traceLocation)) + } catch (exception: Exception) { + Timber.d("Something went wrong when sending the event to the server $exception") + result.postValue(Result.Error) + } + } + } else { + val traceLocation = TraceLocation( + UUID.randomUUID().toString(), + traceLocationType, + description, + address, + startDate?.toInstant(), + endDate?.toInstant(), + defaultCheckInLengthInMinutes.toInt(), + "ByteRepresentation".toByteArray().toByteString(), + "ServerSignature".toByteArray().toByteString() + ) + traceLocationRepository.addTraceLocation(traceLocation) + result.postValue(Result.Success(traceLocation)) + } } catch (exception: Exception) { Timber.d("Something went wrong when trying to create an event: $exception") result.postValue(Result.Error) diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml index afd82d5c5bb5f5f2b149d587eae2549a9a3204ac..1b654e9f6dc04d4f8ab6fa85b70b8a9513c184e2 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml @@ -54,6 +54,7 @@ android:hint="Description" android:maxLines="1" android:padding="@dimen/spacing_tiny" + android:text="TraceLocation created from TestMenu" tools:ignore="HardcodedText" /> </com.google.android.material.textfield.TextInputLayout> @@ -71,6 +72,7 @@ android:hint="Address" android:maxLines="1" android:padding="@dimen/spacing_tiny" + android:text="Address of TraceLocation" tools:ignore="HardcodedText" /> </com.google.android.material.textfield.TextInputLayout> @@ -89,6 +91,7 @@ android:hint="Start (yyyy-MM-dd HH:mm)" android:maxLines="1" android:padding="@dimen/spacing_tiny" + android:text="2020-01-15 15:00" tools:ignore="HardcodedText" /> </com.google.android.material.textfield.TextInputLayout> @@ -107,6 +110,7 @@ android:hint="End (yyyy-MM-dd HH:mm)" android:maxLines="1" android:padding="@dimen/spacing_tiny" + android:text="2020-01-15 18:00" tools:ignore="HardcodedText" /> </com.google.android.material.textfield.TextInputLayout> @@ -126,6 +130,7 @@ android:maxLines="1" android:padding="@dimen/spacing_tiny" android:inputType="numberDecimal" + android:text="90" tools:ignore="HardcodedText" /> </com.google.android.material.textfield.TextInputLayout> @@ -137,6 +142,14 @@ android:text="Create Event" tools:ignore="HardcodedText" /> + <com.google.android.material.button.MaterialButton + android:id="@+id/send_to_server_button" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Send to server" + tools:ignore="HardcodedText" /> + <TextView android:id="@+id/resultText" style="@style/subtitle" 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 cedba81d312970d03e1c16c12dbdb69b669439e3..66b4c97cc1236425f16b107e5894631b480aa32b 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 @@ -18,9 +18,9 @@ <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/debug_container" style="@style/Card" - android:layout_margin="@dimen/spacing_tiny" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny"> <TextView android:id="@+id/debug_container_title" @@ -115,6 +115,17 @@ app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_verification" tools:text="DataDonation: ?" /> + <TextView + android:id="@+id/environment_url_tracelocation" + 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="Create TraceLocation: ?" /> + <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_tracelocation" /> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> 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..8249835b1dec34abb7ae92f90ba614f4ce3674ff 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 @@ -5,6 +5,7 @@ import androidx.core.content.edit import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.CREATE_TRACELOCATION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DATA_DONATION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DOWNLOAD import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.LOG_UPLOAD @@ -34,6 +35,7 @@ class EnvironmentSetup @Inject constructor( DOWNLOAD("DOWNLOAD_CDN_URL"), VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"), DATA_DONATION("DATA_DONATION_CDN_URL"), + CREATE_TRACELOCATION("CREATE_TRACELOCATION_URL"), LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"), SAFETYNET_API_KEY("SAFETYNET_API_KEY") } @@ -115,6 +117,8 @@ class EnvironmentSetup @Inject constructor( get() = getEnvironmentValue(DOWNLOAD).asString val dataDonationCdnUrl: String get() = getEnvironmentValue(DATA_DONATION).asString + val traceLocationCdnUrl: String + get() = getEnvironmentValue(CREATE_TRACELOCATION).asString val appConfigVerificationKey: String get() = getEnvironmentValue(VERIFICATION_KEYS).asString diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/CreateTraceLocationCDNServerUrl.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/CreateTraceLocationCDNServerUrl.kt new file mode 100644 index 0000000000000000000000000000000000000000..69756ed82cd852fc7e3693960e00ab28f6923ca1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/CreateTraceLocationCDNServerUrl.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.environment.eventregistration + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CreateTraceLocationCDNServerUrl diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/CreateTraceLocationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/CreateTraceLocationModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..814a89969c68c9d2348a303d425ab9c3cddbfc27 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/CreateTraceLocationModule.kt @@ -0,0 +1,47 @@ +package de.rki.coronawarnapp.environment.eventregistration + +import dagger.Module +import dagger.Provides +import dagger.Reusable +import de.rki.coronawarnapp.environment.BaseEnvironmentModule +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.eventregistration.events.server.CreateTraceLocationApiV1 +import de.rki.coronawarnapp.http.HttpClientDefault +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.protobuf.ProtoConverterFactory +import javax.inject.Singleton + +@Module +class CreateTraceLocationModule : BaseEnvironmentModule() { + + @Reusable + @TraceLocationCDNHttpClient + @Provides + fun cdnHttpClient(@HttpClientDefault okHttpClient: OkHttpClient): OkHttpClient = okHttpClient.newBuilder().build() + + @Singleton + @CreateTraceLocationCDNServerUrl + @Provides + fun provideCreateTraceLocationCDNServerUrl(environment: EnvironmentSetup): String { + val url = environment.traceLocationCdnUrl + return requireValidUrl(url) + } + + @Singleton + @Provides + fun provideCreateTraceLocationApi( + @TraceLocationCDNHttpClient okHttpClient: OkHttpClient, + @CreateTraceLocationCDNServerUrl url: String, + protoConverterFactory: ProtoConverterFactory, + gsonConverterFactory: GsonConverterFactory + ): CreateTraceLocationApiV1 = + Retrofit.Builder() + .client(okHttpClient) + .baseUrl(url) + .addConverterFactory(protoConverterFactory) + .addConverterFactory(gsonConverterFactory) + .build() + .create(CreateTraceLocationApiV1::class.java) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/TraceLocationCDNHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/TraceLocationCDNHttpClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..a424b7659c2a3f3ad7f975c81d50967a6b70850b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/TraceLocationCDNHttpClient.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.environment.eventregistration + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class TraceLocationCDNHttpClient 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 225c6afeae5ea5c9307518071e7287b666654753..090a9c4d1cf60a7b8d07842461e5af8b1c7844ca 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,13 +2,19 @@ package de.rki.coronawarnapp.eventregistration import dagger.Binds import dagger.Module +import de.rki.coronawarnapp.environment.eventregistration.CreateTraceLocationModule import de.rki.coronawarnapp.eventregistration.checkins.download.DownloadedCheckInsRepo import de.rki.coronawarnapp.eventregistration.checkins.download.FakeDownloadedCheckInsRepo import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -@Module +@Module( + includes = [ + CreateTraceLocationModule::class + ] +) abstract class EventRegistrationModule { + @Binds abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): TraceLocationRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt index d7d49bd547cc59434414797e9a17a514f5c6e193..c55e49759a20fbcf7a8e0bf904ac93317f482c6d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt @@ -3,9 +3,11 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode import android.os.Parcelable import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation.parseFrom import kotlinx.parcelize.Parcelize import okio.ByteString import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString import org.joda.time.Instant const val TRACE_LOCATION_VERSION = 1 @@ -43,3 +45,23 @@ fun TraceLocationEntity.toTraceLocation() = TraceLocation( signature = signatureBase64.decodeBase64()!!, version = version ) + +fun TraceLocationOuterClass.SignedTraceLocation.toTraceLocation(): TraceLocation { + + val traceLocation = parseFrom(location) + + fun Long.getInstantOrNull() = if (this == 0L) null else Instant.ofEpochSecond(this) + + return TraceLocation( + guid = traceLocation.guid, + type = traceLocation.type, + description = traceLocation.description, + address = traceLocation.address, + startDate = traceLocation.startTimestamp.getInstantOrNull(), + endDate = traceLocation.endTimestamp.getInstantOrNull(), + defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes, + byteRepresentation = traceLocation.toByteArray().toByteString(), + signature = signature.toByteArray().toByteString(), + version = traceLocation.version + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationCreator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationCreator.kt new file mode 100644 index 0000000000000000000000000000000000000000..925fc8e7cf2e409ee38e1d8dffbe7e4502943014 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationCreator.kt @@ -0,0 +1,51 @@ +package de.rki.coronawarnapp.eventregistration.events + +import dagger.Lazy +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocation +import de.rki.coronawarnapp.eventregistration.events.server.TraceLocationServer +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.util.security.SignatureValidation +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +const val TRACE_LOCATION_VERSION = 1 + +@Singleton +class TraceLocationCreator @Inject constructor( + private val api: Lazy<TraceLocationServer>, + private val repository: TraceLocationRepository, + private val signatureValidation: SignatureValidation +) { + + suspend fun createTraceLocation(traceLocationUserInput: TraceLocationUserInput): TraceLocation { + + val signedTraceLocation = api.get().retrieveSignedTraceLocation(traceLocationUserInput) + + val isSignatureValid = try { + signatureValidation.hasValidSignature( + signedTraceLocation.location.toByteArray(), + sequenceOf(signedTraceLocation.signature.toByteArray()) + ) + } catch (exception: Exception) { + Timber.e("Signature Validation Failed: $exception") + throw SignatureValidationFailedException("Signature Validation failed", exception) + } + + if (!isSignatureValid) { + Timber.e("The received trace location has an invalid signature") + throw InvalidSignatureException("The received trace location has an invalid signature", null) + } + + val traceLocation = signedTraceLocation.toTraceLocation() + + repository.addTraceLocation(traceLocation) + + return traceLocation + } +} + +// TODO: Finalize Error Handling in a future PR when it was specified in TechSpecs +class InvalidSignatureException(message: String?, cause: Throwable?) : Exception(message, cause) +class SignatureValidationFailedException(message: String?, cause: Throwable?) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUserInput.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUserInput.kt new file mode 100644 index 0000000000000000000000000000000000000000..139fe361c96a6c1c3aaaafbc6128c2026dc5a09e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUserInput.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import org.joda.time.Instant + +data class TraceLocationUserInput( + val type: TraceLocationOuterClass.TraceLocationType, + val description: String, + val address: String, + val startDate: Instant?, + val endDate: Instant?, + val defaultCheckInLengthInMinutes: Int +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/CreateTraceLocationApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/CreateTraceLocationApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8a56a67f855eaaecdbd7bd2ab5100af0fb8cb64 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/CreateTraceLocationApiV1.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.eventregistration.events.server + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface CreateTraceLocationApiV1 { + + @POST("/version/v1/trace-location") + suspend fun createTraceLocation( + @Body requestBody: TraceLocationOuterClass.TraceLocation + ): Response<TraceLocationOuterClass.SignedTraceLocation> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationServer.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c6b3e0936e20e28554eb1935026f30a17ac8701 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationServer.kt @@ -0,0 +1,49 @@ +package de.rki.coronawarnapp.eventregistration.events.server + +import androidx.annotation.VisibleForTesting +import dagger.Lazy +import de.rki.coronawarnapp.eventregistration.events.TRACE_LOCATION_VERSION +import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TraceLocationServer @Inject constructor( + private val api: Lazy<CreateTraceLocationApiV1> +) { + + suspend fun retrieveSignedTraceLocation( + traceLocationUserInput: TraceLocationUserInput + ): TraceLocationOuterClass.SignedTraceLocation { + + val traceLocationProto = traceLocationUserInput.toTraceLocationProtoBuf() + val response = api.get().createTraceLocation(traceLocationProto) + + if (!response.isSuccessful) throw HttpException(response) + if (response.body() == null) { + throw IllegalStateException("Response is successful, but body is empty.") + } + + val signedTraceLocation = response.body()!! + + Timber.d("Successfully received %s", signedTraceLocation) + return signedTraceLocation + } +} + +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +fun TraceLocationUserInput.toTraceLocationProtoBuf(): TraceLocationOuterClass.TraceLocation { + return TraceLocationOuterClass.TraceLocation.newBuilder() + .setVersion(TRACE_LOCATION_VERSION) + .setType(type) + .setDescription(description) + .setAddress(address) + .setStartTimestamp(startDate?.seconds ?: 0) + .setEndTimestamp(endDate?.seconds ?: 0) + .setDefaultCheckInLengthInMinutes(defaultCheckInLengthInMinutes) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt index 42e9b689de70d02c062d7ca00013023336ace753..31a061fd5e201df0e6fd4f44de53bb3a4469fc3f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt @@ -31,18 +31,18 @@ class DefaultTraceLocationRepository @Inject constructor( override val allTraceLocations: Flow<List<TraceLocation>> get() = traceLocationDao.allEntries().map { it.toTraceLocations() } - override fun addTraceLocation(event: TraceLocation) { + override fun addTraceLocation(traceLocation: TraceLocation) { appScope.launch { - Timber.d("Add hosted event: $event") - val eventEntity = event.toTraceLocationEntity() + Timber.d("Add hosted event: $traceLocation") + val eventEntity = traceLocation.toTraceLocationEntity() traceLocationDao.insert(eventEntity) } } - override fun deleteTraceLocation(event: TraceLocation) { + override fun deleteTraceLocation(traceLocation: TraceLocation) { appScope.launch { - Timber.d("Delete hosted event: $event") - val eventEntity = event.toTraceLocationEntity() + Timber.d("Delete hosted event: $traceLocation") + val eventEntity = traceLocation.toTraceLocationEntity() traceLocationDao.delete(eventEntity) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt index 17ab6fff55381a602b6809f809f15033e05bd72a..fe94d86be1bdf396a4b6ce1f2cf2c84f41151c96 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt @@ -7,9 +7,9 @@ interface TraceLocationRepository { val allTraceLocations: Flow<List<TraceLocation>> - fun addTraceLocation(event: TraceLocation) + fun addTraceLocation(traceLocation: TraceLocation) - fun deleteTraceLocation(event: TraceLocation) + fun deleteTraceLocation(traceLocation: TraceLocation) fun deleteAllTraceLocations() } 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 62534f7290aa26c8cc7948ecfc0b71cd7b3166f1..bdde7bc4b2042dd63f9e3f034599452f0cf4422c 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}" + traceLocationCdnUrl shouldBe "https://tracelocation-${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.CREATE_TRACELOCATION.rawKey shouldBe "CREATE_TRACELOCATION_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", + "CREATE_TRACELOCATION_URL": "https://tracelocation-PROD", "SAFETYNET_API_KEY": "placeholder-PROD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD" }, @@ -155,9 +158,10 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-DEV", "DATA_DONATION_CDN_URL": "https://datadonation-DEV", "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV", + "CREATE_TRACELOCATION_URL": "https://tracelocation-DEV", "SAFETYNET_API_KEY": "placeholder-DEV", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV" - }, + }, "INT": { "USE_EUR_KEY_PKGS" : false, "SUBMISSION_CDN_URL": "https://submission-INT", @@ -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", + "CREATE_TRACELOCATION_URL": "https://tracelocation-INT", "SAFETYNET_API_KEY": "placeholder-INT", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT" }, @@ -175,8 +180,10 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU", "DATA_DONATION_CDN_URL": "https://datadonation-WRU", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU", + "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU", "SAFETYNET_API_KEY": "placeholder-WRU", - "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU" + "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU", + "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU" }, "WRU-XD": { "USE_EUR_KEY_PKGS" : true, @@ -185,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", + "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU-XD", "SAFETYNET_API_KEY": "placeholder-WRU-XD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD" }, @@ -195,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", + "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU-XA", "SAFETYNET_API_KEY": "placeholder-WRU-XA", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA" }, @@ -205,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", + "CREATE_TRACELOCATION_URL": "https://tracelocation-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/checkins/qrcode/SignedTraceLocationToTraceLocationMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/SignedTraceLocationToTraceLocationMapperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..73e845e338116bb5e8bdca0b3322c80b2fb39bc8 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/SignedTraceLocationToTraceLocationMapperTest.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.eventregistration.events.server.TraceLocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.toByteString +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +internal class SignedTraceLocationToTraceLocationMapperTest : BaseTest() { + + @Test + fun `SignedTraceLocation_toTraceLocation() should map temporary event correctly`() { + TraceLocationData.signedTraceLocationTemporary.toTraceLocation() shouldBe TraceLocation( + guid = "serverGeneratedGuid", + version = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "Event Registration Release Party", + address = "SAP Headquarter", + startDate = Instant.parse("2021-05-01T19:00:00.000Z"), + endDate = Instant.parse("2021-05-01T23:30:00.000Z"), + byteRepresentation = TraceLocationData.signedTraceLocationTemporary.location.toByteArray().toByteString(), + signature = "ServerSignature".toByteArray().toByteString(), + defaultCheckInLengthInMinutes = 180 + ) + } + + @Test + fun `SignedTraceLocation_toTraceLocation() should map permanent event correctly`() { + TraceLocationData.signedTraceLocationPermanent.toTraceLocation() shouldBe TraceLocation( + guid = "serverGeneratedGuid", + version = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "IceCream Shop", + address = "IceCream Wonderland Street 1", + startDate = null, + endDate = null, + byteRepresentation = TraceLocationData.signedTraceLocationPermanent.location.toByteArray().toByteString(), + signature = "ServerSignature".toByteArray().toByteString(), + defaultCheckInLengthInMinutes = 30 + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationCreatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationCreatorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb260f3ddb23040e4af5b53632f883ebd1749ba8 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationCreatorTest.kt @@ -0,0 +1,79 @@ +package de.rki.coronawarnapp.eventregistration.events + +import com.google.protobuf.ByteString +import dagger.Lazy +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocation +import de.rki.coronawarnapp.eventregistration.events.server.TraceLocationServer +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.security.SignatureValidation +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.verify +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +internal class TraceLocationCreatorTest : BaseTest() { + + @MockK lateinit var api: Lazy<TraceLocationServer> + @MockK lateinit var repository: TraceLocationRepository + @MockK lateinit var signatureValidation: SignatureValidation + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + private fun createInstance() = TraceLocationCreator(api, repository, signatureValidation) + + @Test + fun `createTraceLocation() should return traceLocation and store it in repository when everything works fine`() = + runBlockingTest { + + val traceLocationToReturn = TraceLocationOuterClass.TraceLocation.newBuilder() + .setVersion(TRACE_LOCATION_VERSION) + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_PRIVATE_EVENT) + .setDescription("Top Secret Private Event") + .setAddress("top secret address") + .setStartTimestamp(Instant.parse("2020-01-01T14:00:00.000Z").seconds) + .setEndTimestamp(Instant.parse("2020-01-01T18:00:00.000Z").seconds) + .setDefaultCheckInLengthInMinutes(180) + .build() + + val signedTraceLocationToReturn = TraceLocationOuterClass.SignedTraceLocation.newBuilder() + .setLocation(traceLocationToReturn.toByteString()) + .setSignature(ByteString.copyFromUtf8("Signature")) + .build() + + coEvery { api.get().retrieveSignedTraceLocation(any()) } returns signedTraceLocationToReturn + every { signatureValidation.hasValidSignature(any(), any()) } returns true + every { repository.addTraceLocation(any()) } just Runs + + val userInput = TraceLocationUserInput( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_PRIVATE_EVENT, + description = "Top Secret Private Event", + address = "top secret address", + startDate = Instant.parse("2020-01-01T14:00:00.000Z"), + endDate = Instant.parse("2020-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 180 + ) + + val actualTraceLocation = createInstance().createTraceLocation(userInput) + val expectedTraceLocation = signedTraceLocationToReturn.toTraceLocation() + + verify(exactly = 1) { repository.addTraceLocation(expectedTraceLocation) } + + actualTraceLocation shouldBe expectedTraceLocation + } + + // TODO: Add tests for exception handling when exception handling is specified in the TechSpecs +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocationKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationMapperTest.kt similarity index 99% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocationKtTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationMapperTest.kt index 88032335e06c03d8d6ccbdc8606b8da673f46b68..5e171b843c2f03417585fee74b56973d1ae14178 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocationKtTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationMapperTest.kt @@ -12,7 +12,7 @@ import org.joda.time.Instant import org.junit.jupiter.api.Test import testhelpers.BaseTest -internal class DefaultTraceLocationKtTest : BaseTest() { +internal class TraceLocationMapperTest : BaseTest() { @Test fun `toTraceLocation() should map to correct object when providing all arguments`() { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationData.kt new file mode 100644 index 0000000000000000000000000000000000000000..797c31c55b5126afeb87a0f68ef52d263179c7e4 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationData.kt @@ -0,0 +1,53 @@ +package de.rki.coronawarnapp.eventregistration.events.server + +import com.google.protobuf.ByteString +import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import org.joda.time.Instant + +object TraceLocationData { + + val traceLocationTemporaryUserInput = TraceLocationUserInput( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "Event Registration Release Party", + address = "SAP Headquarter", + startDate = Instant.parse("2021-05-01T19:00:00.000Z"), + endDate = Instant.parse("2021-05-01T23:30:00.000Z"), + defaultCheckInLengthInMinutes = 180 + ) + + private val traceLocationTemporary: TraceLocationOuterClass.TraceLocation = + TraceLocationOuterClass.TraceLocation.newBuilder() + .setGuid("serverGeneratedGuid") + .setVersion(1) + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER) + .setDescription("Event Registration Release Party") + .setAddress("SAP Headquarter") + .setStartTimestamp(Instant.parse("2021-05-01T19:00:00.000Z").seconds) + .setEndTimestamp(Instant.parse("2021-05-01T23:30:00.000Z").seconds) + .setDefaultCheckInLengthInMinutes(180) + .build() + + val signedTraceLocationTemporary: TraceLocationOuterClass.SignedTraceLocation = + TraceLocationOuterClass.SignedTraceLocation.newBuilder() + .setLocation(traceLocationTemporary.toByteString()) + .setSignature(ByteString.copyFromUtf8("ServerSignature")) + .build() + + private val traceLocationPermanent: TraceLocationOuterClass.TraceLocation = + TraceLocationOuterClass.TraceLocation.newBuilder() + .setGuid("serverGeneratedGuid") + .setVersion(1) + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER) + .setDescription("IceCream Shop") + .setAddress("IceCream Wonderland Street 1") + .setDefaultCheckInLengthInMinutes(30) + .build() + + val signedTraceLocationPermanent: TraceLocationOuterClass.SignedTraceLocation = + TraceLocationOuterClass.SignedTraceLocation.newBuilder() + .setLocation(traceLocationPermanent.toByteString()) + .setSignature(ByteString.copyFromUtf8("ServerSignature")) + .build() +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationServerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..945ba8fc06aadb9616282ecfbc9e321575437d5b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/TraceLocationServerTest.kt @@ -0,0 +1,106 @@ +package de.rki.coronawarnapp.eventregistration.events.server + +import dagger.Lazy +import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import retrofit2.HttpException +import retrofit2.Response +import testhelpers.BaseTest + +internal class TraceLocationServerTest : BaseTest() { + + @MockK lateinit var api: Lazy<CreateTraceLocationApiV1> + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + @AfterEach + fun tearDown() { + } + + private fun getInstance() = TraceLocationServer(api) + + @Test + fun `retrieveSignedTraceLocation() should return SignedTraceLocation when everything works fine`() = + runBlockingTest { + + val signedTraceLocationMock = TraceLocationData.signedTraceLocationTemporary + coEvery { api.get().createTraceLocation(any()) } returns Response.success(200, signedTraceLocationMock) + + val userInput = TraceLocationData.traceLocationTemporaryUserInput + val actualSignedTraceLocation = getInstance().retrieveSignedTraceLocation(userInput) + + actualSignedTraceLocation shouldBe signedTraceLocationMock + } + + @Test + fun `retrieveSignedTraceLocation() should throw HttpException when we receive an unsuccessful response`() = + runBlockingTest { + coEvery { api.get().createTraceLocation(any()) } returns Response.error( + 400, + "Client Error".toResponseBody() + ) + + shouldThrow<HttpException> { + getInstance().retrieveSignedTraceLocation(TraceLocationData.traceLocationTemporaryUserInput) + } + } + + // TODO: Add additional error handling tests once it is defined in TecSpec + + @Test + fun `toTraceLocationProtoBuf() should map user input correctly for temporary trace locations`() { + TraceLocationUserInput( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "Event Registration Release Party", + address = "SAP Headquarter", + startDate = Instant.parse("2021-05-01T19:00:00.000Z"), + endDate = Instant.parse("2021-05-01T23:30:00.000Z"), + defaultCheckInLengthInMinutes = 180 + ).toTraceLocationProtoBuf().run { + guid shouldBe "" + version shouldBe 1 + type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER + description shouldBe "Event Registration Release Party" + address shouldBe "SAP Headquarter" + startTimestamp shouldBe Instant.parse("2021-05-01T19:00:00.000Z").seconds + endTimestamp shouldBe Instant.parse("2021-05-01T23:30:00.000Z").seconds + defaultCheckInLengthInMinutes shouldBe 180 + } + } + + @Test + fun `toTraceLocationProtoBuf() should map user input correctly for permanent trace locations`() { + TraceLocationUserInput( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "IceCream Shop", + address = "IceCream Wonderland Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 30 + ).toTraceLocationProtoBuf().run { + guid shouldBe "" + version shouldBe 1 + type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER + description shouldBe "IceCream Shop" + address shouldBe "IceCream Wonderland Street 1" + startTimestamp shouldBe 0 + endTimestamp shouldBe 0 + defaultCheckInLengthInMinutes shouldBe 30 + } + } +} 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..4ffeb6e16ff3ded079df2dcbda081d0d1a992c6f 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 @@ -38,6 +38,7 @@ class DebugOptionsFragmentViewModelTest : BaseTestInstrumentation() { every { environmentSetup.downloadCdnUrl } returns "downloadUrl" every { environmentSetup.verificationCdnUrl } returns "verificationUrl" every { environmentSetup.dataDonationCdnUrl } returns "dataDonationUrl" + every { environmentSetup.traceLocationCdnUrl } returns "createTraceLocationUrl" every { environmentSetup.currentEnvironment = any() } answers { currentEnvironment = arg(0)