From e87c2028f92ed0758e6f6b323cc9ade03550c6f9 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Mon, 11 Jan 2021 17:27:29 +0100
Subject: [PATCH] Additional debug log censoring (EXPOSUREAPP-4451) (#2060)

* Add log censoring for diary locations, persons and QR code token.

* Fix divider not being hidden if bug report entry is hidden.

* Fix censor modules not being applied in succession if multiple modules match.

* Remove debug helper and address registration token PR issue

Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com>
Co-authored-by: ralfgehrer <mail@ralfgehrer.com>
---
 .../bugreporting/BugReportingSharedModule.kt  | 26 +++++-
 .../bugreporting/censors/BugCensor.kt         |  2 +-
 .../censors/DiaryLocationCensor.kt            | 44 ++++++++++
 .../bugreporting/censors/DiaryPersonCensor.kt | 44 ++++++++++
 .../bugreporting/censors/QRCodeCensor.kt      | 29 +++++++
 .../censors/RegistrationTokenCensor.kt        | 18 ++--
 .../bugreporting/debuglog/DebugLogger.kt      | 11 ++-
 .../bugreporting/debuglog/DebugLoggerScope.kt |  4 +-
 .../ui/information/InformationFragment.kt     |  2 +-
 .../scan/SubmissionQRCodeScanViewModel.kt     |  2 +
 .../util/debug/UncaughtExceptionLogger.kt     |  5 ++
 .../src/main/res/layout/include_row.xml       |  1 +
 .../censors/DiaryLocationCensorTest.kt        | 86 +++++++++++++++++++
 .../censors/DiaryPersonCensorTest.kt          | 86 +++++++++++++++++++
 .../bugreporting/censors/QRCodeCensorTest.kt  | 84 ++++++++++++++++++
 .../censors/RegistrationTokenCensorTest.kt    | 28 +++---
 .../scan/SubmissionQRCodeScanViewModelTest.kt |  4 +
 17 files changed, 447 insertions(+), 29 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
index 99c18ab32..cfb6696c5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
@@ -3,8 +3,15 @@ package de.rki.coronawarnapp.bugreporting
 import dagger.Module
 import dagger.Provides
 import de.rki.coronawarnapp.bugreporting.censors.BugCensor
+import de.rki.coronawarnapp.bugreporting.censors.DiaryLocationCensor
+import de.rki.coronawarnapp.bugreporting.censors.DiaryPersonCensor
+import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor
 import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor
 import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
+import de.rki.coronawarnapp.bugreporting.debuglog.DebugLoggerScope
+import de.rki.coronawarnapp.bugreporting.debuglog.DebuggerScope
+import kotlinx.coroutines.CoroutineScope
+import timber.log.Timber
 import javax.inject.Singleton
 
 @Module
@@ -14,9 +21,24 @@ class BugReportingSharedModule {
     @Provides
     fun debugLogger() = DebugLogger
 
+    @Singleton
+    @DebuggerScope
+    @Provides
+    fun scope(): CoroutineScope = DebugLoggerScope
+
     @Singleton
     @Provides
     fun censors(
-        registrationTokenCensor: RegistrationTokenCensor
-    ): List<BugCensor> = listOf(registrationTokenCensor)
+        registrationTokenCensor: RegistrationTokenCensor,
+        diaryPersonCensor: DiaryPersonCensor,
+        diaryLocationCensor: DiaryLocationCensor,
+        qrCodeCensor: QRCodeCensor
+    ): List<BugCensor> = listOf(
+        registrationTokenCensor,
+        diaryPersonCensor,
+        diaryLocationCensor,
+        qrCodeCensor
+    ).also {
+        Timber.d("Loaded BugCensors: %s", it)
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/BugCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/BugCensor.kt
index e9c2c69ea..1540f5b4d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/BugCensor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/BugCensor.kt
@@ -7,5 +7,5 @@ interface BugCensor {
     /**
      * If there is something to censor a new log line is returned, otherwise returns null
      */
-    fun checkLog(entry: LogLine): LogLine?
+    suspend fun checkLog(entry: LogLine): LogLine?
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt
new file mode 100644
index 000000000..6a30f1945
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt
@@ -0,0 +1,44 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.debuglog.DebuggerScope
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.util.CWADebug
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
+
+@Reusable
+class DiaryLocationCensor @Inject constructor(
+    @DebuggerScope debugScope: CoroutineScope,
+    diary: ContactDiaryRepository
+) : BugCensor {
+
+    private val locations by lazy {
+        diary.locations.stateIn(
+            scope = debugScope,
+            started = SharingStarted.Lazily,
+            initialValue = null
+        ).filterNotNull()
+    }
+
+    override suspend fun checkLog(entry: LogLine): LogLine? {
+        val locationsNow = locations.first()
+
+        if (locationsNow.isEmpty()) return null
+
+        var newMessage = locationsNow.fold(entry.message) { oldMsg, location ->
+            oldMsg.replace(location.locationName, "Location#${location.locationId}")
+        }
+
+        if (CWADebug.isDeviceForTestersBuild) {
+            newMessage = entry.message
+        }
+
+        return entry.copy(message = newMessage)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt
new file mode 100644
index 000000000..f2406f14e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt
@@ -0,0 +1,44 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.debuglog.DebuggerScope
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.util.CWADebug
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
+
+@Reusable
+class DiaryPersonCensor @Inject constructor(
+    @DebuggerScope debugScope: CoroutineScope,
+    diary: ContactDiaryRepository
+) : BugCensor {
+
+    private val persons by lazy {
+        diary.people.stateIn(
+            scope = debugScope,
+            started = SharingStarted.Lazily,
+            initialValue = null
+        ).filterNotNull()
+    }
+
+    override suspend fun checkLog(entry: LogLine): LogLine? {
+        val personsNow = persons.first()
+
+        if (personsNow.isEmpty()) return null
+
+        var newMessage = personsNow.fold(entry.message) { oldMsg, person ->
+            oldMsg.replace(person.fullName, "Person#${person.personId}")
+        }
+
+        if (CWADebug.isDeviceForTestersBuild) {
+            newMessage = entry.message
+        }
+
+        return entry.copy(message = newMessage)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt
new file mode 100644
index 000000000..208ac8811
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt
@@ -0,0 +1,29 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import de.rki.coronawarnapp.util.CWADebug
+import javax.inject.Inject
+
+@Reusable
+class QRCodeCensor @Inject constructor() : BugCensor {
+
+    override suspend fun checkLog(entry: LogLine): LogLine? {
+
+        val guid = lastGUID ?: return null
+        if (!entry.message.contains(guid)) return null
+
+        var newMessage = entry.message.replace(guid, PLACEHOLDER + guid.takeLast(4))
+
+        if (CWADebug.isDeviceForTestersBuild) {
+            newMessage = entry.message
+        }
+
+        return entry.copy(message = newMessage)
+    }
+
+    companion object {
+        var lastGUID: String? = null
+        private const val PLACEHOLDER = "########-####-####-####-########"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt
index 5832e2f6f..590df2a5a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt
@@ -5,19 +5,23 @@ import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.util.CWADebug
 import javax.inject.Inject
-import kotlin.math.min
 
 @Reusable
 class RegistrationTokenCensor @Inject constructor() : BugCensor {
-    override fun checkLog(entry: LogLine): LogLine? {
+    override suspend fun checkLog(entry: LogLine): LogLine? {
         val token = LocalData.registrationToken() ?: return null
         if (!entry.message.contains(token)) return null
 
-        val replacement = if (CWADebug.isDeviceForTestersBuild) {
-            token
-        } else {
-            token.substring(0, min(4, token.length)) + "###-####-####-####-############"
+        var newMessage = entry.message.replace(token, PLACEHOLDER + token.takeLast(4))
+
+        if (CWADebug.isDeviceForTestersBuild) {
+            newMessage = entry.message
         }
-        return entry.copy(message = entry.message.replace(token, replacement))
+
+        return entry.copy(message = newMessage)
+    }
+
+    companion object {
+        private const val PLACEHOLDER = "########-####-####-####-########"
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt
index 2f4e3c065..e6e33aeeb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt
@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.util.CWADebug
 import de.rki.coronawarnapp.util.di.ApplicationComponent
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -107,8 +108,14 @@ object DebugLogger : DebugLoggerBase() {
                         while (!isDaggerReady) {
                             yield()
                         }
-                        val censoredLine = bugCensors.get().mapNotNull { it.checkLog(rawLine) }.firstOrNull()
-                        appendLogLine(censoredLine ?: rawLine)
+                        launch {
+                            // Censor data sources need a moment to know what to censor
+                            delay(1000)
+                            val censoredLine = bugCensors.get().fold(rawLine) { prev, censor ->
+                                censor.checkLog(prev) ?: prev
+                            }
+                            appendLogLine(censoredLine)
+                        }
                     }
                 } catch (e: CancellationException) {
                     Timber.tag(TAG).i("Logging was canceled.")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt
index 4943b1f11..6b48f8632 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt
@@ -6,10 +6,8 @@ import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.asCoroutineDispatcher
 import java.util.concurrent.Executors
 import javax.inject.Qualifier
-import javax.inject.Singleton
 import kotlin.coroutines.CoroutineContext
 
-@Singleton
 object DebugLoggerScope : CoroutineScope {
     val dispatcher = Executors.newSingleThreadExecutor(
         NamedThreadFactory("DebugLogger")
@@ -20,4 +18,4 @@ object DebugLoggerScope : CoroutineScope {
 @Qualifier
 @MustBeDocumented
 @Retention(AnnotationRetention.RUNTIME)
-annotation class AppScope
+annotation class DebuggerScope
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt
index 198e37eae..419cdeff8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt
@@ -58,7 +58,7 @@ class InformationFragment : Fragment(R.layout.fragment_information), AutoInject
         setAccessibilityDelegate()
 
         // TODO Hidden until further clarification regarding release schedule is available
-        binding.informationDebuglog.mainRow.isGone = !CWADebug.isDeviceForTestersBuild
+        binding.informationDebuglog.rootLayout.isGone = !CWADebug.isDeviceForTestersBuild
     }
 
     override fun onResume() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
index 8ce0bd334..37b4f7c72 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan
 
 import androidx.lifecycle.MutableLiveData
 import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.http.CwaWebException
@@ -30,6 +31,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor(
     fun validateTestGUID(rawResult: String) {
         val scanResult = QRScanResult(rawResult)
         if (scanResult.isValid) {
+            QRCodeCensor.lastGUID = scanResult.guid
             scanStatusValue.postValue(ScanStatus.SUCCESS)
             doDeviceRegistration(scanResult)
         } else {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt
index a9887e63f..2b7abc116 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.util.debug
 
+import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
 import timber.log.Timber
 
 class UncaughtExceptionLogger(
@@ -12,6 +13,10 @@ class UncaughtExceptionLogger(
 
     override fun uncaughtException(thread: Thread, error: Throwable) {
         Timber.tag(thread.name).e(error, "Uncaught exception!")
+        if (DebugLogger.isLogging) {
+            // Make sure this crash is written before killing the app.
+            Thread.sleep(1500)
+        }
         wrappedHandler?.uncaughtException(thread, error)
     }
 
diff --git a/Corona-Warn-App/src/main/res/layout/include_row.xml b/Corona-Warn-App/src/main/res/layout/include_row.xml
index 1f5997334..aa8e13047 100644
--- a/Corona-Warn-App/src/main/res/layout/include_row.xml
+++ b/Corona-Warn-App/src/main/res/layout/include_row.xml
@@ -27,6 +27,7 @@
     <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:id="@+id/root_layout"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent">
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt
new file mode 100644
index 000000000..991635564
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt
@@ -0,0 +1,86 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.util.CWADebug
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.mockkObject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class DiaryLocationCensorTest : BaseTest() {
+
+    @MockK lateinit var diaryRepo: ContactDiaryRepository
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkObject(CWADebug)
+        every { CWADebug.isDeviceForTestersBuild } returns false
+    }
+
+    @AfterEach
+    fun teardown() {
+        QRCodeCensor.lastGUID = null
+        clearAllMocks()
+    }
+
+    private fun createInstance(scope: CoroutineScope) = DiaryLocationCensor(
+        debugScope = scope,
+        diary = diaryRepo
+    )
+
+    private fun mockLocation(id: Long, name: String) = mockk<ContactDiaryLocation>().apply {
+        every { locationId } returns id
+        every { locationName } returns name
+    }
+
+    @Test
+    fun `censoring replaces the logline message`() = runBlockingTest {
+        every { diaryRepo.locations } returns flowOf(
+            listOf(mockLocation(1, "Berlin"), mockLocation(2, "Munich"), mockLocation(3, "Aachen"))
+        )
+        val instance = createInstance(this)
+        val censorMe = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Munich is nice, but Aachen is nice too.",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(censorMe) shouldBe censorMe.copy(
+            message = "Location#2 is nice, but Location#3 is nice too."
+        )
+
+        every { CWADebug.isDeviceForTestersBuild } returns true
+        instance.checkLog(censorMe) shouldBe censorMe.copy(
+            message = "Munich is nice, but Aachen is nice too."
+        )
+    }
+
+    @Test
+    fun `censoring returns null if there are no locations no match`() = runBlockingTest {
+        every { diaryRepo.locations } returns flowOf(emptyList())
+        val instance = createInstance(this)
+        val notCensored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Can't visit many cities during lockdown...",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(notCensored) shouldBe null
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt
new file mode 100644
index 000000000..fe731ed3e
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt
@@ -0,0 +1,86 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.util.CWADebug
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.mockkObject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class DiaryPersonCensorTest : BaseTest() {
+
+    @MockK lateinit var diaryRepo: ContactDiaryRepository
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkObject(CWADebug)
+        every { CWADebug.isDeviceForTestersBuild } returns false
+    }
+
+    @AfterEach
+    fun teardown() {
+        QRCodeCensor.lastGUID = null
+        clearAllMocks()
+    }
+
+    private fun createInstance(scope: CoroutineScope) = DiaryPersonCensor(
+        debugScope = scope,
+        diary = diaryRepo
+    )
+
+    private fun mockPerson(id: Long, name: String) = mockk<ContactDiaryPerson>().apply {
+        every { personId } returns id
+        every { fullName } returns name
+    }
+
+    @Test
+    fun `censoring replaces the logline message`() = runBlockingTest {
+        every { diaryRepo.people } returns flowOf(
+            listOf(mockPerson(1, "Luka"), mockPerson(2, "Ralf"), mockPerson(3, "Matthias"))
+        )
+        val instance = createInstance(this)
+        val censorMe = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Ralf needs more coffee, but Matthias has had enough for today.",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(censorMe) shouldBe censorMe.copy(
+            message = "Person#2 needs more coffee, but Person#3 has had enough for today."
+        )
+
+        every { CWADebug.isDeviceForTestersBuild } returns true
+        instance.checkLog(censorMe) shouldBe censorMe.copy(
+            message = "Ralf needs more coffee, but Matthias has had enough for today."
+        )
+    }
+
+    @Test
+    fun `censoring returns null if there are no persons no match`() = runBlockingTest {
+        every { diaryRepo.people } returns flowOf(emptyList())
+        val instance = createInstance(this)
+        val notCensored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "May 2021 be better than 2020.",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(notCensored) shouldBe null
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt
new file mode 100644
index 000000000..0fc7a543d
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt
@@ -0,0 +1,84 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import de.rki.coronawarnapp.util.CWADebug
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.mockkObject
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class QRCodeCensorTest : BaseTest() {
+
+    private val testGUID = "63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f"
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkObject(CWADebug)
+        every { CWADebug.isDeviceForTestersBuild } returns false
+    }
+
+    @AfterEach
+    fun teardown() {
+        QRCodeCensor.lastGUID = null
+        clearAllMocks()
+    }
+
+    private fun createInstance() = QRCodeCensor()
+
+    @Test
+    fun `censoring replaces the logline message`() = runBlockingTest {
+        QRCodeCensor.lastGUID = testGUID
+        val instance = createInstance()
+        val censored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "I'm a shy qrcode: $testGUID",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(censored) shouldBe censored.copy(
+            message = "I'm a shy qrcode: ########-####-####-####-########3a2f"
+        )
+
+        every { CWADebug.isDeviceForTestersBuild } returns true
+        instance.checkLog(censored) shouldBe censored.copy(
+            message = "I'm a shy qrcode: 63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f"
+        )
+    }
+
+    @Test
+    fun `censoring returns null if there is no match`() = runBlockingTest {
+        QRCodeCensor.lastGUID = testGUID.replace("f", "a")
+        val instance = createInstance()
+        val notCensored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "I'm a shy qrcode: $testGUID",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(notCensored) shouldBe null
+    }
+
+    @Test
+    fun `censoring aborts if no qrcode was set`() = runBlockingTest {
+        QRCodeCensor.lastGUID = null
+        val instance = createInstance()
+        val notCensored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "I'm a shy qrcode: $testGUID",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(notCensored) shouldBe null
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt
index 8c13580ef..89ae8cd1e 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt
@@ -9,6 +9,7 @@ import io.mockk.clearAllMocks
 import io.mockk.every
 import io.mockk.mockkObject
 import io.mockk.verify
+import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
@@ -37,7 +38,7 @@ class RegistrationTokenCensorTest : BaseTest() {
     private fun createInstance() = RegistrationTokenCensor()
 
     @Test
-    fun `censoring replaces the logline message`() {
+    fun `censoring replaces the logline message`() = runBlockingTest {
         val instance = createInstance()
         val filterMe = LogLine(
             timestamp = 1,
@@ -47,19 +48,25 @@ class RegistrationTokenCensorTest : BaseTest() {
             throwable = null
         )
         instance.checkLog(filterMe) shouldBe filterMe.copy(
-            message = "I'm a shy registration token: 63b4###-####-####-####-############"
+            message = "I'm a shy registration token: ########-####-####-####-########3a2f"
+        )
+
+        every { CWADebug.isDeviceForTestersBuild } returns true
+        instance.checkLog(filterMe) shouldBe filterMe.copy(
+            message = "I'm a shy registration token: 63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f"
         )
 
         verify { LocalData.registrationToken() }
     }
 
     @Test
-    fun `censoring returns null if thereis no match`() {
+    fun `censoring returns null if there is no token`() = runBlockingTest {
+        every { LocalData.registrationToken() } returns null
         val instance = createInstance()
         val filterMeNot = LogLine(
             timestamp = 1,
             priority = 3,
-            message = "I'm not a registration token ;)",
+            message = "I'm a shy registration token: $testToken",
             tag = "I'm a tag",
             throwable = null
         )
@@ -67,20 +74,15 @@ class RegistrationTokenCensorTest : BaseTest() {
     }
 
     @Test
-    fun `token is not censored on tester builds`() {
-        every { CWADebug.isDeviceForTestersBuild } returns true
+    fun `censoring returns null if there is no match`() = runBlockingTest {
         val instance = createInstance()
-        val filterMe = LogLine(
+        val filterMeNot = LogLine(
             timestamp = 1,
             priority = 3,
-            message = "I'm a shy registration token: $testToken",
+            message = "I'm not a registration token ;)",
             tag = "I'm a tag",
             throwable = null
         )
-        instance.checkLog(filterMe) shouldBe filterMe.copy(
-            message = "I'm a shy registration token: 63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f"
-        )
-
-        verify { LocalData.registrationToken() }
+        instance.checkLog(filterMeNot) shouldBe null
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
index b4136caab..9df500c21 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.ui.submission.qrcode.scan
 
+import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor
 import de.rki.coronawarnapp.playbook.BackgroundNoise
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.ScanStatus
@@ -40,10 +41,13 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
 
         viewModel.scanStatusValue.value shouldBe ScanStatus.STARTED
 
+        QRCodeCensor.lastGUID = null
+
         // valid guid
         val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
         viewModel.validateTestGUID("https://localhost/?$guid")
         viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.SUCCESS, it.value) }
+        QRCodeCensor.lastGUID = guid
 
         // invalid guid
         viewModel.validateTestGUID("https://no-guid-here")
-- 
GitLab