- [x] Dateinamen-Tests vorhanden
- [x] Fortsetzen-Tests vorhanden
- [x] Blockstatus-Tests vorhanden
- [x] Repository-Tests vorhanden
This commit is contained in:
2026-10-08 12:15:31 +02:00
parent e6bc99d144
commit dcfa11bef9
9 changed files with 2143 additions and 0 deletions
@@ -0,0 +1,151 @@
package de.medapp.wunddoku.domain.usecase
import de.medapp.wunddoku.domain.model.CheckItem
import de.medapp.wunddoku.domain.model.DocumentationBlock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Blockstatus-Tests: Wann gilt ein Dokumentationsblock als "bearbeitet"?
* Diese Regel steuert Fortschrittsanzeige UND PDF-Export und muss deshalb exakt stimmen.
*/
class BlockEditedPolicyTest {
private var idCounter = 0
private fun item(
checked: Boolean,
isFreeText: Boolean = false,
freeTextValue: String = "",
isStaticHint: Boolean = false,
inputSpec: String = "",
): CheckItem {
idCounter++
return CheckItem(
checkItemStateId = "state-$idCounter",
definitionId = "def-$idCounter",
label = "Eintrag $idCounter",
order = idCounter,
checked = checked,
isFreeText = isFreeText,
freeTextValue = freeTextValue,
isStaticHint = isStaticHint,
inputSpec = inputSpec,
)
}
private fun block(vararg items: CheckItem) = DocumentationBlock(
blockStateId = "block-state",
definitionId = "block-def",
title = "Testblock",
order = 1,
items = items.toList(),
)
// --- nicht bearbeitet ----------------------------------------------------------------
@Test
fun block_ohne_eintraege_ist_nicht_bearbeitet() {
assertFalse(BlockEditedPolicy.isEdited(block()))
}
@Test
fun block_ohne_angehakten_eintrag_ist_nicht_bearbeitet() {
val b = block(item(checked = false), item(checked = false), item(checked = false, isFreeText = true))
assertFalse(BlockEditedPolicy.isEdited(b))
}
@Test
fun nicht_angehakter_freitext_mit_text_zaehlt_nicht_als_bearbeitung() {
// Das Freitextfeld ist in der UI gesperrt, solange die Checkbox nicht angehakt ist.
val b = block(item(checked = false, isFreeText = true, freeTextValue = "Text ohne Haken"))
assertFalse(BlockEditedPolicy.isEdited(b))
}
@Test
fun nur_hinweistexte_machen_einen_block_nicht_bearbeitet() {
// Hinweistexte sind technisch immer checked = true, aber keine Nutzereingabe.
val b = block(
item(checked = true, isStaticHint = true),
item(checked = true, isStaticHint = true),
)
assertFalse(BlockEditedPolicy.isEdited(b))
}
@Test
fun hinweistexte_plus_nicht_angehakte_eintraege_sind_nicht_bearbeitet() {
val b = block(
item(checked = true, isStaticHint = true),
item(checked = false),
item(checked = false, isFreeText = true),
)
assertFalse(BlockEditedPolicy.isEdited(b))
}
// --- bearbeitet ----------------------------------------------------------------------
@Test
fun ein_angehakter_normaler_eintrag_macht_den_block_bearbeitet() {
assertTrue(BlockEditedPolicy.isEdited(block(item(checked = true))))
}
@Test
fun angehakter_freitext_zaehlt_auch_ohne_eingegebenen_text() {
val b = block(item(checked = true, isFreeText = true, freeTextValue = ""))
assertTrue(BlockEditedPolicy.isEdited(b))
}
@Test
fun angehakter_freitext_mit_text_ist_bearbeitet() {
val b = block(item(checked = true, isFreeText = true, freeTextValue = "Rötung am Wundrand"))
assertTrue(BlockEditedPolicy.isEdited(b))
}
@Test
fun angehakter_eintrag_mit_strukturierter_eingabe_ist_bearbeitet() {
val b = block(item(checked = true, freeTextValue = "3", inputSpec = "spec"))
assertTrue(BlockEditedPolicy.isEdited(b))
}
@Test
fun hinweistext_wird_ignoriert_sobald_ein_echter_eintrag_angehakt_ist() {
val b = block(
item(checked = true, isStaticHint = true),
item(checked = true),
)
assertTrue(BlockEditedPolicy.isEdited(b))
}
@Test
fun mehrere_angehakte_eintraege_sind_bearbeitet() {
val b = block(item(checked = true), item(checked = true), item(checked = false))
assertTrue(BlockEditedPolicy.isEdited(b))
}
// --- Zusammenspiel mit DocumentationBlock.isEdited ------------------------------------
@Test
fun documentation_block_isEdited_delegiert_an_die_policy() {
val edited = block(item(checked = true))
val untouched = block(item(checked = false), item(checked = true, isStaticHint = true))
assertEquals(BlockEditedPolicy.isEdited(edited), edited.isEdited)
assertEquals(BlockEditedPolicy.isEdited(untouched), untouched.isEdited)
assertTrue(edited.isEdited)
assertFalse(untouched.isEdited)
}
@Test
fun isEdited_wird_aus_dem_aktuellen_inhalt_abgeleitet_und_nicht_gespeichert() {
val untouched = block(item(checked = false))
assertFalse(untouched.isEdited)
val nowChecked = untouched.copy(items = untouched.items.map { it.copy(checked = true) })
assertTrue(nowChecked.isEdited)
val uncheckedAgain = nowChecked.copy(items = nowChecked.items.map { it.copy(checked = false) })
assertFalse(uncheckedAgain.isEdited)
}
}
@@ -0,0 +1,245 @@
package de.medapp.wunddoku.pdf
import de.medapp.wunddoku.domain.model.CheckItem
import de.medapp.wunddoku.domain.model.Documentation
import de.medapp.wunddoku.domain.model.DocumentationBlock
import de.medapp.wunddoku.domain.model.DocumentationStatus
import de.medapp.wunddoku.domain.model.PatientStammdatenSnapshot
import de.medapp.wunddoku.domain.model.Photo
import de.medapp.wunddoku.domain.model.Signature
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
/**
* Blockstatus im PDF-Export: Nur bearbeitete Blöcke und nur angehakte Optionen gelangen ins PDF,
* alles wird aus den historischen Snapshots gebildet und stabil sortiert.
*/
class DocumentationToPdfExportModelMapperTest {
private val snapshot = PatientStammdatenSnapshot(
firstName = "Max",
lastName = "Mustermann",
birthDate = LocalDate.of(1980, 3, 14),
street = "Musterstraße 1",
postalCode = "12345",
city = "Musterstadt",
)
// Mittag in der Systemzeitzone -> das lokale Datum ist zeitzonenunabhängig der 23.08.2026.
private val createdAt: Instant =
LocalDate.of(2026, 8, 23).atTime(12, 0).atZone(ZoneId.systemDefault()).toInstant()
private fun item(
id: String,
order: Int,
checked: Boolean,
label: String = "Label $id",
isFreeText: Boolean = false,
freeTextValue: String = "",
isStaticHint: Boolean = false,
inputSpec: String = "",
) = CheckItem(
checkItemStateId = "state-$id",
definitionId = id,
label = label,
order = order,
checked = checked,
isFreeText = isFreeText,
freeTextValue = freeTextValue,
isStaticHint = isStaticHint,
inputSpec = inputSpec,
)
private fun block(id: String, order: Int, title: String = "Titel $id", items: List<CheckItem>) =
DocumentationBlock("state-$id", id, title, order, items)
private fun documentation(
blocks: List<DocumentationBlock>,
photos: List<Photo> = emptyList(),
signatures: List<Signature> = emptyList(),
) = Documentation(
id = "doc-1",
patientId = "patient-1",
patientSnapshot = snapshot,
status = DocumentationStatus.IN_PROGRESS,
createdAt = createdAt,
updatedAt = createdAt,
continuedFromDocumentationId = null,
catalogVersion = 4,
blocks = blocks,
photos = photos,
signatures = signatures,
)
// --- Blockfilter ---------------------------------------------------------------------
@Test
fun nur_bearbeitete_bloecke_werden_exportiert() {
val doc = documentation(
listOf(
block("a", 1, items = listOf(item("a1", 1, checked = true))),
block("b", 2, items = listOf(item("b1", 1, checked = false))),
block("c", 3, items = listOf(item("c1", 1, checked = true))),
)
)
val model = DocumentationToPdfExportModelMapper.map(doc)
assertEquals(listOf("Titel a", "Titel c"), model.blocks.map { it.title })
}
@Test
fun block_mit_nur_hinweistexten_wird_nicht_exportiert() {
val doc = documentation(
listOf(
block(
"hint", 1,
items = listOf(
item("h1", 1, checked = true, isStaticHint = true),
item("h2", 2, checked = false),
),
),
)
)
assertTrue(DocumentationToPdfExportModelMapper.map(doc).blocks.isEmpty())
}
@Test
fun dokumentation_ohne_bearbeiteten_block_ergibt_leere_blockliste() {
val doc = documentation(listOf(block("a", 1, items = listOf(item("a1", 1, checked = false)))))
assertTrue(DocumentationToPdfExportModelMapper.map(doc).blocks.isEmpty())
}
// --- Itemfilter ----------------------------------------------------------------------
@Test
fun nur_angehakte_optionen_erscheinen_im_pdf() {
val doc = documentation(
listOf(
block(
"a", 1,
items = listOf(
item("a1", 1, checked = true, label = "Ja"),
item("a2", 2, checked = false, label = "Nein"),
item("a3", 3, checked = true, label = "Auch ja"),
),
),
)
)
val items = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items
assertEquals(listOf("Ja", "Auch ja"), items.map { it.label })
}
@Test
fun hinweistexte_eines_bearbeiteten_blocks_bleiben_im_pdf_erhalten() {
val doc = documentation(
listOf(
block(
"a", 1,
items = listOf(
item("hint", 1, checked = true, label = "Reinigen mit:", isStaticHint = true),
item("a2", 2, checked = true, label = "Ringer-Lösung"),
),
),
)
)
val items = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items
assertEquals(listOf("Reinigen mit:", "Ringer-Lösung"), items.map { it.label })
}
@Test
fun freitext_und_strukturierte_eingabe_werden_unveraendert_durchgereicht() {
val doc = documentation(
listOf(
block(
"a", 1,
items = listOf(
item("f", 1, checked = true, label = "Freitext", isFreeText = true, freeTextValue = "Rötung"),
item("s", 2, checked = true, label = "Maße", freeTextValue = "3|4", inputSpec = "SPEC"),
),
),
)
)
val items = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items
assertEquals(PdfExportItem("Freitext", isFreeText = true, freeTextValue = "Rötung", inputSpec = ""), items[0])
assertEquals(PdfExportItem("Maße", isFreeText = false, freeTextValue = "3|4", inputSpec = "SPEC"), items[1])
}
// --- Sortierung ----------------------------------------------------------------------
@Test
fun bloecke_und_optionen_werden_nach_order_sortiert() {
val doc = documentation(
listOf(
block("zweiter", 2, items = listOf(item("z1", 1, checked = true))),
block(
"erster", 1,
items = listOf(
item("e2", 2, checked = true, label = "B"),
item("e1", 1, checked = true, label = "A"),
),
),
)
)
val blocks = DocumentationToPdfExportModelMapper.map(doc).blocks
assertEquals(listOf("Titel erster", "Titel zweiter"), blocks.map { it.title })
assertEquals(listOf("A", "B"), blocks.first().items.map { it.label })
}
@Test
fun fotos_und_signaturen_werden_nach_orderIndex_sortiert() {
val photos = listOf(
Photo("p2", "doc-1", "zweites.jpg", 1, createdAt),
Photo("p1", "doc-1", "erstes.jpg", 0, createdAt),
)
val signatures = listOf(
Signature("s2", "doc-1", "Berta", "b.png", 1, createdAt),
Signature("s1", "doc-1", "Anna", "a.png", 0, createdAt),
)
val model = DocumentationToPdfExportModelMapper.map(documentation(emptyList(), photos, signatures))
assertEquals(listOf("erstes.jpg", "zweites.jpg"), model.photos.map { it.fileName })
assertEquals(listOf("Anna", "Berta"), model.signatures.map { it.signerName })
assertEquals(listOf("a.png", "b.png"), model.signatures.map { it.fileName })
}
@Test
fun alle_signaturen_werden_exportiert_ohne_obergrenze() {
val signatures = (0 until 5).map { Signature("s$it", "doc-1", "Person $it", "s$it.png", it, createdAt) }
val model = DocumentationToPdfExportModelMapper.map(documentation(emptyList(), signatures = signatures))
assertEquals(5, model.signatures.size)
}
// --- Kopfdaten (Snapshot) ------------------------------------------------------------
@Test
fun kopfdaten_stammen_aus_dem_patienten_snapshot_der_dokumentation() {
val model = DocumentationToPdfExportModelMapper.map(documentation(emptyList()))
assertEquals("doc-1", model.documentationId)
assertEquals("Max Mustermann", model.patientFullName)
assertEquals(LocalDate.of(1980, 3, 14), model.patientBirthDate)
assertEquals("Musterstraße 1, 12345 Musterstadt", model.patientDisplayAddress)
assertEquals(LocalDate.of(2026, 8, 23), model.createdAt)
}
@Test
fun dateiname_folgt_dem_schema_und_nutzt_snapshot_und_erstelldatum() {
val model = DocumentationToPdfExportModelMapper.map(documentation(emptyList()))
assertEquals("2026-08-23_Max_Mustermann_1980-03-14.pdf", model.fileName)
}
}
@@ -0,0 +1,175 @@
package de.medapp.wunddoku.pdf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate
/**
* Reine JVM-Tests (keine Android-Abhängigkeit) für das Dateinamensschema
* `Erstelldatum_Vorname_Nachname_Geburtsdatum.pdf`.
*/
class PdfFileNameGeneratorTest {
private val created = LocalDate.of(2026, 8, 23)
private val birth = LocalDate.of(1980, 3, 14)
private fun name(
first: String,
last: String,
createdAt: LocalDate = created,
birthDate: LocalDate = birth,
): String = PdfFileNameGenerator.generate(createdAt, first, last, birthDate)
// --- Grundschema ---------------------------------------------------------------------
@Test
fun standardfall_folgt_dem_vereinbarten_schema() {
assertEquals("2026-08-23_Max_Mustermann_1980-03-14.pdf", name("Max", "Mustermann"))
}
@Test
fun datumsteile_werden_mit_fuehrenden_nullen_im_iso_format_ausgegeben() {
val result = name("Max", "Mustermann", createdAt = LocalDate.of(2026, 1, 5), birthDate = LocalDate.of(1999, 2, 3))
assertEquals("2026-01-05_Max_Mustermann_1999-02-03.pdf", result)
}
@Test
fun schaltjahr_geburtsdatum_wird_korrekt_ausgegeben() {
val result = name("Max", "Mustermann", birthDate = LocalDate.of(2000, 2, 29))
assertEquals("2026-08-23_Max_Mustermann_2000-02-29.pdf", result)
}
@Test
fun ergebnis_ist_deterministisch() {
assertEquals(name("Max", "Mustermann"), name("Max", "Mustermann"))
}
@Test
fun verschiedene_erstelldaten_fuehren_zu_verschiedenen_dateinamen() {
val a = name("Max", "Mustermann", createdAt = LocalDate.of(2026, 8, 23))
val b = name("Max", "Mustermann", createdAt = LocalDate.of(2026, 8, 24))
assertNotEquals(a, b)
}
// --- Umlaute / Diakritika ------------------------------------------------------------
@Test
fun deutsche_umlaute_werden_transliteriert_statt_entfernt() {
assertEquals("2026-08-23_Max_Mueller_1980-03-14.pdf", name("Max", "Müller"))
assertEquals("2026-08-23_Aenne_Oezdemir_1980-03-14.pdf", name("Änne", "Özdemir"))
assertEquals("2026-08-23_Uwe_Ueberall_1980-03-14.pdf", name("Uwe", "Überall"))
}
@Test
fun eszett_wird_zu_doppel_s() {
assertEquals("2026-08-23_Max_Weiss_1980-03-14.pdf", name("Max", "Weiß"))
}
@Test
fun mueller_und_mller_bleiben_unterscheidbar() {
assertNotEquals(name("Max", "Müller"), name("Max", "Mller"))
}
@Test
fun andere_diakritika_werden_entfernt() {
assertEquals("2026-08-23_Jose_Celik_1980-03-14.pdf", name("José", "Çelik"))
}
// --- Leerzeichen / Sonderzeichen -----------------------------------------------------
@Test
fun leerzeichen_werden_zu_unterstrichen() {
assertEquals("2026-08-23_Anna_Maria_Mustermann_1980-03-14.pdf", name("Anna Maria", "Mustermann"))
}
@Test
fun mehrfaches_whitespace_wird_zu_einem_unterstrich_zusammengefasst() {
assertEquals("2026-08-23_Anna_Maria_Mustermann_1980-03-14.pdf", name("Anna Maria", "Mustermann"))
assertEquals("2026-08-23_Anna_Maria_Mustermann_1980-03-14.pdf", name("Anna\t\nMaria", "Mustermann"))
}
@Test
fun mehrfache_unterstriche_werden_zusammengefasst() {
assertEquals("2026-08-23_Max_Muster_mann_1980-03-14.pdf", name("Max", "Muster__mann"))
}
@Test
fun fuehrende_und_nachgestellte_trennzeichen_werden_entfernt() {
val expected = "2026-08-23_Max_Mustermann_1980-03-14.pdf"
assertEquals(expected, name(" Max ", "Mustermann"))
assertEquals(expected, name("_Max_", "Mustermann"))
assertEquals(expected, name("-Max-", "Mustermann"))
}
@Test
fun bindestrich_im_doppelnamen_bleibt_erhalten() {
assertEquals("2026-08-23_Aenne-Marie_Meyer-Luedenscheidt_1980-03-14.pdf", name("Änne-Marie", "Meyer-Lüdenscheidt"))
}
@Test
fun apostroph_und_punkt_werden_entfernt() {
assertEquals("2026-08-23_Sean_OBrien_1980-03-14.pdf", name("Sean", "O'Brien"))
assertEquals("2026-08-23_Max_Dr_Mustermann_1980-03-14.pdf", name("Max", "Dr. Mustermann"))
}
// --- Sicherheit (Pfad-/Dateisystemzeichen) --------------------------------------------
@Test
fun pfadseparatoren_und_punkte_koennen_keinen_pfad_erzeugen() {
assertEquals(
"2026-08-23_Unbekannt_etcpasswd_1980-03-14.pdf",
name("../..", "../etc/passwd"),
)
}
@Test
fun fuer_dateinamen_ungueltige_zeichen_werden_entfernt() {
assertEquals(
"2026-08-23_MaxMuster_Mustermann_1980-03-14.pdf",
name("Max:Mus*ter?", "Mustermann"),
)
assertEquals(
"2026-08-23_Max_Mustermann_1980-03-14.pdf",
name("Ma<x>|\"", "Muster\\mann"),
)
}
@Test
fun emojis_werden_entfernt() {
assertEquals("2026-08-23_Max_Mustermann_1980-03-14.pdf", name("Max😀", "Mustermann"))
}
@Test
fun dateiname_besteht_immer_nur_aus_sicheren_zeichen_und_endet_auf_pdf() {
val inputs = listOf(
"Max" to "Mustermann",
"Änne-Marie" to "Özdemir Weiß",
"José" to "O'Brien",
"../../x" to "a\\b/c",
" " to "",
"😀" to "???",
"Ma<x>|\"" to "Muster\\mann",
)
val allowed = Regex("^[A-Za-z0-9_.-]+$")
for ((first, last) in inputs) {
val result = name(first, last)
assertTrue("Unsicherer Dateiname: $result", allowed.matches(result))
assertTrue("Falsche Endung: $result", result.endsWith(".pdf"))
}
}
// --- Platzhalter für leere Namen -----------------------------------------------------
@Test
fun leerer_oder_blanker_name_wird_zu_unbekannt() {
assertEquals("2026-08-23_Unbekannt_Mustermann_1980-03-14.pdf", name("", "Mustermann"))
assertEquals("2026-08-23_Max_Unbekannt_1980-03-14.pdf", name("Max", " "))
}
@Test
fun name_nur_aus_ungueltigen_zeichen_wird_zu_unbekannt() {
assertEquals("2026-08-23_Unbekannt_Unbekannt_1980-03-14.pdf", name("???", "***"))
}
}