diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt index e41065b3c8ee61e11a10d85ea24fdbf3c0ed4fff..1a35bb0361d6c87e14447419bebb714fb747f23b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt @@ -6,9 +6,11 @@ import dagger.Module import dagger.Provides import dagger.Reusable import de.rki.coronawarnapp.util.serialization.adapter.ByteArrayAdapter +import de.rki.coronawarnapp.util.serialization.adapter.ByteStringBase64Adapter import de.rki.coronawarnapp.util.serialization.adapter.DurationAdapter import de.rki.coronawarnapp.util.serialization.adapter.InstantAdapter import de.rki.coronawarnapp.util.serialization.adapter.LocalDateAdapter +import okio.ByteString import org.joda.time.Duration import org.joda.time.Instant import org.joda.time.LocalDate @@ -24,5 +26,6 @@ class SerializationModule { .registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()) .registerTypeAdapter(Duration::class.java, DurationAdapter()) .registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter()) + .registerTypeAdapter(ByteString::class.java, ByteStringBase64Adapter()) .create() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/adapter/ByteStringBase64Adapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/adapter/ByteStringBase64Adapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4abf80511d4ea75b9e08aa093ae50a9d8ad0b6f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/adapter/ByteStringBase64Adapter.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.util.serialization.adapter + +import com.google.gson.JsonParseException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import okio.ByteString +import okio.ByteString.Companion.decodeBase64 +import org.json.JSONObject.NULL + +class ByteStringBase64Adapter : TypeAdapter<ByteString>() { + override fun write(out: JsonWriter, value: ByteString?) { + if (value == null) out.nullValue() + else value.base64().let { out.value(it) } + } + + override fun read(reader: JsonReader): ByteString? = when (reader.peek()) { + NULL -> reader.nextNull().let { null } + else -> { + val raw = reader.nextString() + raw.decodeBase64() ?: throw JsonParseException("Can't decode base64 ByteString: $raw") + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt index c11b906f2bcd77cfa50e5410d5f3f790039fc43a..006f67b7ecae3ac0a8e40f73addb29ce59deec54 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt @@ -1,7 +1,27 @@ package de.rki.coronawarnapp.vaccination.core import org.joda.time.Instant +import org.joda.time.LocalDate + +interface ProofCertificate { + val personIdentifier: VaccinatedPersonIdentifier -data class ProofCertificate( val expiresAt: Instant -) + + val firstName: String? + val lastName: String + + val dateOfBirth: LocalDate + + val vaccineName: String + val medicalProductName: String + val vaccineManufacturer: String + + val doseNumber: Int + val totalSeriesOfDoses: Int + + val vaccinatedAt: LocalDate + + val certificateIssuer: String + val certificateId: String +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt index 57190e9edfad71b42f3c993c753b5a0f403a09fe..fc29037ffa37a26efb60cd0a22a0868ef7a16af0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt @@ -1,32 +1,45 @@ package de.rki.coronawarnapp.vaccination.core -import org.joda.time.Instant +import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData +import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet import org.joda.time.LocalDate data class VaccinatedPerson( - val vaccinationCertificates: Set<VaccinationCertificate>, - val proofCertificates: Set<ProofCertificate>, - val isRefreshing: Boolean, - val lastUpdatedAt: Instant, + internal val data: PersonData, + private val valueSet: VaccinationValueSet?, + val isUpdatingData: Boolean = false, + val lastError: Throwable? = null, ) { - val identifier: VaccinatedPersonIdentifier = "" + val identifier: VaccinatedPersonIdentifier + get() = data.identifier - val firstName: String - get() = "" + val vaccinationStatus: Status + get() = if (proofCertificates.isNotEmpty()) Status.COMPLETE else Status.INCOMPLETE + + val vaccinationCertificates: Set<VaccinationCertificate> + get() = data.vaccinations.map { + it.toVaccinationCertificate(valueSet) + }.toSet() + + val proofCertificates: Set<ProofCertificate> + get() = data.proofs.map { + it.toProofCertificate(valueSet) + }.toSet() + + val firstName: String? + get() = vaccinationCertificates.first().firstName val lastName: String - get() = "" + get() = vaccinationCertificates.first().lastName val dateOfBirth: LocalDate - get() = LocalDate.now() + get() = vaccinationCertificates.first().dateOfBirth - val vaccinationStatus: Status - get() = if (proofCertificates.isNotEmpty()) Status.COMPLETE else Status.INCOMPLETE + val isEligbleForProofCertificate: Boolean + get() = data.isEligbleForProofCertificate enum class Status { INCOMPLETE, COMPLETE } } - -typealias VaccinatedPersonIdentifier = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..7629fcf8eec2503b8ce1bcebacbe0c4b06c5df6e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt @@ -0,0 +1,56 @@ +package de.rki.coronawarnapp.vaccination.core + +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateV1 +import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationDateOfBirthMissmatchException +import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationNameMissmatchException +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateV1 +import org.joda.time.LocalDate + +data class VaccinatedPersonIdentifier( + val dateOfBirth: LocalDate, + val lastNameStandardized: String, + val firstNameStandardized: String? +) { + val code: String by lazy { + val dob = dateOfBirth.toString() + val lastName = lastNameStandardized + val firstName = firstNameStandardized + "$dob#$lastName#$firstName" + } + + fun requireMatch(other: VaccinatedPersonIdentifier) { + if (lastNameStandardized != other.lastNameStandardized) { + throw VaccinationNameMissmatchException( + "Family name does not match, got ${other.lastNameStandardized}, expected $lastNameStandardized" + ) + } + if (firstNameStandardized != other.firstNameStandardized) { + throw VaccinationNameMissmatchException( + "Given name does not match, got ${other.firstNameStandardized}, expected $firstNameStandardized" + ) + } + if (dateOfBirth != other.dateOfBirth) { + throw VaccinationDateOfBirthMissmatchException( + "Date of birth does not match, got ${other.dateOfBirth}, expected $dateOfBirth" + ) + } + } +} + +val VaccinationCertificateV1.personIdentifier: VaccinatedPersonIdentifier + get() = VaccinatedPersonIdentifier( + dateOfBirth = dateOfBirth, + lastNameStandardized = nameData.familyNameStandardized, + firstNameStandardized = nameData.givenNameStandardized + ) + +val ProofCertificateV1.personIdentifier: VaccinatedPersonIdentifier + get() = VaccinatedPersonIdentifier( + dateOfBirth = dateOfBirth, + lastNameStandardized = nameData.familyNameStandardized, + firstNameStandardized = nameData.givenNameStandardized + ) + +val VaccinationCertificateQRCode.personIdentifier: VaccinatedPersonIdentifier + get() = parsedData.vaccinationCertificate.personIdentifier diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt index 2a8025ba3ca547ce56ff13905ee7dd449502dfe2..990316b2263122938888448170fee58831c5680a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt @@ -1,20 +1,24 @@ package de.rki.coronawarnapp.vaccination.core import de.rki.coronawarnapp.ui.Country -import org.joda.time.Instant import org.joda.time.LocalDate -data class VaccinationCertificate( - val firstName: String, - val lastName: String, - val dateOfBirth: LocalDate, - val vaccinatedAt: Instant, - val vaccineName: String, - val vaccineManufacturer: String, - val chargeId: String, - val certificateIssuer: String, - val certificateCountry: Country, - val certificateId: String, -) { - val identifier: VaccinatedPersonIdentifier get() = "" +interface VaccinationCertificate { + val firstName: String? + val lastName: String + val dateOfBirth: LocalDate + val vaccinatedAt: LocalDate + + val vaccineName: String + val vaccineManufacturer: String + val medicalProductName: String + + val doseNumber: Int + val totalSeriesOfDoses: Int + + val certificateIssuer: String + val certificateCountry: Country + val certificateId: String + + val personIdentifier: VaccinatedPersonIdentifier } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationException.kt new file mode 100644 index 0000000000000000000000000000000000000000..4963bf1bd500fddac6397d6da57a8f06a5d8eaf5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.vaccination.core + +open class VaccinationException( + cause: Throwable?, + message: String +) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..a88454e14122585eea2c2c91cd35b78c8bc648ad --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt @@ -0,0 +1,39 @@ +package de.rki.coronawarnapp.vaccination.core.qrcode + +import okio.ByteString +import org.joda.time.LocalDate + +class VaccinationCertificateCOSEParser { + + fun parse(vaccinationCOSE: ByteString): VaccinationCertificateData { + // TODO + val cert = VaccinationCertificateV1( + version = "1.0.0", + nameData = VaccinationCertificateV1.NameData( + givenName = "François-Joan", + givenNameStandardized = "FRANCOIS<JOAN", + familyName = "d'Arsøns - van Halen", + familyNameStandardized = "DARSONS<VAN<HALEN", + ), + dateOfBirth = LocalDate.parse("2009-02-28"), + vaccinationDatas = listOf( + VaccinationCertificateV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119349007", + medicalProductId = "EU/1/20/1528", + marketAuthorizationHolderId = "ORG-100030215", + doseNumber = 1, + totalSeriesOfDoses = 2, + vaccinatedAt = LocalDate.parse("2021-04-21"), + countryOfVaccination = "NL", + certificateIssuer = "Ministry of Public Health, Welfare and Sport", + uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ", + ) + ), + ) + + return VaccinationCertificateData( + vaccinationCertificate = cert + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateData.kt new file mode 100644 index 0000000000000000000000000000000000000000..b22ec8052f825ecc91a4b6edae44c9d9d80dca5d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateData.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.vaccination.core.qrcode + +/** + * Represents the information gained from data in COSE representation + */ +data class VaccinationCertificateData constructor( + // Parsed json + val vaccinationCertificate: VaccinationCertificateV1 +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt index ad4ff24d66c095b366fc303af897299321717b59..89a0837d36b49162cd8595339bd9816749be648b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt @@ -1,9 +1,13 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import okio.ByteString + // TODO data class VaccinationCertificateQRCode( - // Vaccine or prophylaxis - val vaccineNameId: String, - val vaccineMedicinalProduct: String, - val marketAuthorizationHolder: String, -) + val parsedData: VaccinationCertificateData, + // COSE representation of the vaccination certificate (as byte sequence) + val certificateCOSE: ByteString, +) { + val uniqueCertificateIdentifier: String + get() = parsedData.vaccinationCertificate.vaccinationDatas.single().uniqueCertificateIdentifier +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..777fe822e5bbbe3f9a537b52afc597b8d4325082 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.vaccination.core.qrcode + +import com.google.gson.annotations.SerializedName +import org.joda.time.LocalDate + +data class VaccinationCertificateV1( + @SerializedName("ver") val version: String, + @SerializedName("nam") val nameData: NameData, + @SerializedName("dob") val dateOfBirth: LocalDate, + @SerializedName("v") val vaccinationDatas: List<VaccinationData>, +) { + + data class NameData( + @SerializedName("fn") val familyName: String?, + @SerializedName("fnt") val familyNameStandardized: String, + @SerializedName("gn") val givenName: String?, + @SerializedName("gnt") val givenNameStandardized: String?, + ) + + data class VaccinationData( + // Disease or agent targeted, e.g. "tg": "840539006" + @SerializedName("tg") val targetId: String, + // Vaccine or prophylaxis, e.g. "vp": "1119349007" + @SerializedName("vp") val vaccineId: String, + // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528", + @SerializedName("mp") val medicalProductId: String, + // Marketing Authorization Holder, e.g. "ma": "ORG-100030215", + @SerializedName("ma") val marketAuthorizationHolderId: String, + // Dose Number, e.g. "dn": 2 + @SerializedName("dn") val doseNumber: Int, + // Total Series of Doses, e.g. "sd": 2, + @SerializedName("sd") val totalSeriesOfDoses: Int, + // Date of Vaccination, e.g. "dt" : "2021-04-21" + @SerializedName("dt") val vaccinatedAt: LocalDate, + // Country of Vaccination, e.g. "co": "NL" + @SerializedName("co") val countryOfVaccination: String, + // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", + @SerializedName("is") val certificateIssuer: String, + // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + @SerializedName("ci") val uniqueCertificateIdentifier: String + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt index 1fdf66e001a7a7629d0d7ccf82976f4221ac996e..c4b6a7fb8536a0def5677044033c3cad525cee58 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt @@ -1,85 +1,207 @@ package de.rki.coronawarnapp.vaccination.core.repository -import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope -import de.rki.coronawarnapp.vaccination.core.ProofCertificate +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.flow.HotDataFlow +import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson +import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate +import de.rki.coronawarnapp.vaccination.core.personIdentifier import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinatedPersonNotFoundException +import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationCertificateNotFoundException +import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData +import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationStorage +import de.rki.coronawarnapp.vaccination.core.repository.storage.toProofContainer +import de.rki.coronawarnapp.vaccination.core.repository.storage.toVaccinationContainer +import de.rki.coronawarnapp.vaccination.core.server.VaccinationProofServer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import org.joda.time.Instant -import org.joda.time.LocalDate +import kotlinx.coroutines.plus +import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class VaccinationRepository @Inject constructor( - @AppScope private val scope: CoroutineScope, + @AppScope private val appScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + private val timeStamper: TimeStamper, + private val storage: VaccinationStorage, + private val valueSetsRepository: ValueSetsRepository, + private val vaccinationProofServer: VaccinationProofServer, ) { - private val vc = VaccinationCertificate( - firstName = "Max", - lastName = "Mustermann", - dateOfBirth = LocalDate.now(), - vaccinatedAt = Instant.now(), - vaccineName = "Comirnaty (mRNA)", - vaccineManufacturer = "BioNTech", - chargeId = "CB2342", - certificateIssuer = "Landratsamt Potsdam", - certificateCountry = Country.DE, - certificateId = "05930482748454836478695764787840" - ) - - private val vc1 = VaccinationCertificate( - firstName = "Max", - lastName = "Mustermann", - dateOfBirth = LocalDate.now(), - vaccinatedAt = Instant.now(), - vaccineName = "Comirnaty (mRNA)", - vaccineManufacturer = "BioNTech", - chargeId = "CB2342", - certificateIssuer = "Landratsamt Potsdam", - certificateCountry = Country.DE, - certificateId = "05930482748454836478695764787841" - ) - - private val pc = ProofCertificate( - expiresAt = Instant.now() - ) - - // TODO read from repos - val vaccinationInfos: Flow<Set<VaccinatedPerson>> = flowOf( - setOf( - VaccinatedPerson( - setOf(vc), - setOf(), - isRefreshing = false, - lastUpdatedAt = Instant.now() - ), - VaccinatedPerson( - setOf(vc1), - setOf(pc), - isRefreshing = false, - lastUpdatedAt = Instant.now() - ) - ) - ) + private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow( + loggingTag = TAG, + scope = appScope + dispatcherProvider.IO, + sharingBehavior = SharingStarted.Lazily, + ) { + storage.personContainers + .map { personContainer -> + VaccinatedPerson( + data = personContainer, + valueSet = null, + isUpdatingData = false, + lastError = null + ) + } + .toSet() + .also { Timber.tag(TAG).v("Restored vaccination data: %s", it) } + } + + init { + internalData.data + .onStart { Timber.tag(TAG).d("Observing test data.") } + .onEach { vaccinatedPersons -> + Timber.tag(TAG).v("Vaccination data changed: %s", vaccinatedPersons) + storage.personContainers = vaccinatedPersons.map { it.data }.toSet() + } + .catch { + it.reportProblem(TAG, "Failed to snapshot vaccination data to storage.") + throw it + } + .launchIn(appScope + dispatcherProvider.IO) + } + + val vaccinationInfos: Flow<Set<VaccinatedPerson>> = combine( + internalData.data, + valueSetsRepository.latestValueSet + ) { personDatas, currentValueSet -> + personDatas.map { it.copy(valueSet = currentValueSet) }.toSet() + } suspend fun registerVaccination( qrCode: VaccinationCertificateQRCode ): VaccinationCertificate { + Timber.tag(TAG).v("registerVaccination(qrCode=%s)", qrCode) + + val updatedData = internalData.updateBlocking { + val originalPerson = if (this.isNotEmpty()) { + Timber.tag(TAG).d("There is an existing person we must match.") + this.single().also { + it.identifier.requireMatch(qrCode.personIdentifier) + Timber.tag(TAG).i("New certificate matches existing person!") + } + } else { + VaccinatedPerson( + data = PersonData( + vaccinations = emptySet(), + proofs = emptySet() + ), + valueSet = null, + ) + } + + val newCertificate = qrCode.toVaccinationContainer(scannedAt = timeStamper.nowUTC) + + val modifiedPerson = originalPerson.copy( + data = originalPerson.data.copy( + vaccinations = originalPerson.data.vaccinations.plus(newCertificate) + ) + ) + + this.toMutableSet().apply { + remove(originalPerson) + add(modifiedPerson) + } + } + + val updatedPerson = updatedData.single { it.identifier == qrCode.personIdentifier } + + if (updatedPerson.isEligbleForProofCertificate) { + Timber.tag(TAG).i("%s is eligble for proof certificate, launching async check.", updatedPerson.identifier) + appScope.launch { + refresh(updatedPerson.identifier) + } + } + + return updatedPerson.vaccinationCertificates.single { + it.certificateId == qrCode.uniqueCertificateIdentifier + } + } + + suspend fun checkForProof(personIdentifier: VaccinatedPersonIdentifier?) { + Timber.tag(TAG).i("checkForProof(personIdentifier=%s)", personIdentifier) + withContext(appScope.coroutineContext) { + internalData.updateBlocking { + val originalPerson = this.singleOrNull { + it.identifier == personIdentifier + } ?: throw VaccinatedPersonNotFoundException("Identifier=$personIdentifier") + + val eligbleCert = originalPerson.data.vaccinations.first { it.isEligbleForProofCertificate } + + val proof = try { + vaccinationProofServer.getProofCertificate(eligbleCert.vaccinationCertificateCOSE) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to check for proof.") + null + } + + val modifiedPerson = proof?.let { + originalPerson.copy( + data = originalPerson.data.copy( + proofs = setOf(it.toProofContainer(timeStamper.nowUTC)) + ) + ) + } ?: originalPerson + + this.toMutableSet().apply { + remove(originalPerson) + add(modifiedPerson) + } + } + } throw NotImplementedError() } + suspend fun refresh(personIdentifier: VaccinatedPersonIdentifier?) { + Timber.tag(TAG).d("refresh(personIdentifier=%s)", personIdentifier) + // TODO + } + suspend fun clear() { - throw NotImplementedError() + Timber.tag(TAG).w("Clearing vaccination data.") + internalData.updateBlocking { + Timber.tag(TAG).v("Deleting: %s", this) + emptySet() + } } - suspend fun deleteVaccinationCertificate(vaccinationCertificateId: String) = - scope.launch { - // TODO delete Vaccination + suspend fun deleteVaccinationCertificate(vaccinationCertificateId: String) { + Timber.tag(TAG).w("deleteVaccinationCertificate(certificateId=%s)", vaccinationCertificateId) + internalData.updateBlocking { + val target = this.find { person -> + person.vaccinationCertificates.any { it.certificateId == vaccinationCertificateId } + } ?: throw VaccinationCertificateNotFoundException( + "No vaccination certificate found for $vaccinationCertificateId" + ) + + val newTarget = target.copy( + data = target.data.copy( + vaccinations = target.data.vaccinations.filter { + it.certificateId != vaccinationCertificateId + }.toSet() + ) + ) + + this.map { + if (it != target) newTarget else it + }.toSet() } + } + + companion object { + private const val TAG = "VaccinationRepository" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ffaf2e5348106232304541a11db4fcce9562c5e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.vaccination.core.repository + +import de.rki.coronawarnapp.submission.server.SubmissionServer +import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ValueSetsRepository @Inject constructor( + private val submissionServer: SubmissionServer +) { + + val latestValueSet: Flow<VaccinationValueSet?> = flowOf(null) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinatedPersonNotFoundException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinatedPersonNotFoundException.kt new file mode 100644 index 0000000000000000000000000000000000000000..34e54c6f088361b3679092e07e728ca63d970a28 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinatedPersonNotFoundException.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.vaccination.core.repository.errors + +import de.rki.coronawarnapp.vaccination.core.VaccinationException + +class VaccinatedPersonNotFoundException( + message: String +) : VaccinationException( + message = message, + cause = null +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationCertificateNotFoundException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationCertificateNotFoundException.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b3562f9093a0d15fe7e7c812d0de28f299ac056 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationCertificateNotFoundException.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.vaccination.core.repository.errors + +import de.rki.coronawarnapp.vaccination.core.VaccinationException + +class VaccinationCertificateNotFoundException( + message: String +) : VaccinationException( + message = message, + cause = null +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationDateOfBirthMissmatchException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationDateOfBirthMissmatchException.kt new file mode 100644 index 0000000000000000000000000000000000000000..bedc695d22726250f93bcb4273b84dc17fc21430 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationDateOfBirthMissmatchException.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.vaccination.core.repository.errors + +import de.rki.coronawarnapp.vaccination.core.VaccinationException + +class VaccinationDateOfBirthMissmatchException( + message: String +) : VaccinationException( + message = message, + cause = null +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationNameMissmatchException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationNameMissmatchException.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e27e652d6519f83fa9473c2a0e064c4b77e1411 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationNameMissmatchException.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.vaccination.core.repository.errors + +import de.rki.coronawarnapp.vaccination.core.VaccinationException + +class VaccinationNameMissmatchException( + message: String +) : VaccinationException( + message = message, + cause = null +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/PersonData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/PersonData.kt new file mode 100644 index 0000000000000000000000000000000000000000..527f570ce6f7538bea563fd77e4739ef735fbaab --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/PersonData.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier +import org.joda.time.Instant + +data class PersonData( + @SerializedName("vaccinationData") val vaccinations: Set<VaccinationContainer>, + @SerializedName("proofData") val proofs: Set<ProofContainer>, + @SerializedName("lastSuccessfulProofCertificateRun") val lastSuccessfulPCRunAt: Instant = Instant.EPOCH, + @SerializedName("proofCertificateRunPending") val isPCRunPending: Boolean = false, +) { + val identifier: VaccinatedPersonIdentifier + get() = vaccinations.first().personIdentifier + + val isEligbleForProofCertificate: Boolean + get() = vaccinations.any { it.isEligbleForProofCertificate } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainer.kt new file mode 100644 index 0000000000000000000000000000000000000000..fec60be5d44adc3971a72a13f2acf3927020e955 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainer.kt @@ -0,0 +1,84 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.vaccination.core.ProofCertificate +import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier +import de.rki.coronawarnapp.vaccination.core.personIdentifier +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateCOSEParser +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateData +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateResponse +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateV1 +import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet +import okio.ByteString +import org.joda.time.Instant +import org.joda.time.LocalDate + +@Keep +data class ProofContainer( + @SerializedName("proofCOSE") val proofCOSE: ByteString, + @SerializedName("receivedAt") val receivedAt: Instant, +) { + @Transient internal var preParsedData: ProofCertificateData? = null + + // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null + @Suppress("unused") + constructor() : this(ByteString.EMPTY, Instant.EPOCH) + + @delegate:Transient + private val proofData: ProofCertificateData by lazy { + preParsedData ?: ProofCertificateCOSEParser().parse(proofCOSE) + } + + val proof: ProofCertificateV1 + get() = proofData.proofCertificate + + val vaccination: ProofCertificateV1.VaccinationData + get() = proof.vaccinationDatas.single() + + val personIdentifier: VaccinatedPersonIdentifier + get() = proof.personIdentifier + + fun toProofCertificate(valueSet: VaccinationValueSet?): ProofCertificate = object : ProofCertificate { + override val expiresAt: Instant + get() = proofData.expiresAt + + override val personIdentifier: VaccinatedPersonIdentifier + get() = proof.personIdentifier + + override val firstName: String? + get() = proof.nameData.givenName + override val lastName: String + get() = proof.nameData.familyName ?: proof.nameData.familyNameStandardized + override val dateOfBirth: LocalDate + get() = proof.dateOfBirth + + override val vaccinatedAt: LocalDate + get() = vaccination.vaccinatedAt + + override val doseNumber: Int + get() = vaccination.doseNumber + override val totalSeriesOfDoses: Int + get() = vaccination.totalSeriesOfDoses + + override val vaccineName: String + get() = valueSet?.getDisplayText(vaccination.vaccineId) ?: vaccination.vaccineId + override val vaccineManufacturer: String + get() = valueSet?.getDisplayText(vaccination.marketAuthorizationHolderId) + ?: vaccination.marketAuthorizationHolderId + override val medicalProductName: String + get() = valueSet?.getDisplayText(vaccination.medicalProductId) ?: vaccination.medicalProductId + + override val certificateIssuer: String + get() = vaccination.certificateIssuer + override val certificateId: String + get() = vaccination.uniqueCertificateIdentifier + } +} + +fun ProofCertificateResponse.toProofContainer(receivedAt: Instant) = ProofContainer( + proofCOSE = proofCertificateCOSE, + receivedAt = receivedAt, +).apply { + preParsedData = proofCertificateData +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt new file mode 100644 index 0000000000000000000000000000000000000000..0519e17300c05a76cc42e22cdc853c9fb826fe1a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt @@ -0,0 +1,92 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier +import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate +import de.rki.coronawarnapp.vaccination.core.personIdentifier +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateCOSEParser +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateV1 +import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet +import okio.ByteString +import org.joda.time.Instant +import org.joda.time.LocalDate + +@Keep +data class VaccinationContainer( + @SerializedName("vaccinationCertificateCOSE") val vaccinationCertificateCOSE: ByteString, + @SerializedName("scannedAt") val scannedAt: Instant, +) { + + @Transient internal var preParsedData: VaccinationCertificateData? = null + + // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null + @Suppress("unused") + constructor() : this(ByteString.EMPTY, Instant.EPOCH) + + @delegate:Transient + private val certificateData: VaccinationCertificateData by lazy { + preParsedData ?: VaccinationCertificateCOSEParser().parse(vaccinationCertificateCOSE) + } + + val certificate: VaccinationCertificateV1 + get() = certificateData.vaccinationCertificate + + val vaccination: VaccinationCertificateV1.VaccinationData + get() = certificate.vaccinationDatas.single() + + val certificateId: String + get() = vaccination.uniqueCertificateIdentifier + + val personIdentifier: VaccinatedPersonIdentifier + get() = certificate.personIdentifier + + val isEligbleForProofCertificate: Boolean + get() = vaccination.doseNumber == vaccination.totalSeriesOfDoses + + fun toVaccinationCertificate(valueSet: VaccinationValueSet?) = object : VaccinationCertificate { + override val personIdentifier: VaccinatedPersonIdentifier + get() = certificate.personIdentifier + + override val firstName: String? + get() = certificate.nameData.givenName + override val lastName: String + get() = certificate.nameData.familyName ?: certificate.nameData.familyNameStandardized + + override val dateOfBirth: LocalDate + get() = certificate.dateOfBirth + + override val vaccinatedAt: LocalDate + get() = vaccination.vaccinatedAt + + override val doseNumber: Int + get() = vaccination.doseNumber + override val totalSeriesOfDoses: Int + get() = vaccination.totalSeriesOfDoses + + override val vaccineName: String + get() = valueSet?.getDisplayText(vaccination.vaccineId) ?: vaccination.vaccineId + override val vaccineManufacturer: String + get() = valueSet?.getDisplayText(vaccination.marketAuthorizationHolderId) + ?: vaccination.marketAuthorizationHolderId + override val medicalProductName: String + get() = valueSet?.getDisplayText(vaccination.medicalProductId) ?: vaccination.medicalProductId + + override val certificateIssuer: String + get() = vaccination.certificateIssuer + override val certificateCountry: Country + get() = Country.values().singleOrNull { it.code == vaccination.countryOfVaccination } ?: Country.DE + override val certificateId: String + get() = vaccination.uniqueCertificateIdentifier + } +} + +fun VaccinationCertificateQRCode.toVaccinationContainer(scannedAt: Instant) = VaccinationContainer( + vaccinationCertificateCOSE = certificateCOSE, + scannedAt = scannedAt, +).apply { + preParsedData = parsedData +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt new file mode 100644 index 0000000000000000000000000000000000000000..15753b86bf2fbaa06e0431de786fea901bab1e44 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt @@ -0,0 +1,64 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import android.content.Context +import androidx.core.content.edit +import com.google.gson.Gson +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.fromJson +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class VaccinationStorage @Inject constructor( + @AppContext val context: Context, + @BaseGson val baseGson: Gson +) { + + private val prefs by lazy { + context.getSharedPreferences("vaccination_localdata", Context.MODE_PRIVATE) + } + + private val gson by lazy { + // Allow for custom type adapter. + baseGson + } + + var personContainers: Set<PersonData> + get() { + Timber.tag(TAG).d("vaccinatedPersons - load()") + val persons = prefs.all.mapNotNull { (key, value) -> + if (!key.startsWith(PKEY_PERSON_PREFIX)) { + return@mapNotNull null + } + value as String + gson.fromJson<PersonData>(value).also { + Timber.tag(TAG).v("Person loaded: %s", it) + requireNotNull(it.identifier) + } + } + return persons.toSet() + } + set(persons) { + Timber.tag(TAG).d("vaccinatedPersons - save(%s)", persons) + + prefs.edit { + prefs.all.keys.filter { it.startsWith(PKEY_PERSON_PREFIX) }.forEach { + Timber.tag(TAG).v("Removing data for %s", it) + remove(it) + } + persons.forEach { + val raw = gson.toJson(it) + val identifier = it.identifier + Timber.tag(TAG).v("Storing vaccinatedPerson %s -> %s", identifier, raw) + putString("$PKEY_PERSON_PREFIX${identifier.code}", raw) + } + } + } + + companion object { + private const val TAG = "VaccinationStorage" + private const val PKEY_PERSON_PREFIX = "vaccination.person." + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateCOSEParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateCOSEParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..97eafeda40e53f915ea00909d2f3b79e18e8077f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateCOSEParser.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.vaccination.core.server + +import okio.ByteString +import org.joda.time.Instant +import org.joda.time.LocalDate + +class ProofCertificateCOSEParser { + + fun parse(proofCOSE: ByteString): ProofCertificateData { + // TODO + val cert = ProofCertificateV1( + version = "1.0.0", + nameData = ProofCertificateV1.NameData( + givenName = "François-Joan", + givenNameStandardized = "FRANCOIS<JOAN", + familyName = "d'Arsøns - van Halen", + familyNameStandardized = "DARSONS<VAN<HALEN", + ), + dateOfBirth = LocalDate.parse("2009-02-28"), + vaccinationDatas = listOf( + ProofCertificateV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119349007", + medicalProductId = "EU/1/20/1528", + marketAuthorizationHolderId = "ORG-100030215", + doseNumber = 2, + totalSeriesOfDoses = 2, + vaccinatedAt = LocalDate.parse("2021-04-22"), + countryOfVaccination = "DE", + certificateIssuer = "Ministry of Public Health, Welfare and Sport", + uniqueCertificateIdentifier = "urn:uvci:01:NL:THECAKEISALIE", + ) + ) + ) + return ProofCertificateData( + proofCertificate = cert, + issuedAt = Instant.EPOCH, + issuerCountryCode = "DE", + expiresAt = Instant.EPOCH + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateData.kt new file mode 100644 index 0000000000000000000000000000000000000000..382dbc8d37a6189fc195035123c3e1a6f85c3adf --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateData.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.vaccination.core.server + +import org.joda.time.Instant + +/** + * Represents the information gained from data in COSE representation + */ +data class ProofCertificateData constructor( + // Parsed json + val proofCertificate: ProofCertificateV1, + // Issuer (2-letter country code) + val issuerCountryCode: String, + // Issued at (server data returns UNIX timestamp in seconds) + val issuedAt: Instant, + // Expiration time (server data returns UNIX timestamp in seconds) + val expiresAt: Instant, +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateException.kt new file mode 100644 index 0000000000000000000000000000000000000000..447790e02173bdb25da275124eb56b65c913e9c0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateException.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.vaccination.core.server + +import de.rki.coronawarnapp.vaccination.core.VaccinationException + +open class ProofCertificateException( + cause: Throwable?, + message: String +) : VaccinationException( + message = message, + cause = cause +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateResponse.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc1ab87a8b3f8a8e25569c67ce7cb4115e7a97a5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateResponse.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.vaccination.core.server + +import okio.ByteString + +interface ProofCertificateResponse { + val proofCertificateData: ProofCertificateData + + // COSE representation of the Proof Certificate (as byte sequence) + val proofCertificateCOSE: ByteString +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateServerData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateServerData.kt deleted file mode 100644 index f298c33287488bab4704e00cfda3570da5b50271..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateServerData.kt +++ /dev/null @@ -1,3 +0,0 @@ -package de.rki.coronawarnapp.vaccination.core.server - -interface ProofCertificateServerData diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..0808cdbd70c310ad988fa034857b603fad4aa790 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateV1.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.vaccination.core.server + +import com.google.gson.annotations.SerializedName +import org.joda.time.LocalDate + +// TODO check correctness, copy paste from vaccination cert +data class ProofCertificateV1( + @SerializedName("ver") val version: String, + @SerializedName("nam") val nameData: NameData, + @SerializedName("dob") val dateOfBirth: LocalDate, + @SerializedName("v") val vaccinationDatas: List<VaccinationData>, +) { + + data class NameData( + @SerializedName("fn") val familyName: String?, + @SerializedName("fnt") val familyNameStandardized: String, + @SerializedName("gn") val givenName: String?, + @SerializedName("gnt") val givenNameStandardized: String?, + ) + + data class VaccinationData( + // Disease or agent targeted, e.g. "tg": "840539006" + @SerializedName("tg") val targetId: String, + // Vaccine or prophylaxis, e.g. "vp": "1119349007" + @SerializedName("vp") val vaccineId: String, + // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528", + @SerializedName("mp") val medicalProductId: String, + // Marketing Authorization Holder, e.g. "ma": "ORG-100030215", + @SerializedName("ma") val marketAuthorizationHolderId: String, + // Dose Number, e.g. "dn": 2 + @SerializedName("dn") val doseNumber: Int, + // Total Series of Doses, e.g. "sd": 2, + @SerializedName("sd") val totalSeriesOfDoses: Int, + // Date of Vaccination, e.g. "dt" : "2021-04-21" + @SerializedName("dt") val vaccinatedAt: LocalDate, + // Country of Vaccination, e.g. "co": "NL" + @SerializedName("co") val countryOfVaccination: String, + // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", + @SerializedName("is") val certificateIssuer: String, + // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" + @SerializedName("ci") val uniqueCertificateIdentifier: String + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt index 8259e4855f9567628a75dd586470d6f83b2b4098..dd3a546794c27e8fcf0a258ef032cc564b0de142 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt @@ -1,15 +1,18 @@ package de.rki.coronawarnapp.vaccination.core.server -import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate +import dagger.Reusable +import okio.ByteString +import javax.inject.Inject /** * Talks with IBM servers? */ -class VaccinationProofServer { +@Reusable +class VaccinationProofServer @Inject constructor() { suspend fun getProofCertificate( - vaccinationCertificate: Set<VaccinationCertificate> - ): ProofCertificateServerData { + vaccinationCertificate: ByteString + ): ProofCertificateResponse { throw NotImplementedError() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt index 4f2c5e9fc53ac18b3b4d5dc7d269f7837212b93f..3b4ed12919eec83292dbd27d63cbecabb23e245b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt @@ -1,11 +1,15 @@ package de.rki.coronawarnapp.vaccination.core.server +import dagger.Reusable import java.util.Locale +import javax.inject.Inject /** * Talks with CWA servers */ -class VaccinationServer { +@Reusable +class VaccinationServer @Inject constructor() { + suspend fun getVaccinationValueSets(languageCode: Locale): VaccinationValueSet { throw NotImplementedError() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt index f8c9d6d923ae637fe079c2a6c0aecc35a37c49a0..c1f88db7386d45b9a6e7a1ed50fbbf361fc22606 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt @@ -4,4 +4,6 @@ import java.util.Locale interface VaccinationValueSet { val languageCode: Locale + + fun getDisplayText(key: String): String? } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt index c413060ae32e7d4b499f16b91c61ef491a4f70fd..eb7be530e7fe4dce45c176839ce921b699a9c292 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt @@ -87,7 +87,7 @@ class VaccinationDetailsFragment : Fragment(R.layout.fragment_vaccination_detail vaccinatedAt.text = certificate.vaccinatedAt.toString(format) vaccineName.text = certificate.vaccineName vaccineManufacturer.text = certificate.vaccineManufacturer - chargeId.text = certificate.chargeId + chargeId.text = "Removed?" certificateIssuer.text = certificate.certificateIssuer certificateCountry.text = certificate.certificateCountry.getLabel(requireContext()) certificateId.text = certificate.certificateId diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8f8596ee00e7be776b987a6602f9ac8c8de5804 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt @@ -0,0 +1,196 @@ +package de.rki.coronawarnapp.vaccination.core + +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateV1 +import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData +import de.rki.coronawarnapp.vaccination.core.repository.storage.ProofContainer +import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainer +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateData +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateResponse +import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateV1 +import okio.ByteString +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant +import org.joda.time.LocalDate + +object VaccinationTestData { + + val PERSON_A_VAC_1_JSON = VaccinationCertificateV1( + version = "1.0.0", + nameData = VaccinationCertificateV1.NameData( + givenName = "François-Joan", + givenNameStandardized = "FRANCOIS<JOAN", + familyName = "d'Arsøns - van Halen", + familyNameStandardized = "DARSONS<VAN<HALEN", + ), + dateOfBirth = LocalDate.parse("2009-02-28"), + vaccinationDatas = listOf( + VaccinationCertificateV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119349007", + medicalProductId = "EU/1/20/1528", + marketAuthorizationHolderId = "ORG-100030215", + doseNumber = 1, + totalSeriesOfDoses = 2, + vaccinatedAt = LocalDate.parse("2021-04-21"), + countryOfVaccination = "NL", + certificateIssuer = "Ministry of Public Health, Welfare and Sport", + uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ", + ) + ), + ) + + val PERSON_A_VAC_1_DATA = VaccinationCertificateData( + vaccinationCertificate = PERSON_A_VAC_1_JSON + ) + + val PERSON_A_VAC_1_QRCODE = VaccinationCertificateQRCode( + parsedData = PERSON_A_VAC_1_DATA, + certificateCOSE = "VGhlIENha2UgaXMgTm90IGEgTGll".decodeBase64()!! + ) + + val PERSON_A_VAC_1_CONTAINER = VaccinationContainer( + scannedAt = Instant.ofEpochMilli(1620062834471), + vaccinationCertificateCOSE = "VGhlIGNha2UgaXMgYSBsaWUu".decodeBase64()!!, + ).apply { + preParsedData = PERSON_A_VAC_1_DATA + } + + val PERSON_A_VAC_2_JSON = VaccinationCertificateV1( + version = "1.0.0", + nameData = VaccinationCertificateV1.NameData( + givenName = "François-Joan", + givenNameStandardized = "FRANCOIS<JOAN", + familyName = "d'Arsøns - van Halen", + familyNameStandardized = "DARSONS<VAN<HALEN", + ), + dateOfBirth = LocalDate.parse("2009-02-28"), + vaccinationDatas = listOf( + VaccinationCertificateV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119349007", + medicalProductId = "EU/1/20/1528", + marketAuthorizationHolderId = "ORG-100030215", + doseNumber = 1, + totalSeriesOfDoses = 2, + vaccinatedAt = LocalDate.parse("2021-04-22"), + countryOfVaccination = "NL", + certificateIssuer = "Ministry of Public Health, Welfare and Sport", + uniqueCertificateIdentifier = "urn:uvci:01:NL:THECAKEISALIE", + ) + ), + ) + + val PERSON_A_VAC_2_DATA = VaccinationCertificateData( + vaccinationCertificate = PERSON_A_VAC_2_JSON + ) + + val PERSON_A_VAC_2_QRCODE = VaccinationCertificateQRCode( + parsedData = PERSON_A_VAC_2_DATA, + certificateCOSE = "VGhlIGNha2UgaXMgYSBsaWUu".decodeBase64()!! + ) + + val PERSON_A_VAC_2_CONTAINER = VaccinationContainer( + scannedAt = Instant.ofEpochMilli(1620149234473), + vaccinationCertificateCOSE = "VGhlIENha2UgaXMgTm90IGEgTGll".decodeBase64()!!, + ).apply { + preParsedData = PERSON_A_VAC_2_DATA + } + + val PERSON_A_PROOF_JSON = ProofCertificateV1( + version = "1.0.0", + nameData = ProofCertificateV1.NameData( + givenName = "François-Joan", + givenNameStandardized = "FRANCOIS<JOAN", + familyName = "d'Arsøns - van Halen", + familyNameStandardized = "DARSONS<VAN<HALEN", + ), + dateOfBirth = LocalDate.parse("2009-02-28"), + vaccinationDatas = listOf( + ProofCertificateV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119349007", + medicalProductId = "EU/1/20/1528", + marketAuthorizationHolderId = "ORG-100030215", + doseNumber = 2, + totalSeriesOfDoses = 2, + vaccinatedAt = LocalDate.parse("2021-04-22"), + countryOfVaccination = "DE", + certificateIssuer = "Ministry of Public Health, Welfare and Sport", + uniqueCertificateIdentifier = "urn:uvci:01:NL:THECAKEISALIE", + ) + ) + ) + + val PERSON_A_PROOF_DATA = ProofCertificateData( + proofCertificate = PERSON_A_PROOF_JSON, + issuedAt = Instant.EPOCH, + issuerCountryCode = "DE", + expiresAt = Instant.EPOCH + ) + + val PERSON_A_PROOF_1_CONTAINER = ProofContainer( + receivedAt = Instant.ofEpochMilli(1620062834474), + proofCOSE = "VGhpc0lzQVByb29mQ09TRQ".decodeBase64()!!, + ).apply { + preParsedData = PERSON_A_PROOF_DATA + } + + val PERSON_A_PROOF_1_RESPONSE = object : ProofCertificateResponse { + override val proofCertificateData: ProofCertificateData + get() = ProofCertificateData( + proofCertificate = PERSON_A_PROOF_JSON, + expiresAt = Instant.EPOCH, + issuedAt = Instant.EPOCH, + issuerCountryCode = "DE", + ) + override val proofCertificateCOSE: ByteString + get() = "VGhpc0lzQVByb29mQ09TRQ".decodeBase64()!! + } + + val PERSON_A_DATA_2VAC_PROOF = PersonData( + vaccinations = setOf(PERSON_A_VAC_1_CONTAINER, PERSON_A_VAC_2_CONTAINER), + proofs = setOf(PERSON_A_PROOF_1_CONTAINER), + ) + + val PERSON_B_VAC_1_JSON = VaccinationCertificateV1( + version = "1.0.0", + nameData = VaccinationCertificateV1.NameData( + givenName = "Sir Jakob", + givenNameStandardized = "SIR<JAKOB", + familyName = "Von Mustermensch", + familyNameStandardized = "VON<MUSTERMENSCH", + ), + dateOfBirth = LocalDate.parse("1996-12-24"), + vaccinationDatas = listOf( + VaccinationCertificateV1.VaccinationData( + targetId = "840539006", + vaccineId = "1119349007", + medicalProductId = "EU/1/20/1528", + marketAuthorizationHolderId = "ORG-100030215", + doseNumber = 1, + totalSeriesOfDoses = 2, + vaccinatedAt = LocalDate.parse("2021-04-21"), + countryOfVaccination = "NL", + certificateIssuer = "Ministry of Public Health, Welfare and Sport", + uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ", + ) + ) + ) + val PERSON_B_VAC_1_DATA = VaccinationCertificateData( + vaccinationCertificate = PERSON_B_VAC_1_JSON + ) + + val PERSON_B_VAC_1_CONTAINER = VaccinationContainer( + scannedAt = Instant.ofEpochMilli(1620062834471), + vaccinationCertificateCOSE = "VGhpc0lzSmFrb2I".decodeBase64()!!, + ).apply { + preParsedData = PERSON_B_VAC_1_DATA + } + + val PERSON_B_DATA_1VAC_NOPROOF = PersonData( + vaccinations = setOf(PERSON_B_VAC_1_CONTAINER), + proofs = emptySet() + ) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9a707688ee09916569fac6caa90e9828045a41d4 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt @@ -0,0 +1,135 @@ +package de.rki.coronawarnapp.vaccination.core.repository + +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.vaccination.core.VaccinationTestData +import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData +import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationStorage +import de.rki.coronawarnapp.vaccination.core.server.VaccinationProofServer +import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider +import testhelpers.coroutines.runBlockingTest2 +import timber.log.Timber + +class VaccinationRepositoryTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + + @MockK lateinit var storage: VaccinationStorage + @MockK lateinit var valueSetsRepository: ValueSetsRepository + @MockK lateinit var vaccinationProofServer: VaccinationProofServer + @MockK lateinit var vaccinationValueSet: VaccinationValueSet + + private var testStorage: Set<PersonData> = emptySet() + + private var nowUTC = Instant.ofEpochMilli(1234567890) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns nowUTC + + every { valueSetsRepository.latestValueSet } returns flowOf(vaccinationValueSet) + + coEvery { vaccinationProofServer.getProofCertificate(any()) } returns VaccinationTestData.PERSON_A_PROOF_1_RESPONSE + + storage.apply { + every { personContainers } answers { testStorage } + every { personContainers = any() } answers { testStorage = arg(0) } + } + } + + private fun createInstance(scope: CoroutineScope) = VaccinationRepository( + appScope = scope, + dispatcherProvider = TestDispatcherProvider(), + timeStamper = timeStamper, + storage = storage, + valueSetsRepository = valueSetsRepository, + vaccinationProofServer = vaccinationProofServer, + ) + + @Test + fun `add new certificate - no prior data`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(this) + + advanceUntilIdle() + + instance.registerVaccination(VaccinationTestData.PERSON_A_VAC_1_QRCODE).apply { + Timber.i("Returned cert is %s", this) + this.personIdentifier shouldBe VaccinationTestData.PERSON_A_VAC_1_CONTAINER.personIdentifier + } + } + + @Test + fun `add new certificate - existing data`() = runBlockingTest2(ignoreActive = true) { +// val dataBefore = VaccinationTestData.PERSON_A_DATA_2VAC_PROOF.copy( +// vaccinations = setOf(VaccinationTestData.PERSON_A_VAC_1_CONTAINER), +// proofs = emptySet() +// ) +// val dataAfter = VaccinationTestData.PERSON_A_DATA_2VAC_PROOF.copy( +// vaccinations = setOf( +// VaccinationTestData.PERSON_A_VAC_1_CONTAINER, +// VaccinationTestData.PERSON_A_VAC_2_CONTAINER.copy(scannedAt = nowUTC) +// ), +// proofs = emptySet() +// ) +// testStorage = setOf(dataBefore) +// +// val instance = createInstance(this) +// +// advanceUntilIdle() +// +// instance.registerVaccination(VaccinationTestData.PERSON_A_VAC_2_QRCODE).apply { +// Timber.i("Returned cert is %s", this) +// this.personIdentifier shouldBe VaccinationTestData.PERSON_A_VAC_2_CONTAINER.personIdentifier +// } +// +// testStorage.first() shouldBe dataAfter + } + + @Test + fun `add new certificate - if eligble for proof, start request`() = runBlockingTest2(ignoreActive = true) { +// TODO() + } + + @Test + fun `add new certificate - does not match existing person`() { +// TODO() + } + + @Test + fun `add new certificate - duplicate certificate`() { +// TODO() + } + + @Test + fun `clear data`() { +// TODO() + } + + @Test + fun `remove certificate`() { +// TODO() + } + + @Test + fun `remove certificate - starts proof check if we deleted a vaccination that was eligble for proof`() { +// TODO() + } + + @Test + fun `check for new proof certificate`() { +// TODO() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3676dac8ed9c0a8379c18031d1777d1ae028a951 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainerTest.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class ProofContainerTest : BaseTest() { + + @Test + fun `person identifier calculation`() { + // TODO + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..46e3165ff8088c73ac7d6178352fafe6465826f3 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class VaccinationContainerTest : BaseTest() { + + @Test + fun `person identifier calculation`() { + // TODO + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7139897663af75b859c059e11ca2a5ac6e7303bf --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt @@ -0,0 +1,143 @@ +package de.rki.coronawarnapp.vaccination.core.repository.storage + +import android.content.Context +import androidx.core.content.edit +import de.rki.coronawarnapp.util.serialization.SerializationModule +import de.rki.coronawarnapp.vaccination.core.VaccinationTestData +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import okio.ByteString.Companion.decodeBase64 +import org.junit.Ignore +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.extensions.toComparableJsonPretty +import testhelpers.preferences.MockSharedPreferences + +@Ignore +class VaccinationStorageTest : BaseTest() { + + @MockK lateinit var context: Context + private lateinit var mockPreferences: MockSharedPreferences + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockPreferences = MockSharedPreferences() + + every { + context.getSharedPreferences("vaccination_localdata", Context.MODE_PRIVATE) + } returns mockPreferences + } + + private fun createInstance() = VaccinationStorage( + context = context, + baseGson = SerializationModule().baseGson() + ) + + @Test + fun `init is sideeffect free`() { + createInstance() + } + + @Test + fun `storing empty set deletes data`() { + mockPreferences.edit { + putString("dontdeleteme", "test") + putString("vaccination.person.test", "test") + } + createInstance().personContainers = emptySet() + + mockPreferences.dataMapPeek.keys.single() shouldBe "dontdeleteme" + } + + @Test + fun `store one fully vaccinated person`() { + val instance = createInstance() + instance.personContainers = setOf(VaccinationTestData.PERSON_A_DATA_2VAC_PROOF) + + val json = + (mockPreferences.dataMapPeek["vaccination.person.2009-02-28#DARSONS<VAN<HALEN#FRANCOIS<JOAN"] as String) + + json.toComparableJsonPretty() shouldBe """ + { + "vaccinationData": [ + { + "vaccinationCertificateCOSE": "VGhlIGNha2UgaXMgYSBsaWUu", + "scannedAt": 1620062834471 + }, + { + "vaccinationCertificateCOSE": "VGhlIENha2UgaXMgTm90IGEgTGll", + "scannedAt": 1620149234473 + } + ], + "proofData": [ + { + "proofCOSE": "VGhpc0lzQVByb29mQ09TRQ==", + "receivedAt": 1620062834474 + } + ], + "lastSuccessfulProofCertificateRun": 0, + "proofCertificateRunPending": false + } + """.toComparableJsonPretty() + + instance.personContainers.single().apply { + this shouldBe VaccinationTestData.PERSON_A_DATA_2VAC_PROOF + this.vaccinations.map { it.vaccinationCertificateCOSE } shouldBe setOf( + "VGhlIGNha2UgaXMgYSBsaWUu".decodeBase64()!!, + "VGhlIENha2UgaXMgTm90IGEgTGll".decodeBase64()!!, + ) + this.proofs.map { it.proofCOSE } shouldBe setOf( + "VGhpc0lzQVByb29mQ09TRQ==".decodeBase64()!!, + ) + } + } + + @Test + fun `store incompletely vaccinated person`() { + val instance = createInstance() + instance.personContainers = setOf(VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF) + + val json = (mockPreferences.dataMapPeek["vaccination.person.1996-12-24#VON<MUSTERMENSCH#SIR<JAKOB"] as String) + + json.toComparableJsonPretty() shouldBe """ + { + "vaccinationData": [ + { + "vaccinationCertificateCOSE": "VGhpc0lzSmFrb2I=", + "scannedAt": 1620062834471 + } + ], + "proofData": [], + "lastSuccessfulProofCertificateRun": 0, + "proofCertificateRunPending": false + } + """.toComparableJsonPretty() + + instance.personContainers.single().apply { + this shouldBe VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF + this.vaccinations.single().vaccinationCertificateCOSE shouldBe "VGhpc0lzSmFrb2I=".decodeBase64()!! + } + } + + @Test + fun `store two persons`() { + createInstance().apply { + personContainers = setOf( + VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF, + VaccinationTestData.PERSON_A_DATA_2VAC_PROOF + ) + personContainers shouldBe setOf( + VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF, + VaccinationTestData.PERSON_A_DATA_2VAC_PROOF + ) + + personContainers = emptySet() + personContainers shouldBe emptySet() + } + } +}