From 310db328d6ae33e5eb7834fa5968b88de062cf98 Mon Sep 17 00:00:00 2001
From: Juraj Kusnier <jurajkusnier@users.noreply.github.com>
Date: Tue, 25 May 2021 10:44:14 +0200
Subject: [PATCH]  Contact Journal Extension - Store RAT and PCR Tests results
 (EXPOSUREAPP-6041) (#3238)

* prepare new TestResultRepository

* Code refactoring

* Update tests

* Remove old files

* Code refactoring

* Move TestJournal Table to ContactDiaryDatabase

* remove unused files

* revert DataReset.kt

* rename ContactDiaryTest > ContactDiaryCoronaTest

* rename ContactDiaryTestEntity > ContactDiaryCoronaTestEntity

* add some tests
---
 .../4.json                                    | 282 ++++++++++++++++++
 .../ContactDiaryDatabaseMigrationTest.kt      |  73 +++++
 .../storage/ContactDiaryDatabaseTest.kt       |  37 +++
 .../coronatest/ui/CoronaTestTestFragment.kt   |  22 +-
 .../ui/CoronaTestTestFragmentViewModel.kt     |   8 +
 .../res/layout/fragment_test_coronatest.xml   |  27 ++
 .../storage/ContactDiaryDatabase.kt           |  15 +-
 .../storage/dao/ContactDiaryCoronaTestDao.kt  |  21 ++
 .../entity/ContactDiaryCoronaTestEntity.kt    |  56 ++++
 .../converters/ContactDiaryRoomConverters.kt  |  15 +
 .../ContactDiaryDatabaseMigration3To4.kt      |  38 +++
 .../storage/repo/ContactDiaryRepository.kt    |   7 +
 .../repo/DefaultContactDiaryRepository.kt     |  20 ++
 .../coronatest/CoronaTestRepository.kt        |   3 +
 .../coronatest/type/CoronaTest.kt             |   4 +-
 .../coronatest/type/pcr/PCRCoronaTest.kt      |   6 +-
 .../type/rapidantigen/RACoronaTest.kt         |   6 +-
 .../storage/CoronaTestStorageTest.kt          |   4 -
 .../rki/coronawarnapp/util/DataResetTest.kt   |   2 +-
 19 files changed, 620 insertions(+), 26 deletions(-)
 create mode 100644 Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt

diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json
new file mode 100644
index 000000000..393379694
--- /dev/null
+++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json
@@ -0,0 +1,282 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 4,
+    "identityHash": "3ed51cebcbafb1960bf5194f27748e12",
+    "entities": [
+      {
+        "tableName": "locations",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationName` TEXT NOT NULL, `phoneNumber` TEXT, `emailAddress` TEXT, `traceLocationID` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "locationId",
+            "columnName": "locationId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "locationName",
+            "columnName": "locationName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "phoneNumber",
+            "columnName": "phoneNumber",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "emailAddress",
+            "columnName": "emailAddress",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traceLocationID",
+            "columnName": "traceLocationID",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "locationId"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "locationvisits",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` TEXT NOT NULL, `fkLocationId` INTEGER NOT NULL, `duration` INTEGER, `circumstances` TEXT, `checkInID` INTEGER, FOREIGN KEY(`fkLocationId`) REFERENCES `locations`(`locationId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "date",
+            "columnName": "date",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "fkLocationId",
+            "columnName": "fkLocationId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "duration",
+            "columnName": "duration",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "circumstances",
+            "columnName": "circumstances",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "checkInID",
+            "columnName": "checkInID",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_locationvisits_fkLocationId",
+            "unique": false,
+            "columnNames": [
+              "fkLocationId"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_locationvisits_fkLocationId` ON `${TABLE_NAME}` (`fkLocationId`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "locations",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "fkLocationId"
+            ],
+            "referencedColumns": [
+              "locationId"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "persons",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`personId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fullName` TEXT NOT NULL, `phoneNumber` TEXT, `emailAddress` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "personId",
+            "columnName": "personId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "fullName",
+            "columnName": "fullName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "phoneNumber",
+            "columnName": "phoneNumber",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "emailAddress",
+            "columnName": "emailAddress",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "personId"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "personencounters",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` TEXT NOT NULL, `fkPersonId` INTEGER NOT NULL, `durationClassification` TEXT, `withMask` INTEGER, `wasOutside` INTEGER, `circumstances` TEXT, FOREIGN KEY(`fkPersonId`) REFERENCES `persons`(`personId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "date",
+            "columnName": "date",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "fkPersonId",
+            "columnName": "fkPersonId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "durationClassification",
+            "columnName": "durationClassification",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "withMask",
+            "columnName": "withMask",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "wasOutside",
+            "columnName": "wasOutside",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "circumstances",
+            "columnName": "circumstances",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_personencounters_fkPersonId",
+            "unique": false,
+            "columnNames": [
+              "fkPersonId"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_personencounters_fkPersonId` ON `${TABLE_NAME}` (`fkPersonId`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "persons",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "fkPersonId"
+            ],
+            "referencedColumns": [
+              "personId"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "corona_tests",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `testType` TEXT NOT NULL, `result` TEXT NOT NULL, `time` TEXT NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "testType",
+            "columnName": "testType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "result",
+            "columnName": "result",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "time",
+            "columnName": "time",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ed51cebcbafb1960bf5194f27748e12')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt
index ca8693d3e..17c6963cb 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt
@@ -16,6 +16,7 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEncoun
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity
 import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration1To2
 import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration2To3
+import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration3To4
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
 import kotlinx.coroutines.flow.first
@@ -231,6 +232,78 @@ class ContactDiaryDatabaseMigrationTest : BaseTestInstrumentation() {
         }
     }
 
+    @Test
+    fun migrate3To4() {
+        val location = ContactDiaryLocationEntity(
+            locationId = 1,
+            locationName = "My Location Name",
+            phoneNumber = "1234567890",
+            emailAddress = "email@address.com",
+            traceLocationID = null
+        )
+
+        val locationVisit = ContactDiaryLocationVisitEntity(
+            id = 2,
+            date = LocalDate.parse("2020-12-31"),
+            fkLocationId = 1,
+            duration = Duration.standardMinutes(13),
+            circumstances = "N/A",
+            checkInID = null
+        )
+
+        val locationAfter = location.copy(traceLocationID = "jshrgu-aifhioaio-aofsjof-samofp-kjsadngsgf".decodeBase64())
+        val locationVisitAfter = locationVisit.copy(checkInID = 101)
+
+        val locationValues = ContentValues().apply {
+            put("locationId", location.locationId)
+            put("locationName", location.locationName)
+            put("phoneNumber", location.phoneNumber)
+            put("emailAddress", location.emailAddress)
+        }
+
+        val locationVisitValues = ContentValues().apply {
+            put("id", locationVisit.id)
+            put("date", locationVisit.date.toString())
+            put("fkLocationId", locationVisit.fkLocationId)
+            put("duration", locationVisit.duration?.millis)
+            put("circumstances", locationVisit.circumstances)
+        }
+
+        helper.createDatabase(DB_NAME, 3).apply {
+            insert("locations", SQLiteDatabase.CONFLICT_FAIL, locationValues)
+            insert("locationvisits", SQLiteDatabase.CONFLICT_FAIL, locationVisitValues)
+            close()
+        }
+
+        // Run migration
+        helper.runMigrationsAndValidate(
+            DB_NAME,
+            4,
+            true,
+            ContactDiaryDatabaseMigration3To4
+        )
+
+        val daoDb = ContactDiaryDatabase.Factory(
+            ctx = ApplicationProvider.getApplicationContext()
+        ).create(databaseName = DB_NAME)
+
+        runBlocking {
+            daoDb.locationVisitDao().allEntries().first().single() shouldBe ContactDiaryLocationVisitWrapper(
+                contactDiaryLocationEntity = location,
+                contactDiaryLocationVisitEntity = locationVisit
+            )
+
+            // Test if new attributes are added correctly
+            daoDb.locationDao().update(locationAfter)
+            daoDb.locationVisitDao().update(locationVisitAfter)
+
+            daoDb.locationVisitDao().allEntries().first().single() shouldBe ContactDiaryLocationVisitWrapper(
+                contactDiaryLocationEntity = locationAfter,
+                contactDiaryLocationVisitEntity = locationVisitAfter
+            )
+        }
+    }
+
     @Test
     fun migrate1To2_failure_throws() {
         helper.createDatabase(DB_NAME, 1).apply {
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt
index 6cdc211e2..fdbd5777c 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt
@@ -4,6 +4,11 @@ import androidx.room.Room
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.NEGATIVE
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.POSITIVE
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.ANTIGEN
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.PCR
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitWrapper
@@ -16,6 +21,7 @@ import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.runBlocking
 import okio.ByteString.Companion.decodeBase64
 import org.joda.time.Duration
+import org.joda.time.Instant
 import org.joda.time.LocalDate
 import org.junit.After
 import org.junit.Test
@@ -57,6 +63,12 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() {
         circumstances = "I had to buy snacks.",
         checkInID = 101
     )
+    private val coronaTest = ContactDiaryCoronaTestEntity(
+        id = "123-456-7890",
+        testType = PCR,
+        result = POSITIVE,
+        time = Instant.now()
+    )
 
     // DB
     private val contactDiaryDatabase: ContactDiaryDatabase = Room.inMemoryDatabaseBuilder(
@@ -68,6 +80,7 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() {
     private val locationDao = contactDiaryDatabase.locationDao()
     private val personEncounterDao = contactDiaryDatabase.personEncounterDao()
     private val locationVisitDao = contactDiaryDatabase.locationVisitDao()
+    private val coronaTestsDao = contactDiaryDatabase.coronaTestDao()
 
     private fun List<ContactDiaryPersonEncounterWrapper>.toContactDiaryPersonEncounterEntityList(): List<ContactDiaryPersonEncounterEntity> =
         this.map { it.contactDiaryPersonEncounterEntity }
@@ -229,4 +242,28 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() {
         personEncounterDao.update(updatedEncounter)
         personEncounterFlow.first().single() shouldBe updatedEncounter
     }
+
+    @Test
+    fun updatingCoronaTests(): Unit = runBlocking {
+        val coronaTestsFlow = coronaTestsDao.allTests()
+
+        coronaTestsDao.insertTest(coronaTest)
+        coronaTestsFlow.first().single() shouldBe coronaTest
+
+        val updatedTest = coronaTest.copy(
+            time = Instant.now(),
+            result = NEGATIVE,
+            testType = ANTIGEN
+        )
+
+        coronaTestsDao.insertTest(updatedTest)
+        coronaTestsFlow.first().single() shouldBe coronaTest
+
+        val newTest = coronaTest.copy(
+            id = "AAAAA-AAAAA-AAAAAA"
+        )
+
+        coronaTestsDao.insertTest(newTest)
+        coronaTestsFlow.first().containsAll(listOf(coronaTest, newTest)) shouldBe true
+    }
 }
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt
index 1209ca39b..4eaae484d 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt
@@ -26,7 +26,7 @@ import javax.inject.Inject
 class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), AutoInject {
 
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
-    private val vm: CoronaTestTestFragmentViewModel by cwaViewModels { viewModelFactory }
+    private val viewModel: CoronaTestTestFragmentViewModel by cwaViewModels { viewModelFactory }
 
     private val binding: FragmentTestCoronatestBinding by viewBinding()
 
@@ -50,7 +50,7 @@ class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), Auto
                     qrcodeScanContainer.isVisible = true
                     qrcodeScanPreview.resume()
                     qrcodeScanPreview.decodeSingle { result ->
-                        vm.onQRCodeScanner(result)
+                        viewModel.onQRCodeScanner(result)
                         stop()
                     }
                 }
@@ -65,26 +65,30 @@ class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), Auto
             qrcodeScanViewfinder.setCameraPreview(binding.qrcodeScanPreview)
         }
 
-        vm.pcrtState.observe2(this) {
+        viewModel.pcrtState.observe2(this) {
             binding.pcrtData.text = it.getNiceTextForHumans()
         }
         binding.apply {
-            pcrtDeleteAction.setOnClickListener { vm.deletePCRT() }
-            pcrtRefreshAction.setOnClickListener { vm.refreshPCRT() }
+            pcrtDeleteAction.setOnClickListener { viewModel.deletePCRT() }
+            pcrtRefreshAction.setOnClickListener { viewModel.refreshPCRT() }
         }
 
-        vm.ratState.observe2(this) {
+        viewModel.ratState.observe2(this) {
             binding.ratData.text = it.getNiceTextForHumans()
         }
         binding.apply {
-            ratDeleteAction.setOnClickListener { vm.deleteRAT() }
-            ratRefreshAction.setOnClickListener { vm.refreshRAT() }
+            ratDeleteAction.setOnClickListener { viewModel.deleteRAT() }
+            ratRefreshAction.setOnClickListener { viewModel.refreshRAT() }
         }
 
-        vm.errorEvents.observe2(this) {
+        viewModel.errorEvents.observe2(this) {
             val error = it.tryHumanReadableError(requireContext())
             Toast.makeText(requireContext(), error.description, Toast.LENGTH_LONG).show()
         }
+
+        viewModel.testsInContactDiary.observe2(this) {
+            binding.testsOutput.text = it
+        }
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt
index df524f3e1..544158248 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.asLiveData
 import com.journeyapps.barcodescanner.BarcodeResult
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.latestPCRT
 import de.rki.coronawarnapp.coronatest.latestRAT
@@ -23,6 +24,7 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
     private val coronaTestRepository: CoronaTestRepository,
     private val coronaTestQrCodeValidator: CoronaTestQrCodeValidator,
+    contactDiaryRepository: ContactDiaryRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val errorEvents = SingleLiveEvent<Throwable>()
@@ -38,6 +40,12 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor(
         )
     }.asLiveData(context = dispatcherProvider.Default)
 
+    val testsInContactDiary = contactDiaryRepository.testResults.map {
+        it.foldIndexed(StringBuilder()) { id, buffer, item ->
+            buffer.append(id).append(":\n").append(item).append("\n")
+        }.toString()
+    }.asLiveData(context = dispatcherProvider.Default)
+
     fun refreshPCRT() = launch {
         try {
             Timber.d("Refreshing PCR")
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml
index 3d0c99217..04454809e 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml
@@ -168,5 +168,32 @@
 
         </androidx.constraintlayout.widget.ConstraintLayout>
 
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/contact_diary_tests_container"
+            style="@style/Card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_tiny">
+
+            <TextView
+                android:id="@+id/contact_diary_tests_title"
+                style="@style/headline6Sixteen"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="8dp"
+                android:text="Contact diary tests"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <TextView
+                android:layout_marginTop="8dp"
+                android:id="@+id/tests_output"
+                app:layout_constraintTop_toBottomOf="@id/contact_diary_tests_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
     </LinearLayout>
 </androidx.core.widget.NestedScrollView>
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt
index e7081eac5..8a99d68de 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt
@@ -9,13 +9,16 @@ import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationDao
 import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationVisitDao
 import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonDao
 import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonEncounterDao
+import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryCoronaTestDao
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEncounterEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity
 import de.rki.coronawarnapp.contactdiary.storage.internal.converters.ContactDiaryRoomConverters
 import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration1To2
 import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration2To3
+import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration3To4
 import de.rki.coronawarnapp.util.database.CommonConverters
 import de.rki.coronawarnapp.util.di.AppContext
 import javax.inject.Inject
@@ -25,9 +28,10 @@ import javax.inject.Inject
         ContactDiaryLocationEntity::class,
         ContactDiaryLocationVisitEntity::class,
         ContactDiaryPersonEntity::class,
-        ContactDiaryPersonEncounterEntity::class
+        ContactDiaryPersonEncounterEntity::class,
+        ContactDiaryCoronaTestEntity::class
     ],
-    version = 3,
+    version = 4,
     exportSchema = true
 )
 @TypeConverters(CommonConverters::class, ContactDiaryRoomConverters::class)
@@ -37,11 +41,16 @@ abstract class ContactDiaryDatabase : RoomDatabase() {
     abstract fun locationVisitDao(): ContactDiaryLocationVisitDao
     abstract fun personDao(): ContactDiaryPersonDao
     abstract fun personEncounterDao(): ContactDiaryPersonEncounterDao
+    abstract fun coronaTestDao(): ContactDiaryCoronaTestDao
 
     class Factory @Inject constructor(@AppContext private val ctx: Context) {
         fun create(databaseName: String = CONTACT_DIARY_DATABASE_NAME): ContactDiaryDatabase = Room
             .databaseBuilder(ctx, ContactDiaryDatabase::class.java, databaseName)
-            .addMigrations(ContactDiaryDatabaseMigration1To2, ContactDiaryDatabaseMigration2To3)
+            .addMigrations(
+                ContactDiaryDatabaseMigration1To2,
+                ContactDiaryDatabaseMigration2To3,
+                ContactDiaryDatabaseMigration3To4
+            )
             .build()
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt
new file mode 100644
index 000000000..6e12d9afb
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt
@@ -0,0 +1,21 @@
+package de.rki.coronawarnapp.contactdiary.storage.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface ContactDiaryCoronaTestDao {
+
+    @Query("SELECT * FROM corona_tests")
+    fun allTests(): Flow<List<ContactDiaryCoronaTestEntity>>
+
+    @Insert(onConflict = OnConflictStrategy.IGNORE)
+    suspend fun insertTest(contactDiaryCoronaTestEntity: ContactDiaryCoronaTestEntity)
+
+    @Query("DELETE FROM corona_tests")
+    suspend fun deleteAll()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt
new file mode 100644
index 000000000..70f6e1104
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt
@@ -0,0 +1,56 @@
+package de.rki.coronawarnapp.contactdiary.storage.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.PCR
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.ANTIGEN
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.POSITIVE
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.NEGATIVE
+import org.joda.time.Instant
+
+@Entity(tableName = "corona_tests")
+data class ContactDiaryCoronaTestEntity(
+    @PrimaryKey @ColumnInfo(name = "id") val id: String,
+    @ColumnInfo(name = "testType") val testType: TestType,
+    @ColumnInfo(name = "result") val result: TestResult,
+    @ColumnInfo(name = "time") val time: Instant
+) {
+    enum class TestType(val raw: String) {
+        @SerializedName("PCR")
+        PCR("pcr"),
+
+        @SerializedName("ANTIGEN")
+        ANTIGEN("antigen");
+    }
+
+    enum class TestResult(val raw: String) {
+        @SerializedName("POSITIVE")
+        POSITIVE("POSITIVE"),
+
+        @SerializedName("NEGATIVE")
+        NEGATIVE("NEGATIVE");
+    }
+}
+
+fun CoronaTest.canBeAddedToJournal(): Boolean {
+    return isViewed && (isNegative || isPositive)
+}
+
+fun Map.Entry<CoronaTestGUID, CoronaTest>.asTestResultEntity(): ContactDiaryCoronaTestEntity {
+    return with(value) {
+        ContactDiaryCoronaTestEntity(
+            id = key,
+            testType = if (type == CoronaTest.Type.PCR) PCR else ANTIGEN,
+            result = if (isPositive) POSITIVE else NEGATIVE,
+            time = when (this) {
+                is RACoronaTest -> testedAt
+                else -> registeredAt
+            }
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt
index 95821990d..b7c034529 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.contactdiary.storage.internal.converters
 
 import androidx.room.TypeConverter
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity
 import org.joda.time.Duration
 
 class ContactDiaryRoomConverters {
@@ -25,4 +26,18 @@ class ContactDiaryRoomConverters {
     fun fromJodaDuration(duration: Duration?): Long? {
         return duration?.millis
     }
+
+    @TypeConverter
+    fun toTestType(value: String?): ContactDiaryCoronaTestEntity.TestType? =
+        ContactDiaryCoronaTestEntity.TestType.values().singleOrNull { it.raw == value }
+
+    @TypeConverter
+    fun fromTestType(type: ContactDiaryCoronaTestEntity.TestType?): String? = type?.raw
+
+    @TypeConverter
+    fun toTestResult(value: String?): ContactDiaryCoronaTestEntity.TestResult? =
+        ContactDiaryCoronaTestEntity.TestResult.values().singleOrNull { it.raw == value }
+
+    @TypeConverter
+    fun fromTestResult(type: ContactDiaryCoronaTestEntity.TestResult?): String? = type?.raw
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt
new file mode 100644
index 000000000..a789eed65
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt
@@ -0,0 +1,38 @@
+package de.rki.coronawarnapp.contactdiary.storage.internal.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
+import timber.log.Timber
+
+/**
+ * Migrates the contact diary database from schema version 3 to schema version 4.
+ * We are adding additional table for storing test results
+ */
+@Suppress("MaxLineLength")
+object ContactDiaryDatabaseMigration3To4 : Migration(3, 4) {
+
+    override fun migrate(database: SupportSQLiteDatabase) {
+        try {
+            Timber.i("Attempting migration 3->4...")
+            performMigration(database)
+            Timber.i("Migration 3->4 successful.")
+        } catch (e: Exception) {
+            Timber.e(e, "Migration 3->4 failed")
+            e.report(ExceptionCategory.INTERNAL, "ContactDiary database migration failed.")
+            throw e
+        }
+    }
+
+    private fun performMigration(database: SupportSQLiteDatabase) = with(database) {
+        Timber.d("Running MIGRATION_3_4")
+
+        migrateTestTable()
+    }
+
+    private val migrateTestTable: SupportSQLiteDatabase.() -> Unit = {
+        Timber.d("Create 'corona_tests' table")
+        execSQL("CREATE TABLE IF NOT EXISTS corona_tests (`id` TEXT NOT NULL, `testType` TEXT NOT NULL, `result` TEXT NOT NULL, `time` TEXT NOT NULL, PRIMARY KEY(`id`))")
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt
index c37acddbe..bd2b7c365 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt
@@ -4,6 +4,9 @@ import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity
+import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import kotlinx.coroutines.flow.Flow
 import org.joda.time.LocalDate
 
@@ -52,6 +55,10 @@ interface ContactDiaryRepository {
     suspend fun deletePersonEncounters(contactDiaryPersonEncounters: List<ContactDiaryPersonEncounter>)
     suspend fun deleteAllPersonEncounters()
 
+    // Tests
+    val testResults: Flow<List<ContactDiaryCoronaTestEntity>>
+    suspend fun updateTests(tests: Map<CoronaTestGUID, CoronaTest>)
+
     // Clean
     suspend fun clear()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt
index e94102ed8..279192f28 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt
@@ -10,6 +10,10 @@ import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationDao
 import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationVisitDao
 import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonDao
 import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonEncounterDao
+import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryCoronaTestDao
+import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity
+import de.rki.coronawarnapp.contactdiary.storage.entity.asTestResultEntity
+import de.rki.coronawarnapp.contactdiary.storage.entity.canBeAddedToJournal
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisit
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisitEntity
@@ -18,6 +22,8 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEnco
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounterEntity
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounterSortedList
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEntity
+import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 import org.joda.time.LocalDate
@@ -47,6 +53,10 @@ class DefaultContactDiaryRepository @Inject constructor(
         contactDiaryDatabase.personEncounterDao()
     }
 
+    private val contactDiaryCoronaTestDao: ContactDiaryCoronaTestDao by lazy {
+        contactDiaryDatabase.coronaTestDao()
+    }
+
     // Location
     override val locations: Flow<List<ContactDiaryLocation>> by lazy {
         contactDiaryLocationDao
@@ -243,6 +253,16 @@ class DefaultContactDiaryRepository @Inject constructor(
         contactDiaryPersonEncounterDao.deleteAll()
     }
 
+    override val testResults: Flow<List<ContactDiaryCoronaTestEntity>> by lazy {
+        contactDiaryCoronaTestDao.allTests()
+    }
+
+    override suspend fun updateTests(tests: Map<CoronaTestGUID, CoronaTest>) {
+        tests.filter { it.value.canBeAddedToJournal() }
+            .map { it.asTestResultEntity() }
+            .forEach { contactDiaryCoronaTestDao.insertTest(it) }
+    }
+
     private suspend fun executeWhenIdNotDefault(id: Long, action: (suspend () -> Unit) = { }) {
         if (id != 0L) {
             action()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt
index 740b2545e..8bf90d76a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.coronatest
 
 import de.rki.coronawarnapp.bugreporting.reportProblem
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException
 import de.rki.coronawarnapp.coronatest.errors.DuplicateCoronaTestException
 import de.rki.coronawarnapp.coronatest.errors.UnknownTestTypeException
@@ -39,6 +40,7 @@ class CoronaTestRepository @Inject constructor(
     private val storage: CoronaTestStorage,
     private val processors: Set<@JvmSuppressWildcards CoronaTestProcessor>,
     private val legacyMigration: PCRTestMigration,
+    private val contactDiaryRepository: ContactDiaryRepository
 ) {
 
     private val internalData: HotDataFlow<Map<CoronaTestGUID, CoronaTest>> = HotDataFlow(
@@ -62,6 +64,7 @@ class CoronaTestRepository @Inject constructor(
                 Timber.tag(TAG).v("CoronaTest data changed: %s", it)
                 storage.coronaTests = it.values.toSet()
                 legacyMigration.finishMigration()
+                contactDiaryRepository.updateTests(it)
             }
             .catch {
                 it.reportProblem(TAG, "Failed to snapshot CoronaTest data to storage.")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt
index 02353dcc1..dea5510d6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt
@@ -19,6 +19,7 @@ interface CoronaTest {
     val isViewed: Boolean
 
     val isPositive: Boolean
+    val isNegative: Boolean
 
     val isPending: Boolean
 
@@ -35,9 +36,6 @@ interface CoronaTest {
     // TODO why do we need this PER test
     val isAdvancedConsentGiven: Boolean
 
-    // TODO Why do we need to store this?
-    val isJournalEntryCreated: Boolean
-
     val isResultAvailableNotificationSent: Boolean
 
     enum class Type(val raw: String) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt
index 78c38a1d8..f9ef2816d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt
@@ -26,9 +26,6 @@ data class PCRCoronaTest(
     @SerializedName("isAdvancedConsentGiven")
     override val isAdvancedConsentGiven: Boolean = false,
 
-    @SerializedName("isJournalEntryCreated")
-    override val isJournalEntryCreated: Boolean = false,
-
     @SerializedName("isResultAvailableNotificationSent")
     override val isResultAvailableNotificationSent: Boolean = false,
 
@@ -54,6 +51,9 @@ data class PCRCoronaTest(
     override val isPositive: Boolean
         get() = testResult == CoronaTestResult.PCR_POSITIVE
 
+    override val isNegative: Boolean
+        get() = testResult == CoronaTestResult.PCR_NEGATIVE
+
     override val isPending: Boolean
         get() = testResult == CoronaTestResult.PCR_OR_RAT_PENDING
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt
index 6e353f9b9..2af0abee2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt
@@ -34,9 +34,6 @@ data class RACoronaTest(
     @SerializedName("isAdvancedConsentGiven")
     override val isAdvancedConsentGiven: Boolean = false,
 
-    @SerializedName("isJournalEntryCreated")
-    override val isJournalEntryCreated: Boolean = false,
-
     @SerializedName("isResultAvailableNotificationSent")
     override val isResultAvailableNotificationSent: Boolean = false,
 
@@ -92,6 +89,9 @@ data class RACoronaTest(
     override val isPositive: Boolean
         get() = testResult == RAT_POSITIVE
 
+    override val isNegative: Boolean
+        get() = testResult == RAT_NEGATIVE
+
     override val isPending: Boolean
         get() = setOf(PCR_OR_RAT_PENDING, RAT_PENDING).contains(testResult)
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt
index c3e2537a4..100c26a50 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt
@@ -47,7 +47,6 @@ class CoronaTestStorageTest : BaseTest() {
         isSubmitted = true,
         isViewed = true,
         isAdvancedConsentGiven = true,
-        isJournalEntryCreated = false,
         isResultAvailableNotificationSent = false,
         testResult = CoronaTestResult.PCR_POSITIVE,
         testResultReceivedAt = Instant.ofEpochMilli(2000),
@@ -60,7 +59,6 @@ class CoronaTestStorageTest : BaseTest() {
         isSubmitted = true,
         isViewed = true,
         isAdvancedConsentGiven = true,
-        isJournalEntryCreated = false,
         isResultAvailableNotificationSent = false,
         testResult = CoronaTestResult.RAT_POSITIVE,
         testResultReceivedAt = Instant.ofEpochMilli(2000),
@@ -109,7 +107,6 @@ class CoronaTestStorageTest : BaseTest() {
                     "isSubmitted": true,
                     "isViewed": true,
                     "isAdvancedConsentGiven": true,
-                    "isJournalEntryCreated": false,
                     "isResultAvailableNotificationSent": false,
                     "testResultReceivedAt": 2000,
                     "testResult": 2,
@@ -148,7 +145,6 @@ class CoronaTestStorageTest : BaseTest() {
                     "isSubmitted": true,
                     "isViewed": true,
                     "isAdvancedConsentGiven": true,
-                    "isJournalEntryCreated": false,
                     "isResultAvailableNotificationSent": false,
                     "testResultReceivedAt": 2000,
                     "lastUpdatedAt": 2001,
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt
index 3dbba9d53..6f8799d44 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt
@@ -24,8 +24,8 @@ import de.rki.coronawarnapp.storage.TracingSettings
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.ui.presencetracing.TraceLocationPreferences
-import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
 import de.rki.coronawarnapp.vaccination.core.VaccinationPreferences
+import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
 import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository
 import io.mockk.MockKAnnotations
 import io.mockk.coVerify
-- 
GitLab