From 402abec696bbf8fe490624b9fa026dc91d7e83e4 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Thu, 11 Mar 2021 15:03:43 +0100
Subject: [PATCH] Fix logging related OOM issue (EXPOSUREAPP-5670/5691) #2578

* Add tests to reproduce issue.

* Perform additional validity checks, especially for empty phonenumbers and locations to prevent recursion due to replacing "".

* Small optimizations.

* Refactoring.

* One more unit test doesn't hurt.

* Refactoring.

Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com>
---
 .../bugreporting/censors/BugCensor.kt         | 34 +++++++++
 .../censors/DiaryEncounterCensor.kt           | 12 +++-
 .../censors/DiaryLocationCensor.kt            | 16 +++--
 .../bugreporting/censors/DiaryPersonCensor.kt | 16 +++--
 .../bugreporting/censors/DiaryVisitCensor.kt  | 11 ++-
 .../bugreporting/censors/QRCodeCensor.kt      |  3 +-
 .../censors/RegistrationTokenCensor.kt        |  3 +-
 .../bugreporting/censors/BugCensorTest.kt     | 69 +++++++++++++++++++
 .../censors/DiaryEncounterCensorTest.kt       | 60 ++++++++++++++++
 .../censors/DiaryLocationCensorTest.kt        | 61 ++++++++++++++++
 .../censors/DiaryPersonCensorTest.kt          | 62 +++++++++++++++++
 .../censors/DiaryVisitCensorTest.kt           | 59 ++++++++++++++++
 12 files changed, 388 insertions(+), 18 deletions(-)
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/BugCensorTest.kt

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 1540f5b4d..8be45d08b 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
@@ -8,4 +8,38 @@ interface BugCensor {
      * If there is something to censor a new log line is returned, otherwise returns null
      */
     suspend fun checkLog(entry: LogLine): LogLine?
+
+    companion object {
+        fun withValidName(name: String?, action: (String) -> Unit): Boolean {
+            if (name.isNullOrBlank()) return false
+            if (name.length < 3) return false
+            action(name)
+            return true
+        }
+
+        fun withValidEmail(email: String?, action: (String) -> Unit): Boolean {
+            if (email.isNullOrBlank()) return false
+            if (email.length < 6) return false
+            action(email)
+            return true
+        }
+
+        fun withValidPhoneNumber(number: String?, action: (String) -> Unit): Boolean {
+            if (number.isNullOrBlank()) return false
+            if (number.length < 4) return false
+            action(number)
+            return true
+        }
+
+        fun withValidComment(comment: String?, action: (String) -> Unit): Boolean {
+            if (comment.isNullOrBlank()) return false
+            if (comment.length < 3) return false
+            action(comment)
+            return true
+        }
+
+        fun LogLine.toNewLogLineIfDifferent(newMessage: String): LogLine? {
+            return if (newMessage != message) copy(message = newMessage) else null
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt
index e8d6c96a8..b8d393411 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt
@@ -1,6 +1,8 @@
 package de.rki.coronawarnapp.bugreporting.censors
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidComment
 import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
@@ -31,11 +33,15 @@ class DiaryEncounterCensor @Inject constructor(
         if (encountersNow.isEmpty()) return null
 
         val newMessage = encountersNow.fold(entry.message) { orig, encounter ->
-            if (encounter.circumstances.isNullOrBlank()) return@fold orig
+            var wip = orig
 
-            orig.replace(encounter.circumstances!!, "Encounter#${encounter.id}/Circumstances")
+            withValidComment(encounter.circumstances) {
+                wip = wip.replace(it, "Encounter#${encounter.id}/Circumstances")
+            }
+
+            wip
         }
 
-        return entry.copy(message = newMessage)
+        return entry.toNewLogLineIfDifferent(newMessage)
     }
 }
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
index acf70ef4c..24bf031cf 100644
--- 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
@@ -1,6 +1,10 @@
 package de.rki.coronawarnapp.bugreporting.censors
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidEmail
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidName
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidPhoneNumber
 import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
@@ -31,19 +35,21 @@ class DiaryLocationCensor @Inject constructor(
         if (locationsNow.isEmpty()) return null
 
         val newMessage = locationsNow.fold(entry.message) { orig, location ->
-            var wip = orig.replace(location.locationName, "Location#${location.locationId}/Name")
+            var wip = orig
 
-            location.emailAddress?.let {
+            withValidName(location.locationName) {
+                wip = wip.replace(it, "Location#${location.locationId}/Name")
+            }
+            withValidEmail(location.emailAddress) {
                 wip = wip.replace(it, "Location#${location.locationId}/EMail")
             }
-
-            location.phoneNumber?.let {
+            withValidPhoneNumber(location.phoneNumber) {
                 wip = wip.replace(it, "Location#${location.locationId}/PhoneNumber")
             }
 
             wip
         }
 
-        return entry.copy(message = newMessage)
+        return entry.toNewLogLineIfDifferent(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
index 6f56e4bfe..42341a8b5 100644
--- 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
@@ -1,6 +1,10 @@
 package de.rki.coronawarnapp.bugreporting.censors
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidEmail
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidName
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.withValidPhoneNumber
 import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
@@ -31,19 +35,21 @@ class DiaryPersonCensor @Inject constructor(
         if (personsNow.isEmpty()) return null
 
         val newMessage = personsNow.fold(entry.message) { orig, person ->
-            var wip = orig.replace(person.fullName, "Person#${person.personId}/Name")
+            var wip = orig
 
-            person.emailAddress?.let {
+            withValidName(person.fullName) {
+                wip = wip.replace(it, "Person#${person.personId}/Name")
+            }
+            withValidEmail(person.emailAddress) {
                 wip = wip.replace(it, "Person#${person.personId}/EMail")
             }
-
-            person.phoneNumber?.let {
+            withValidPhoneNumber(person.phoneNumber) {
                 wip = wip.replace(it, "Person#${person.personId}/PhoneNumber")
             }
 
             wip
         }
 
-        return entry.copy(message = newMessage)
+        return entry.toNewLogLineIfDifferent(newMessage)
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt
index 266eff75e..ff0c63cf1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.bugreporting.censors
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
 import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
@@ -31,11 +32,15 @@ class DiaryVisitCensor @Inject constructor(
         if (visitsNow.isEmpty()) return null
 
         val newMessage = visitsNow.fold(entry.message) { orig, visit ->
-            if (visit.circumstances.isNullOrBlank()) return@fold orig
+            var wip = orig
 
-            orig.replace(visit.circumstances!!, "Visit#${visit.id}/Circumstances")
+            BugCensor.withValidComment(visit.circumstances) {
+                wip = wip.replace(it, "Visit#${visit.id}/Circumstances")
+            }
+
+            wip
         }
 
-        return entry.copy(message = newMessage)
+        return entry.toNewLogLineIfDifferent(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
index 8b23a7004..b85888d1d 100644
--- 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
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.bugreporting.censors
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
 import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.util.CWADebug
 import javax.inject.Inject
@@ -19,7 +20,7 @@ class QRCodeCensor @Inject constructor() : BugCensor {
             entry.message.replace(guid, PLACEHOLDER + guid.takeLast(4))
         }
 
-        return entry.copy(message = newMessage)
+        return entry.toNewLogLineIfDifferent(newMessage)
     }
 
     companion object {
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 843f67ff1..6d1d15f60 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
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.bugreporting.censors
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
 import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.util.CWADebug
@@ -18,7 +19,7 @@ class RegistrationTokenCensor @Inject constructor() : BugCensor {
             entry.message.replace(token, PLACEHOLDER + token.takeLast(4))
         }
 
-        return entry.copy(message = newMessage)
+        return entry.toNewLogLineIfDifferent(newMessage)
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/BugCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/BugCensorTest.kt
new file mode 100644
index 000000000..c0680a991
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/BugCensorTest.kt
@@ -0,0 +1,69 @@
+package de.rki.coronawarnapp.bugreporting.censors
+
+import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent
+import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class BugCensorTest : BaseTest() {
+
+    @Test
+    fun `name censoring validity`() {
+        BugCensor.withValidName(null) {} shouldBe false
+        BugCensor.withValidName("") {} shouldBe false
+        BugCensor.withValidName("  ") {} shouldBe false
+        BugCensor.withValidName("       ") {} shouldBe false
+        BugCensor.withValidName("J") {} shouldBe false
+        BugCensor.withValidName("Jo") {} shouldBe false
+        BugCensor.withValidName("Joe") {} shouldBe true
+    }
+
+    @Test
+    fun `email censoring validity`() {
+        BugCensor.withValidEmail(null) {} shouldBe false
+        BugCensor.withValidEmail("") {} shouldBe false
+        BugCensor.withValidEmail("     ") {} shouldBe false
+        BugCensor.withValidEmail("      ") {} shouldBe false
+        BugCensor.withValidEmail("@") {} shouldBe false
+        BugCensor.withValidEmail("@.") {} shouldBe false
+        BugCensor.withValidEmail("@.de") {} shouldBe false
+        BugCensor.withValidEmail("a@.de") {} shouldBe false
+        BugCensor.withValidEmail("a@b.de") {} shouldBe true
+    }
+
+    @Test
+    fun `phone censoring validity`() {
+        BugCensor.withValidPhoneNumber(null) {} shouldBe false
+        BugCensor.withValidPhoneNumber("    ") {} shouldBe false
+        BugCensor.withValidPhoneNumber("        ") {} shouldBe false
+        BugCensor.withValidPhoneNumber("0") {} shouldBe false
+        BugCensor.withValidPhoneNumber("01") {} shouldBe false
+        BugCensor.withValidPhoneNumber("012") {} shouldBe false
+        BugCensor.withValidPhoneNumber("0123") {} shouldBe true
+    }
+
+    @Test
+    fun `comment censoring validity`() {
+        BugCensor.withValidComment(null) {} shouldBe false
+        BugCensor.withValidComment("   ") {} shouldBe false
+        BugCensor.withValidComment("        ") {} shouldBe false
+        BugCensor.withValidComment("a") {} shouldBe false
+        BugCensor.withValidComment("ab") {} shouldBe false
+        BugCensor.withValidComment("abc") {} shouldBe true
+    }
+
+    @Test
+    fun `loglines are only copied if the message is different`() {
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Message",
+            tag = "Tag",
+            throwable = null
+        )
+        logLine.toNewLogLineIfDifferent("Message") shouldBe null
+        logLine.toNewLogLineIfDifferent("Message ") shouldNotBe logLine
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt
index 9ebaa5866..55c64ca41 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt
@@ -10,10 +10,12 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.mockk
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import kotlin.concurrent.thread
 
 class DiaryEncounterCensorTest : BaseTest() {
 
@@ -98,4 +100,62 @@ class DiaryEncounterCensorTest : BaseTest() {
         )
         instance.checkLog(notCensored) shouldBe null
     }
+
+    @Test
+    fun `censoring returns null if the message did not change`() = runBlockingTest {
+        every { diaryRepo.personEncounters } returns flowOf(
+            listOf(
+                mockEncounter(1, _circumstances = "March weather"),
+                mockEncounter(2, _circumstances = "Rainy, cold"),
+            )
+        )
+
+        val instance = createInstance(this)
+        val notCensored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "I like turtles",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(notCensored) shouldBe null
+    }
+
+    // EXPOSUREAPP-5670 / EXPOSUREAPP-5691
+    @Test
+    fun `replacement doesn't cause recursion`() {
+        every { diaryRepo.personEncounters } returns flowOf(
+            listOf(
+                mockEncounter(1, _circumstances = "March weather"),
+                mockEncounter(2, _circumstances = "Rainy, cold"),
+            )
+        )
+
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Lorem ipsum",
+            tag = "I'm a tag",
+            throwable = null
+        )
+
+        var isFinished = false
+
+        thread {
+            Thread.sleep(500)
+            if (isFinished) return@thread
+            Runtime.getRuntime().exit(1)
+        }
+
+        runBlocking {
+            val instance = createInstance(this)
+
+            val processedLine = try {
+                instance.checkLog(logLine)
+            } finally {
+                isFinished = true
+            }
+            processedLine shouldBe null
+        }
+    }
 }
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
index eaa554089..47c8eae21 100644
--- 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
@@ -10,10 +10,12 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.mockk
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import kotlin.concurrent.thread
 
 class DiaryLocationCensorTest : BaseTest() {
 
@@ -84,4 +86,63 @@ class DiaryLocationCensorTest : BaseTest() {
         )
         instance.checkLog(notCensored) shouldBe null
     }
+
+    @Test
+    fun `if message is the same, don't copy the log line`() = runBlockingTest {
+        every { diaryRepo.locations } returns flowOf(
+            listOf(
+                mockLocation(1, "Test", phone = null, mail = null),
+                mockLocation(2, "Test", phone = null, mail = null),
+                mockLocation(3, "Test", phone = null, mail = null)
+            )
+        )
+        val instance = createInstance(this)
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Lorem ipsum",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(logLine) shouldBe null
+    }
+
+    // EXPOSUREAPP-5670 / EXPOSUREAPP-5691
+    @Test
+    fun `replacement doesn't cause recursion`() {
+        every { diaryRepo.locations } returns flowOf(
+            listOf(
+                mockLocation(1, "Test", phone = null, mail = null),
+                mockLocation(2, "Test", phone = null, mail = null),
+                mockLocation(3, "Test", phone = null, mail = null)
+            )
+        )
+
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Lorem ipsum",
+            tag = "I'm a tag",
+            throwable = null
+        )
+
+        var isFinished = false
+
+        thread {
+            Thread.sleep(500)
+            if (isFinished) return@thread
+            Runtime.getRuntime().exit(1)
+        }
+
+        runBlocking {
+            val instance = createInstance(this)
+
+            val processedLine = try {
+                instance.checkLog(logLine)
+            } finally {
+                isFinished = true
+            }
+            processedLine 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
index 75df6df37..163faca82 100644
--- 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
@@ -10,10 +10,13 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.mockk
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import java.lang.Thread.sleep
+import kotlin.concurrent.thread
 
 class DiaryPersonCensorTest : BaseTest() {
 
@@ -84,4 +87,63 @@ class DiaryPersonCensorTest : BaseTest() {
         )
         instance.checkLog(notCensored) shouldBe null
     }
+
+    @Test
+    fun `if message is the same, don't copy the log line`() = runBlockingTest {
+        every { diaryRepo.people } returns flowOf(
+            listOf(
+                mockPerson(1, "Test", phone = null, mail = null),
+                mockPerson(2, "Test", phone = null, mail = null),
+                mockPerson(3, "Test", phone = null, mail = null)
+            )
+        )
+        val instance = createInstance(this)
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Lorem ipsum",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(logLine) shouldBe null
+    }
+
+    // EXPOSUREAPP-5670 / EXPOSUREAPP-5691
+    @Test
+    fun `replacement doesn't cause recursion`() {
+        every { diaryRepo.people } returns flowOf(
+            listOf(
+                mockPerson(1, "Test", phone = "", mail = ""),
+                mockPerson(2, "Test", phone = "", mail = ""),
+                mockPerson(3, "Test", phone = "", mail = "")
+            )
+        )
+
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Lorem ipsum",
+            tag = "I'm a tag",
+            throwable = null
+        )
+
+        var isFinished = false
+
+        thread {
+            sleep(500)
+            if (isFinished) return@thread
+            Runtime.getRuntime().exit(1)
+        }
+
+        runBlocking {
+            val instance = createInstance(this)
+
+            val processedLine = try {
+                instance.checkLog(logLine)
+            } finally {
+                isFinished = true
+            }
+            processedLine shouldBe null
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt
index 5f9d6b541..6af281b9a 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt
@@ -10,10 +10,12 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.mockk
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import kotlin.concurrent.thread
 
 class DiaryVisitCensorTest : BaseTest() {
 
@@ -94,4 +96,61 @@ class DiaryVisitCensorTest : BaseTest() {
         )
         instance.checkLog(notCensored) shouldBe null
     }
+
+    @Test
+    fun `censoring returns null if the message didn't change`() = runBlockingTest {
+        every { diaryRepo.locationVisits } returns flowOf(
+            listOf(
+                mockVisit(1, _circumstances = "Coffee"),
+                mockVisit(2, _circumstances = "fuels the world."),
+            )
+        )
+        val instance = createInstance(this)
+        val notCensored = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Wakey wakey, eggs and bakey.",
+            tag = "I'm a tag",
+            throwable = null
+        )
+        instance.checkLog(notCensored) shouldBe null
+    }
+
+    // EXPOSUREAPP-5670 / EXPOSUREAPP-5691
+    @Test
+    fun `replacement doesn't cause recursion`() {
+        every { diaryRepo.locationVisits } returns flowOf(
+            listOf(
+                mockVisit(1, _circumstances = "Coffee"),
+                mockVisit(2, _circumstances = "fuels the world."),
+            )
+        )
+
+        val logLine = LogLine(
+            timestamp = 1,
+            priority = 3,
+            message = "Lorem ipsum",
+            tag = "I'm a tag",
+            throwable = null
+        )
+
+        var isFinished = false
+
+        thread {
+            Thread.sleep(500)
+            if (isFinished) return@thread
+            Runtime.getRuntime().exit(1)
+        }
+
+        runBlocking {
+            val instance = createInstance(this)
+
+            val processedLine = try {
+                instance.checkLog(logLine)
+            } finally {
+                isFinished = true
+            }
+            processedLine shouldBe null
+        }
+    }
 }
-- 
GitLab