QA-002 #16
@@ -0,0 +1,259 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.blockStateEntity
|
||||||
|
import de.medapp.wunddoku.testutil.checkItemStateEntity
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** DAO-Tests für [BlockStateDao] und [CheckItemStateDao]. */
|
||||||
|
class BlockAndCheckItemDaoTest : RoomTestBase() {
|
||||||
|
|
||||||
|
// --- BlockStateDao -------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockStates_insertAll_und_getById_liefern_alle_felder() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
val block = blockStateEntity(title = "Größe & Tiefe", order = 3, updatedAt = 4_000L)
|
||||||
|
|
||||||
|
blockStateDao.insertAll(listOf(block))
|
||||||
|
|
||||||
|
assertEquals(block, blockStateDao.getById("b-1"))
|
||||||
|
assertNull(blockStateDao.getById("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockStates_getForDocumentation_sortiert_nach_blockOrder_und_filtert_nach_dokumentation() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
blockStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
blockStateEntity(id = "b-3", documentationId = "d-1", blockDefinitionId = "c", order = 3),
|
||||||
|
blockStateEntity(id = "b-1", documentationId = "d-1", blockDefinitionId = "a", order = 1),
|
||||||
|
blockStateEntity(id = "b-2", documentationId = "d-1", blockDefinitionId = "b", order = 2),
|
||||||
|
blockStateEntity(id = "b-x", documentationId = "d-2", blockDefinitionId = "a", order = 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("b-1", "b-2", "b-3"), blockStateDao.getForDocumentation("d-1").map { it.id })
|
||||||
|
assertEquals(listOf("b-x"), blockStateDao.getForDocumentation("d-2").map { it.id })
|
||||||
|
assertTrue(blockStateDao.getForDocumentation("unbekannt").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockStates_gleiche_blockdefinition_in_derselben_dokumentation_ist_verboten() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity(id = "b-1", blockDefinitionId = "wundart")))
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity(id = "b-2", blockDefinitionId = "wundart")))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, countRows("block_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockStates_gleiche_blockdefinition_in_verschiedenen_dokumentationen_ist_erlaubt() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
|
||||||
|
blockStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
blockStateEntity(id = "b-1", documentationId = "d-1", blockDefinitionId = "wundart"),
|
||||||
|
blockStateEntity(id = "b-2", documentationId = "d-2", blockDefinitionId = "wundart"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(2, countRows("block_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockStates_mit_unbekannter_dokumentation_wirft() = runDbTest {
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity(documentationId = "gibt-es-nicht")))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, countRows("block_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockStates_insertAll_mit_leerer_liste_ist_unkritisch() = runDbTest {
|
||||||
|
blockStateDao.insertAll(emptyList())
|
||||||
|
|
||||||
|
assertEquals(0, countRows("block_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CheckItemStateDao ---------------------------------------------------------------
|
||||||
|
|
||||||
|
private suspend fun seedBlock(id: String = "b-1", documentationId: String = "d-1", definitionId: String = "wundart") {
|
||||||
|
if (documentationDao.getById(documentationId) == null) seedDocumentation(id = documentationId)
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity(id = id, documentationId = documentationId, blockDefinitionId = definitionId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_insertAll_und_getForBlockState_liefern_alle_felder_inklusive_standardwerten() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
val plain = checkItemStateEntity(id = "c-1", definitionId = "plain", order = 1)
|
||||||
|
val full = checkItemStateEntity(
|
||||||
|
id = "c-2", definitionId = "full", order = 2, checked = true, isFreeText = true,
|
||||||
|
freeTextValue = "Text", isStaticHint = false, inputSpec = "SPEC",
|
||||||
|
)
|
||||||
|
|
||||||
|
checkItemStateDao.insertAll(listOf(plain, full))
|
||||||
|
|
||||||
|
val loaded = checkItemStateDao.getForBlockState("b-1")
|
||||||
|
assertEquals(listOf(plain, full), loaded)
|
||||||
|
assertFalse(loaded[0].checked)
|
||||||
|
assertFalse(loaded[0].isFreeText)
|
||||||
|
assertEquals("", loaded[0].freeTextValue)
|
||||||
|
assertFalse(loaded[0].isStaticHint)
|
||||||
|
assertEquals("", loaded[0].inputSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_getForBlockState_sortiert_nach_itemOrder_und_filtert_nach_block() = runDbTest {
|
||||||
|
seedBlock(id = "b-1", definitionId = "a")
|
||||||
|
seedBlock(id = "b-2", definitionId = "b")
|
||||||
|
checkItemStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
checkItemStateEntity(id = "c-3", blockStateId = "b-1", definitionId = "z", order = 3),
|
||||||
|
checkItemStateEntity(id = "c-1", blockStateId = "b-1", definitionId = "x", order = 1),
|
||||||
|
checkItemStateEntity(id = "c-2", blockStateId = "b-1", definitionId = "y", order = 2),
|
||||||
|
checkItemStateEntity(id = "c-x", blockStateId = "b-2", definitionId = "x", order = 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("c-1", "c-2", "c-3"), checkItemStateDao.getForBlockState("b-1").map { it.id })
|
||||||
|
assertEquals(listOf("c-x"), checkItemStateDao.getForBlockState("b-2").map { it.id })
|
||||||
|
assertTrue(checkItemStateDao.getForBlockState("unbekannt").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_setChecked_setzt_und_entfernt_den_haken_ohne_andere_felder_zu_aendern() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
val item = checkItemStateEntity(freeTextValue = "bleibt", inputSpec = "bleibt auch")
|
||||||
|
checkItemStateDao.insertAll(listOf(item))
|
||||||
|
|
||||||
|
checkItemStateDao.setChecked("c-1", true)
|
||||||
|
assertEquals(item.copy(checked = true), checkItemStateDao.getForBlockState("b-1").single())
|
||||||
|
|
||||||
|
checkItemStateDao.setChecked("c-1", false)
|
||||||
|
assertEquals(item, checkItemStateDao.getForBlockState("b-1").single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_setChecked_betrifft_nur_die_angegebene_option() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
checkItemStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
checkItemStateEntity(id = "c-1", definitionId = "a", order = 1),
|
||||||
|
checkItemStateEntity(id = "c-2", definitionId = "b", order = 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
checkItemStateDao.setChecked("c-1", true)
|
||||||
|
|
||||||
|
val byId = checkItemStateDao.getForBlockState("b-1").associateBy { it.id }
|
||||||
|
assertTrue(byId.getValue("c-1").checked)
|
||||||
|
assertFalse(byId.getValue("c-2").checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_setChecked_und_updateFreeTextValue_mit_unbekannter_id_sind_wirkungslos() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
val item = checkItemStateEntity()
|
||||||
|
checkItemStateDao.insertAll(listOf(item))
|
||||||
|
|
||||||
|
checkItemStateDao.setChecked("unbekannt", true)
|
||||||
|
checkItemStateDao.updateFreeTextValue("unbekannt", "x")
|
||||||
|
|
||||||
|
assertEquals(listOf(item), checkItemStateDao.getForBlockState("b-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_updateFreeTextValue_aendert_nur_den_text_und_nicht_den_haken() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(isFreeText = true, checked = true)))
|
||||||
|
|
||||||
|
checkItemStateDao.updateFreeTextValue("c-1", "Rötung am Wundrand")
|
||||||
|
|
||||||
|
val loaded = checkItemStateDao.getForBlockState("b-1").single()
|
||||||
|
assertEquals("Rötung am Wundrand", loaded.freeTextValue)
|
||||||
|
assertTrue(loaded.checked)
|
||||||
|
assertTrue(loaded.isFreeText)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_freitext_laesst_sich_leeren() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(isFreeText = true, freeTextValue = "alt")))
|
||||||
|
|
||||||
|
checkItemStateDao.updateFreeTextValue("c-1", "")
|
||||||
|
|
||||||
|
assertEquals("", checkItemStateDao.getForBlockState("b-1").single().freeTextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_sonderzeichen_und_sql_fragmente_werden_als_reiner_text_gespeichert() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(isFreeText = true)))
|
||||||
|
val text = "O'Brien \"Test\" ; DROP TABLE patients; -- Größe 3,5 × 2 cm\nzweite Zeile\t😀 100% _x_"
|
||||||
|
|
||||||
|
checkItemStateDao.updateFreeTextValue("c-1", text)
|
||||||
|
|
||||||
|
assertEquals(text, checkItemStateDao.getForBlockState("b-1").single().freeTextValue)
|
||||||
|
assertEquals(1, countRows("patients")) // Tabelle existiert weiterhin, der Seed-Patient ist unangetastet
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_inputSpec_mit_steuerzeichen_der_codierung_bleibt_verlustfrei_erhalten() = runDbTest {
|
||||||
|
// InputSpecCodec nutzt \u001E, \u001F und \u001D als Trennzeichen.
|
||||||
|
seedBlock()
|
||||||
|
val spec = "tpl\u001EN\u001Flabel\u001F3\u001Dx\u001Ey"
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(inputSpec = spec)))
|
||||||
|
|
||||||
|
assertEquals(spec, checkItemStateDao.getForBlockState("b-1").single().inputSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_gleiche_optionsdefinition_im_selben_block_ist_verboten() = runDbTest {
|
||||||
|
seedBlock()
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(id = "c-1", definitionId = "dup")))
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(id = "c-2", definitionId = "dup")))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, countRows("check_item_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_gleiche_optionsdefinition_in_verschiedenen_bloecken_ist_erlaubt() = runDbTest {
|
||||||
|
seedBlock(id = "b-1", definitionId = "a")
|
||||||
|
seedBlock(id = "b-2", definitionId = "b")
|
||||||
|
|
||||||
|
checkItemStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
checkItemStateEntity(id = "c-1", blockStateId = "b-1", definitionId = "same"),
|
||||||
|
checkItemStateEntity(id = "c-2", blockStateId = "b-2", definitionId = "same"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(2, countRows("check_item_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkItems_mit_unbekanntem_block_wirft() = runDbTest {
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity(blockStateId = "gibt-es-nicht")))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, countRows("check_item_states"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationStatus
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.awaitMatching
|
||||||
|
import de.medapp.wunddoku.testutil.documentationEntity
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** DAO-Tests für [DocumentationDao] (ohne Suche und Relationen, s. SearchQueryTest / DocumentationRelationsTest). */
|
||||||
|
class DocumentationDaoTest : RoomTestBase() {
|
||||||
|
|
||||||
|
// --- insert / getById ----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert_und_getById_liefern_alle_felder_inklusive_snapshots() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
val entity = documentationEntity(
|
||||||
|
status = DocumentationStatus.COMPLETED,
|
||||||
|
createdAt = 1_000L,
|
||||||
|
updatedAt = 2_000L,
|
||||||
|
pdfLastGeneratedAt = 3_000L,
|
||||||
|
catalogVersion = 3,
|
||||||
|
firstNameSnapshot = "Erika",
|
||||||
|
lastNameSnapshot = "Musterfrau",
|
||||||
|
birthDateSnapshot = "1975-12-01",
|
||||||
|
streetSnapshot = "Hauptstraße 5",
|
||||||
|
postalCodeSnapshot = "54321",
|
||||||
|
citySnapshot = "Beispielstadt",
|
||||||
|
)
|
||||||
|
|
||||||
|
documentationDao.insert(entity)
|
||||||
|
|
||||||
|
assertEquals(entity, documentationDao.getById("d-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nullable_felder_bleiben_null() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
|
||||||
|
val loaded = documentationDao.getById("d-1")!!
|
||||||
|
|
||||||
|
assertNull(loaded.continuedFromDocumentationId)
|
||||||
|
assertNull(loaded.pdfLastGeneratedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getById_liefert_null_fuer_unbekannte_id() = runDbTest {
|
||||||
|
assertNull(documentationDao.getById("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun status_wird_als_enum_name_gespeichert_und_vollstaendig_zurueckgelesen() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
DocumentationStatus.values().forEachIndexed { index, status ->
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-$index", status = status))
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentationStatus.values().forEachIndexed { index, status ->
|
||||||
|
assertEquals(status, documentationDao.getById("d-$index")!!.status)
|
||||||
|
assertEquals(status.name, queryString("SELECT status FROM documentations WHERE id = 'd-$index'"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Foreign Keys --------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert_mit_unbekanntem_patienten_wirft() = runDbTest {
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
documentationDao.insert(documentationEntity(patientId = "gibt-es-nicht"))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, countRows("documentations"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert_mit_unbekannter_fortsetzungsquelle_wirft() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
documentationDao.insert(documentationEntity(continuedFromDocumentationId = "gibt-es-nicht"))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, countRows("documentations"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzungsquelle_wird_beim_loeschen_der_quelle_auf_null_gesetzt() = runDbTest {
|
||||||
|
val source = seedDocumentation(id = "d-source")
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-copy", continuedFromDocumentationId = "d-source"))
|
||||||
|
assertEquals("d-source", documentationDao.getById("d-copy")!!.continuedFromDocumentationId)
|
||||||
|
|
||||||
|
documentationDao.delete(source)
|
||||||
|
|
||||||
|
assertNull(documentationDao.getById("d-source"))
|
||||||
|
val copy = documentationDao.getById("d-copy")
|
||||||
|
assertNotNull("Die Fortsetzung darf nicht mitgelöscht werden", copy)
|
||||||
|
assertNull(copy!!.continuedFromDocumentationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- update / delete -----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun update_aendert_nur_die_uebergebene_dokumentation() = runDbTest {
|
||||||
|
val first = seedDocumentation(id = "d-1")
|
||||||
|
val second = seedDocumentation(id = "d-2")
|
||||||
|
|
||||||
|
documentationDao.update(first.copy(status = DocumentationStatus.COMPLETED, updatedAt = 9_000L, pdfLastGeneratedAt = 9_500L))
|
||||||
|
|
||||||
|
val changed = documentationDao.getById("d-1")!!
|
||||||
|
assertEquals(DocumentationStatus.COMPLETED, changed.status)
|
||||||
|
assertEquals(9_000L, changed.updatedAt)
|
||||||
|
assertEquals(9_500L, changed.pdfLastGeneratedAt)
|
||||||
|
assertEquals(second, documentationDao.getById("d-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun update_einer_unbekannten_id_legt_nichts_an() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
documentationDao.update(documentationEntity(id = "geist"))
|
||||||
|
|
||||||
|
assertNull(documentationDao.getById("geist"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_entfernt_nur_die_uebergebene_dokumentation_und_laesst_den_patienten_bestehen() = runDbTest {
|
||||||
|
val first = seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
|
||||||
|
documentationDao.delete(first)
|
||||||
|
|
||||||
|
assertNull(documentationDao.getById("d-1"))
|
||||||
|
assertNotNull(documentationDao.getById("d-2"))
|
||||||
|
assertNotNull(patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- observeAll / countForPatient ----------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAll_sortiert_nach_erstelldatum_absteigend() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-alt", createdAt = 1_000L)
|
||||||
|
seedDocumentation(id = "d-neu", createdAt = 3_000L)
|
||||||
|
seedDocumentation(id = "d-mitte", createdAt = 2_000L)
|
||||||
|
|
||||||
|
val ids = documentationDao.observeAll().first().map { it.id }
|
||||||
|
|
||||||
|
assertEquals(listOf("d-neu", "d-mitte", "d-alt"), ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAll_liefert_leere_liste_ohne_dokumentationen() = runDbTest {
|
||||||
|
assertTrue(documentationDao.observeAll().first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAll_aktualisiert_sich_bei_insert_update_und_delete() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
val afterInsert = async { documentationDao.observeAll().awaitMatching { it.size == 1 } }
|
||||||
|
val entity = documentationEntity(id = "d-1")
|
||||||
|
documentationDao.insert(entity)
|
||||||
|
assertEquals("d-1", afterInsert.await().single().id)
|
||||||
|
|
||||||
|
val afterUpdate = async {
|
||||||
|
documentationDao.observeAll().awaitMatching { list -> list.singleOrNull()?.status == DocumentationStatus.COMPLETED }
|
||||||
|
}
|
||||||
|
documentationDao.update(entity.copy(status = DocumentationStatus.COMPLETED))
|
||||||
|
afterUpdate.await()
|
||||||
|
|
||||||
|
val afterDelete = async { documentationDao.observeAll().awaitMatching { it.isEmpty() } }
|
||||||
|
documentationDao.delete(entity)
|
||||||
|
assertTrue(afterDelete.await().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun countForPatient_zaehlt_nur_dokumentationen_des_jeweiligen_patienten() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1", patientId = "p-1")
|
||||||
|
seedDocumentation(id = "d-2", patientId = "p-1")
|
||||||
|
seedDocumentation(id = "d-3", patientId = "p-2")
|
||||||
|
|
||||||
|
assertEquals(2, documentationDao.countForPatient("p-1"))
|
||||||
|
assertEquals(1, documentationDao.countForPatient("p-2"))
|
||||||
|
assertEquals(0, documentationDao.countForPatient("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- getLatestForPatient -------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getLatestForPatient_liefert_die_zuletzt_erstellte_dokumentation_des_patienten() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-alt", patientId = "p-1", createdAt = 1_000L)
|
||||||
|
seedDocumentation(id = "d-neu", patientId = "p-1", createdAt = 3_000L)
|
||||||
|
seedDocumentation(id = "d-mitte", patientId = "p-1", createdAt = 2_000L)
|
||||||
|
|
||||||
|
assertEquals("d-neu", documentationDao.getLatestForPatient("p-1")?.documentation?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getLatestForPatient_ignoriert_dokumentationen_anderer_patienten() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1", patientId = "p-1", createdAt = 1_000L)
|
||||||
|
seedDocumentation(id = "d-2", patientId = "p-2", createdAt = 9_000L)
|
||||||
|
|
||||||
|
assertEquals("d-1", documentationDao.getLatestForPatient("p-1")?.documentation?.id)
|
||||||
|
assertEquals("d-2", documentationDao.getLatestForPatient("p-2")?.documentation?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getLatestForPatient_liefert_null_ohne_dokumentation() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
assertNull(documentationDao.getLatestForPatient("p-1"))
|
||||||
|
assertNull(documentationDao.getLatestForPatient("unbekannt"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import de.medapp.wunddoku.data.entity.relations.DocumentationWithDetails
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.blockStateEntity
|
||||||
|
import de.medapp.wunddoku.testutil.checkItemStateEntity
|
||||||
|
import de.medapp.wunddoku.testutil.photoEntity
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import de.medapp.wunddoku.testutil.signatureEntity
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationstests (QA-002): [DocumentationWithDetails] (Dokumentation -> Blöcke -> Optionen,
|
||||||
|
* Fotos, Signaturen) sowie Cascade-/Restrict-Verhalten der Foreign Keys.
|
||||||
|
*/
|
||||||
|
class DocumentationRelationsTest : RoomTestBase() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dokumentation [docId] mit 2 Blöcken ("<prefix>-b1" order 2, "<prefix>-b2" order 1),
|
||||||
|
* 3 Optionen (b1: c1, c2 / b2: c3), 2 Fotos und 2 Signaturen.
|
||||||
|
*/
|
||||||
|
private suspend fun seedWithChildren(docId: String, createdAt: Long, prefix: String) {
|
||||||
|
seedDocumentation(id = docId, patientId = "p-1", createdAt = createdAt)
|
||||||
|
blockStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
blockStateEntity(id = "$prefix-b1", documentationId = docId, blockDefinitionId = "wundart", order = 2),
|
||||||
|
blockStateEntity(id = "$prefix-b2", documentationId = docId, blockDefinitionId = "groesse", order = 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
checkItemStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
checkItemStateEntity(id = "$prefix-c1", blockStateId = "$prefix-b1", definitionId = "a", order = 1),
|
||||||
|
checkItemStateEntity(id = "$prefix-c2", blockStateId = "$prefix-b1", definitionId = "b", order = 2),
|
||||||
|
checkItemStateEntity(id = "$prefix-c3", blockStateId = "$prefix-b2", definitionId = "a", order = 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
photoDao.insert(photoEntity(id = "$prefix-ph1", documentationId = docId, orderIndex = 0))
|
||||||
|
photoDao.insert(photoEntity(id = "$prefix-ph2", documentationId = docId, orderIndex = 1))
|
||||||
|
signatureDao.insert(signatureEntity(id = "$prefix-s1", documentationId = docId, orderIndex = 0))
|
||||||
|
signatureDao.insert(signatureEntity(id = "$prefix-s2", documentationId = docId, orderIndex = 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DocumentationWithDetails.itemIdsByBlock(): Map<String, Set<String>> =
|
||||||
|
blocks.associate { it.blockState.id to it.checkItems.map { item -> item.id }.toSet() }
|
||||||
|
|
||||||
|
// --- Laden ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getFullDocumentation_liefert_dokumentation_mit_allen_kindern() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
|
||||||
|
val full = documentationDao.getFullDocumentation("d-a")!!
|
||||||
|
|
||||||
|
assertEquals("d-a", full.documentation.id)
|
||||||
|
assertEquals(setOf("a-b1", "a-b2"), full.blocks.map { it.blockState.id }.toSet())
|
||||||
|
assertEquals(setOf("a-ph1", "a-ph2"), full.photos.map { it.id }.toSet())
|
||||||
|
assertEquals(setOf("a-s1", "a-s2"), full.signatures.map { it.id }.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun jede_option_haengt_am_richtigen_block() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
|
||||||
|
val full = documentationDao.getFullDocumentation("d-a")!!
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
mapOf("a-b1" to setOf("a-c1", "a-c2"), "a-b2" to setOf("a-c3")),
|
||||||
|
full.itemIdsByBlock(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun relation_enthaelt_die_vollstaendigen_entities_nicht_nur_ids() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
|
||||||
|
val full = documentationDao.getFullDocumentation("d-a")!!
|
||||||
|
|
||||||
|
assertEquals(documentationDao.getById("d-a"), full.documentation)
|
||||||
|
assertEquals(blockStateDao.getById("a-b1"), full.blocks.single { it.blockState.id == "a-b1" }.blockState)
|
||||||
|
assertEquals(photoDao.getById("a-ph1"), full.photos.single { it.id == "a-ph1" })
|
||||||
|
assertEquals(signatureDao.getById("a-s1"), full.signatures.single { it.id == "a-s1" })
|
||||||
|
assertEquals(
|
||||||
|
checkItemStateDao.getForBlockState("a-b1"),
|
||||||
|
full.blocks.single { it.blockState.id == "a-b1" }.checkItems.sortedBy { it.itemOrder },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun relation_vermischt_keine_daten_verschiedener_dokumentationen() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
seedWithChildren("d-b", 2_000L, "b")
|
||||||
|
|
||||||
|
val a = documentationDao.getFullDocumentation("d-a")!!
|
||||||
|
val b = documentationDao.getFullDocumentation("d-b")!!
|
||||||
|
|
||||||
|
assertTrue(a.blocks.all { it.blockState.documentationId == "d-a" })
|
||||||
|
assertTrue(a.photos.all { it.documentationId == "d-a" })
|
||||||
|
assertTrue(a.signatures.all { it.documentationId == "d-a" })
|
||||||
|
assertEquals(setOf("a-c1", "a-c2", "a-c3"), a.itemIdsByBlock().values.flatten().toSet())
|
||||||
|
assertEquals(setOf("b-c1", "b-c2", "b-c3"), b.itemIdsByBlock().values.flatten().toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentation_ohne_kinder_liefert_leere_listen_statt_null() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-leer")
|
||||||
|
|
||||||
|
val full = documentationDao.getFullDocumentation("d-leer")!!
|
||||||
|
|
||||||
|
assertTrue(full.blocks.isEmpty())
|
||||||
|
assertTrue(full.photos.isEmpty())
|
||||||
|
assertTrue(full.signatures.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun block_ohne_optionen_liefert_leere_optionsliste() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity()))
|
||||||
|
|
||||||
|
val block = documentationDao.getFullDocumentation("d-1")!!.blocks.single()
|
||||||
|
|
||||||
|
assertTrue(block.checkItems.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getFullDocumentation_liefert_null_fuer_unbekannte_id() = runDbTest {
|
||||||
|
assertNull(documentationDao.getFullDocumentation("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getLatestForPatient_laedt_auch_die_kinder_der_neuesten_dokumentation() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
seedWithChildren("d-b", 2_000L, "b")
|
||||||
|
|
||||||
|
val latest = documentationDao.getLatestForPatient("p-1")!!
|
||||||
|
|
||||||
|
assertEquals("d-b", latest.documentation.id)
|
||||||
|
assertEquals(setOf("b-b1", "b-b2"), latest.blocks.map { it.blockState.id }.toSet())
|
||||||
|
assertEquals(setOf("b-c1", "b-c2", "b-c3"), latest.itemIdsByBlock().values.flatten().toSet())
|
||||||
|
assertEquals(2, latest.photos.size)
|
||||||
|
assertEquals(2, latest.signatures.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun grosse_dokumentation_wird_vollstaendig_und_korrekt_zugeordnet_geladen() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
val blocks = (1..25).map { b ->
|
||||||
|
blockStateEntity(id = "b-$b", blockDefinitionId = "block_$b", title = "Block $b", order = b)
|
||||||
|
}
|
||||||
|
blockStateDao.insertAll(blocks)
|
||||||
|
val items = (1..25).flatMap { b ->
|
||||||
|
(1..20).map { i ->
|
||||||
|
checkItemStateEntity(id = "c-$b-$i", blockStateId = "b-$b", definitionId = "item_$i", order = i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkItemStateDao.insertAll(items)
|
||||||
|
|
||||||
|
val full = documentationDao.getFullDocumentation("d-1")!!
|
||||||
|
|
||||||
|
assertEquals(25, full.blocks.size)
|
||||||
|
assertEquals(500, full.blocks.sumOf { it.checkItems.size })
|
||||||
|
for (block in full.blocks) {
|
||||||
|
assertEquals(20, block.checkItems.size)
|
||||||
|
assertTrue(block.checkItems.all { it.blockStateId == block.blockState.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cascade / Restrict --------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun foreign_keys_sind_in_der_testdatenbank_aktiviert() = runDbTest {
|
||||||
|
assertEquals(1, queryInt("PRAGMA foreign_keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentation_loeschen_entfernt_bloecke_optionen_fotos_und_signaturen_per_cascade() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
|
||||||
|
documentationDao.delete(documentationDao.getById("d-a")!!)
|
||||||
|
|
||||||
|
assertEquals(0, countRows("documentations"))
|
||||||
|
assertEquals(0, countRows("block_states"))
|
||||||
|
assertEquals(0, countRows("check_item_states"))
|
||||||
|
assertEquals(0, countRows("photos"))
|
||||||
|
assertEquals(0, countRows("signatures"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentation_loeschen_laesst_patient_und_andere_dokumentationen_unangetastet() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
seedWithChildren("d-b", 2_000L, "b")
|
||||||
|
|
||||||
|
documentationDao.delete(documentationDao.getById("d-a")!!)
|
||||||
|
|
||||||
|
assertNotNull(patientDao.getById("p-1"))
|
||||||
|
val remaining = documentationDao.getFullDocumentation("d-b")!!
|
||||||
|
assertEquals(2, remaining.blocks.size)
|
||||||
|
assertEquals(3, remaining.itemIdsByBlock().values.sumOf { it.size })
|
||||||
|
assertEquals(2, remaining.photos.size)
|
||||||
|
assertEquals(2, remaining.signatures.size)
|
||||||
|
assertEquals(2, countRows("block_states"))
|
||||||
|
assertEquals(3, countRows("check_item_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun block_loeschen_entfernt_seine_optionen_per_cascade_aber_nicht_die_anderer_bloecke() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
|
||||||
|
execSql("DELETE FROM block_states WHERE id = 'a-b1'")
|
||||||
|
|
||||||
|
assertEquals(listOf("a-c3"), checkItemStateDao.getForBlockState("a-b2").map { it.id })
|
||||||
|
assertTrue(checkItemStateDao.getForBlockState("a-b1").isEmpty())
|
||||||
|
assertEquals(1, countRows("check_item_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patient_mit_dokumentation_kann_nicht_geloescht_werden_und_kinder_bleiben_erhalten() = runDbTest {
|
||||||
|
seedWithChildren("d-a", 1_000L, "a")
|
||||||
|
|
||||||
|
val failed = runCatching { patientDao.delete(patientDao.getById("p-1")!!) }
|
||||||
|
|
||||||
|
assertTrue(failed.isFailure)
|
||||||
|
assertNotNull(documentationDao.getFullDocumentation("d-a"))
|
||||||
|
assertEquals(3, countRows("check_item_states"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.awaitMatching
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.patientEntity
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** DAO-Tests für [PatientDao]. */
|
||||||
|
class PatientDaoTest : RoomTestBase() {
|
||||||
|
|
||||||
|
// --- insert / getById ----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert_und_getById_liefern_alle_felder_zurueck() = runDbTest {
|
||||||
|
val entity = patientEntity(phone2 = "0987 654321", phone3 = "0555 123")
|
||||||
|
|
||||||
|
patientDao.insert(entity)
|
||||||
|
|
||||||
|
assertEquals(entity, patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun optionale_telefonnummern_bleiben_null() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity(phone2 = null, phone3 = null))
|
||||||
|
|
||||||
|
val loaded = patientDao.getById("p-1")!!
|
||||||
|
|
||||||
|
assertNull(loaded.phone2)
|
||||||
|
assertNull(loaded.phone3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun umlaute_und_sonderzeichen_bleiben_unveraendert() = runDbTest {
|
||||||
|
val entity = patientEntity(
|
||||||
|
firstName = "Jörg-Åke", lastName = "O'Brien-Müller", street = "Straße \"des\" 17. Juni 1/2", city = "Köln-Ehrenfeld",
|
||||||
|
)
|
||||||
|
|
||||||
|
patientDao.insert(entity)
|
||||||
|
|
||||||
|
assertEquals(entity, patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getById_liefert_null_fuer_unbekannte_id() = runDbTest {
|
||||||
|
assertNull(patientDao.getById("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert_mit_vorhandener_id_wirft_und_laesst_den_bestehenden_datensatz_unveraendert() = runDbTest {
|
||||||
|
val original = patientEntity(lastName = "Original")
|
||||||
|
patientDao.insert(original)
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> { patientDao.insert(patientEntity(lastName = "Duplikat")) }
|
||||||
|
|
||||||
|
assertEquals(original, patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun doppelte_stammdaten_mit_unterschiedlicher_id_sind_auf_db_ebene_erlaubt() = runDbTest {
|
||||||
|
// Duplikatschutz liegt bewusst in der Anwendung (findExisting), nicht in einem UNIQUE-Index.
|
||||||
|
patientDao.insert(patientEntity(id = "p-1"))
|
||||||
|
patientDao.insert(patientEntity(id = "p-2"))
|
||||||
|
|
||||||
|
assertEquals(2, countRows("patients"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- update / delete -----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun update_ueberschreibt_die_felder_des_datensatzes() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity())
|
||||||
|
val changed = patientEntity(lastName = "Musterfrau", city = "Neustadt", phone2 = "0111", updatedAt = 2_000L)
|
||||||
|
|
||||||
|
patientDao.update(changed)
|
||||||
|
|
||||||
|
assertEquals(changed, patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun update_einer_unbekannten_id_legt_keinen_datensatz_an() = runDbTest {
|
||||||
|
patientDao.update(patientEntity(id = "geist"))
|
||||||
|
|
||||||
|
assertNull(patientDao.getById("geist"))
|
||||||
|
assertEquals(0, countRows("patients"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_entfernt_nur_den_uebergebenen_patienten() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity(id = "p-1"))
|
||||||
|
patientDao.insert(patientEntity(id = "p-2"))
|
||||||
|
|
||||||
|
patientDao.delete(patientEntity(id = "p-1"))
|
||||||
|
|
||||||
|
assertNull(patientDao.getById("p-1"))
|
||||||
|
assertNotNull(patientDao.getById("p-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_eines_unbekannten_patienten_ist_wirkungslos() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity())
|
||||||
|
|
||||||
|
patientDao.delete(patientEntity(id = "unbekannt"))
|
||||||
|
|
||||||
|
assertEquals(1, countRows("patients"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_wird_durch_vorhandene_dokumentation_verhindert_restrict() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> { patientDao.delete(patientEntity()) }
|
||||||
|
|
||||||
|
assertNotNull(patientDao.getById("p-1"))
|
||||||
|
assertEquals(1, countRows("documentations"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- findExisting --------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findExisting_trifft_bei_gleichem_vornamen_nachnamen_und_geburtsdatum() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity())
|
||||||
|
|
||||||
|
assertEquals("p-1", patientDao.findExisting("Max", "Mustermann", "1980-03-14")?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findExisting_trifft_nicht_bei_abweichendem_feld() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity())
|
||||||
|
|
||||||
|
assertNull(patientDao.findExisting("Moritz", "Mustermann", "1980-03-14"))
|
||||||
|
assertNull(patientDao.findExisting("Max", "Musterfrau", "1980-03-14"))
|
||||||
|
assertNull(patientDao.findExisting("Max", "Mustermann", "1980-03-15"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findExisting_ist_ein_exakter_abgleich_und_unterscheidet_gross_und_kleinschreibung() = runDbTest {
|
||||||
|
// Dokumentiert das aktuelle Verhalten (SQL "="): "max" findet "Max" NICHT.
|
||||||
|
patientDao.insert(patientEntity())
|
||||||
|
|
||||||
|
assertNull(patientDao.findExisting("max", "mustermann", "1980-03-14"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findExisting_liefert_bei_mehreren_treffern_genau_einen() = runDbTest {
|
||||||
|
patientDao.insert(patientEntity(id = "p-1"))
|
||||||
|
patientDao.insert(patientEntity(id = "p-2"))
|
||||||
|
|
||||||
|
val found = patientDao.findExisting("Max", "Mustermann", "1980-03-14")
|
||||||
|
|
||||||
|
assertTrue(found?.id == "p-1" || found?.id == "p-2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- hasDocumentations ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasDocumentations_ist_false_ohne_und_true_mit_dokumentation() = runDbTest {
|
||||||
|
seedPatient("p-1")
|
||||||
|
seedPatient("p-2", lastName = "Musterfrau")
|
||||||
|
seedDocumentation(id = "d-1", patientId = "p-1")
|
||||||
|
|
||||||
|
assertTrue(patientDao.hasDocumentations("p-1"))
|
||||||
|
assertFalse(patientDao.hasDocumentations("p-2"))
|
||||||
|
assertFalse(patientDao.hasDocumentations("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- observeAllWithLastDocumentation -------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAllWithLastDocumentation_sortiert_nach_nachname_dann_vorname() = runDbTest {
|
||||||
|
seedPatient("p-1", firstName = "Max", lastName = "Müller")
|
||||||
|
seedPatient("p-2", firstName = "Anna", lastName = "Müller")
|
||||||
|
seedPatient("p-3", firstName = "Zoe", lastName = "Adler")
|
||||||
|
|
||||||
|
val result = patientDao.observeAllWithLastDocumentation().first()
|
||||||
|
|
||||||
|
assertEquals(listOf("p-3", "p-2", "p-1"), result.map { it.patient.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lastDocumentationAt_ist_das_maximale_erstelldatum_unabhaengig_von_der_einfuegereihenfolge() = runDbTest {
|
||||||
|
seedPatient("p-1")
|
||||||
|
seedDocumentation(id = "d-1", patientId = "p-1", createdAt = 3_000L)
|
||||||
|
seedDocumentation(id = "d-2", patientId = "p-1", createdAt = 1_000L)
|
||||||
|
seedDocumentation(id = "d-3", patientId = "p-1", createdAt = 2_000L)
|
||||||
|
|
||||||
|
val result = patientDao.observeAllWithLastDocumentation().first().single()
|
||||||
|
|
||||||
|
assertEquals(3_000L, result.lastDocumentationAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lastDocumentationAt_ist_null_ohne_dokumentation_und_beruecksichtigt_nur_eigene_dokumentationen() = runDbTest {
|
||||||
|
seedPatient("p-1")
|
||||||
|
seedPatient("p-2", firstName = "Erika", lastName = "Musterfrau")
|
||||||
|
seedDocumentation(id = "d-1", patientId = "p-1", createdAt = 5_000L)
|
||||||
|
|
||||||
|
val byId = patientDao.observeAllWithLastDocumentation().first().associateBy { it.patient.id }
|
||||||
|
|
||||||
|
assertEquals(5_000L, byId.getValue("p-1").lastDocumentationAt)
|
||||||
|
assertNull(byId.getValue("p-2").lastDocumentationAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAllWithLastDocumentation_liefert_leere_liste_ohne_patienten() = runDbTest {
|
||||||
|
assertTrue(patientDao.observeAllWithLastDocumentation().first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAllWithLastDocumentation_aktualisiert_sich_bei_aenderungen() = runDbTest {
|
||||||
|
seedPatient("p-1")
|
||||||
|
val waiting = async {
|
||||||
|
patientDao.observeAllWithLastDocumentation().awaitMatching { list ->
|
||||||
|
list.size == 2 && list.any { it.patient.id == "p-1" && it.lastDocumentationAt == 9_000L }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPatient("p-2", firstName = "Erika", lastName = "Musterfrau")
|
||||||
|
seedDocumentation(id = "d-1", patientId = "p-1", createdAt = 9_000L)
|
||||||
|
|
||||||
|
assertEquals(2, waiting.await().size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.photoEntity
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import de.medapp.wunddoku.testutil.signatureEntity
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** DAO-Tests für [PhotoDao] und [SignatureDao] (beide nach demselben Muster aufgebaut). */
|
||||||
|
class PhotoAndSignatureDaoTest : RoomTestBase() {
|
||||||
|
|
||||||
|
// --- PhotoDao ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_insert_und_getById_liefern_alle_felder() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
val photo = photoEntity(fileName = "wunde_2026-08-23.jpg", orderIndex = 2, createdAt = 5_000L)
|
||||||
|
|
||||||
|
photoDao.insert(photo)
|
||||||
|
|
||||||
|
assertEquals(photo, photoDao.getById("ph-1"))
|
||||||
|
assertNull(photoDao.getById("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_getForDocumentation_sortiert_nach_orderIndex_und_filtert_nach_dokumentation() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
photoDao.insert(photoEntity(id = "ph-3", documentationId = "d-1", orderIndex = 2))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-1", documentationId = "d-1", orderIndex = 0))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-2", documentationId = "d-1", orderIndex = 1))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-x", documentationId = "d-2", orderIndex = 0))
|
||||||
|
|
||||||
|
assertEquals(listOf("ph-1", "ph-2", "ph-3"), photoDao.getForDocumentation("d-1").map { it.id })
|
||||||
|
assertEquals(listOf("ph-x"), photoDao.getForDocumentation("d-2").map { it.id })
|
||||||
|
assertTrue(photoDao.getForDocumentation("unbekannt").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_getMaxOrderIndex_ist_minus_eins_ohne_fotos_sonst_der_hoechste_wert() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
assertEquals(-1, photoDao.getMaxOrderIndex("d-1"))
|
||||||
|
|
||||||
|
photoDao.insert(photoEntity(id = "ph-1", documentationId = "d-1", orderIndex = 0))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-2", documentationId = "d-1", orderIndex = 4))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-3", documentationId = "d-2", orderIndex = 9))
|
||||||
|
|
||||||
|
assertEquals(4, photoDao.getMaxOrderIndex("d-1"))
|
||||||
|
assertEquals(9, photoDao.getMaxOrderIndex("d-2"))
|
||||||
|
assertEquals(-1, photoDao.getMaxOrderIndex("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_getMaxOrderIndex_bleibt_nach_dem_loeschen_eines_mittleren_fotos_korrekt() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
photoDao.insert(photoEntity(id = "ph-1", orderIndex = 0))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-2", orderIndex = 1))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-3", orderIndex = 2))
|
||||||
|
|
||||||
|
photoDao.delete(photoEntity(id = "ph-2", orderIndex = 1))
|
||||||
|
|
||||||
|
assertEquals(2, photoDao.getMaxOrderIndex("d-1"))
|
||||||
|
assertEquals(listOf(0, 2), photoDao.getForDocumentation("d-1").map { it.orderIndex })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_delete_entfernt_nur_das_angegebene_foto() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
photoDao.insert(photoEntity(id = "ph-1", orderIndex = 0))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-2", orderIndex = 1))
|
||||||
|
|
||||||
|
photoDao.delete(photoEntity(id = "ph-1"))
|
||||||
|
|
||||||
|
assertNull(photoDao.getById("ph-1"))
|
||||||
|
assertEquals("ph-2", photoDao.getById("ph-2")?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_mit_gleichem_orderIndex_ist_erlaubt_kein_unique_index() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
|
||||||
|
photoDao.insert(photoEntity(id = "ph-1", orderIndex = 0))
|
||||||
|
photoDao.insert(photoEntity(id = "ph-2", orderIndex = 0))
|
||||||
|
|
||||||
|
assertEquals(2, photoDao.getForDocumentation("d-1").size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun photo_mit_vorhandener_id_oder_unbekannter_dokumentation_wirft() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
photoDao.insert(photoEntity(id = "ph-1"))
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> { photoDao.insert(photoEntity(id = "ph-1", fileName = "andere.jpg")) }
|
||||||
|
expectThrows<SQLiteConstraintException> { photoDao.insert(photoEntity(id = "ph-2", documentationId = "gibt-es-nicht")) }
|
||||||
|
|
||||||
|
assertEquals(1, countRows("photos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SignatureDao --------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signature_insert_und_getById_liefern_alle_felder() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
val signature = signatureEntity(signerName = "Dr. Åsa Müller-Öz", fileName = "sig_1.png", orderIndex = 1, createdAt = 7_000L)
|
||||||
|
|
||||||
|
signatureDao.insert(signature)
|
||||||
|
|
||||||
|
assertEquals(signature, signatureDao.getById("s-1"))
|
||||||
|
assertNull(signatureDao.getById("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signature_getForDocumentation_sortiert_nach_orderIndex_und_filtert_nach_dokumentation() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-3", documentationId = "d-1", orderIndex = 2))
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-1", documentationId = "d-1", orderIndex = 0))
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-2", documentationId = "d-1", orderIndex = 1))
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-x", documentationId = "d-2", orderIndex = 0))
|
||||||
|
|
||||||
|
assertEquals(listOf("s-1", "s-2", "s-3"), signatureDao.getForDocumentation("d-1").map { it.id })
|
||||||
|
assertEquals(listOf("s-x"), signatureDao.getForDocumentation("d-2").map { it.id })
|
||||||
|
assertTrue(signatureDao.getForDocumentation("unbekannt").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signature_getMaxOrderIndex_ist_minus_eins_ohne_signaturen_sonst_der_hoechste_wert() = runDbTest {
|
||||||
|
seedDocumentation(id = "d-1")
|
||||||
|
seedDocumentation(id = "d-2")
|
||||||
|
assertEquals(-1, signatureDao.getMaxOrderIndex("d-1"))
|
||||||
|
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-1", documentationId = "d-1", orderIndex = 0))
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-2", documentationId = "d-1", orderIndex = 3))
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-3", documentationId = "d-2", orderIndex = 7))
|
||||||
|
|
||||||
|
assertEquals(3, signatureDao.getMaxOrderIndex("d-1"))
|
||||||
|
assertEquals(7, signatureDao.getMaxOrderIndex("d-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signature_delete_entfernt_nur_die_angegebene_signatur() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-1", orderIndex = 0))
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-2", orderIndex = 1))
|
||||||
|
|
||||||
|
signatureDao.delete(signatureEntity(id = "s-1"))
|
||||||
|
|
||||||
|
assertNull(signatureDao.getById("s-1"))
|
||||||
|
assertEquals("s-2", signatureDao.getById("s-2")?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signature_mit_vorhandener_id_oder_unbekannter_dokumentation_wirft() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
signatureDao.insert(signatureEntity(id = "s-1"))
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> { signatureDao.insert(signatureEntity(id = "s-1", signerName = "Anderer")) }
|
||||||
|
expectThrows<SQLiteConstraintException> { signatureDao.insert(signatureEntity(id = "s-2", documentationId = "gibt-es-nicht")) }
|
||||||
|
|
||||||
|
assertEquals(1, countRows("signatures"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.awaitMatching
|
||||||
|
import de.medapp.wunddoku.testutil.documentationEntity
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suchtests (QA-002): [DocumentationDao.search] und [PatientDao.searchWithLastDocumentation].
|
||||||
|
*
|
||||||
|
* Szenario (Erstelldatum in Klammern):
|
||||||
|
* - p-1 Max Mustermann, 1980-03-14: d-1 (1000), d-2 (3000)
|
||||||
|
* - p-2 Erika Musterfrau, 1975-12-01: d-3 (2000)
|
||||||
|
* - p-3 Hans Beispiel, 1990-07-30: d-4 (500)
|
||||||
|
* - p-4 Zora Zimmer, 2001-01-01: keine Dokumentation
|
||||||
|
*/
|
||||||
|
class SearchQueryTest : RoomTestBase() {
|
||||||
|
|
||||||
|
private suspend fun seedDoc(id: String, patientId: String, createdAt: Long, first: String, last: String, birth: String) {
|
||||||
|
documentationDao.insert(
|
||||||
|
documentationEntity(
|
||||||
|
id = id, patientId = patientId, createdAt = createdAt,
|
||||||
|
firstNameSnapshot = first, lastNameSnapshot = last, birthDateSnapshot = birth,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun seedScenario() {
|
||||||
|
seedPatient("p-1", "Max", "Mustermann", "1980-03-14")
|
||||||
|
seedPatient("p-2", "Erika", "Musterfrau", "1975-12-01")
|
||||||
|
seedPatient("p-3", "Hans", "Beispiel", "1990-07-30")
|
||||||
|
seedPatient("p-4", "Zora", "Zimmer", "2001-01-01")
|
||||||
|
seedDoc("d-1", "p-1", 1_000L, "Max", "Mustermann", "1980-03-14")
|
||||||
|
seedDoc("d-2", "p-1", 3_000L, "Max", "Mustermann", "1980-03-14")
|
||||||
|
seedDoc("d-3", "p-2", 2_000L, "Erika", "Musterfrau", "1975-12-01")
|
||||||
|
seedDoc("d-4", "p-3", 500L, "Hans", "Beispiel", "1990-07-30")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun searchDocs(query: String) = documentationDao.search(query).first().map { it.id }
|
||||||
|
|
||||||
|
// --- Dokumentationssuche -------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_findet_ueber_den_nachnamen_neueste_zuerst() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("d-2", "d-3", "d-1"), searchDocs("Muster"))
|
||||||
|
assertEquals(listOf("d-2", "d-1"), searchDocs("Mustermann"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_findet_ueber_den_vornamen_ohne_beachtung_der_gross_und_kleinschreibung() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("d-3"), searchDocs("erika"))
|
||||||
|
assertEquals(listOf("d-3"), searchDocs("ERIKA"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_findet_ueber_das_geburtsdatum_auch_teilweise() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("d-4"), searchDocs("1990-07"))
|
||||||
|
assertEquals(listOf("d-2", "d-1"), searchDocs("1980"))
|
||||||
|
assertEquals(listOf("d-3"), searchDocs("12-01"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_ist_ein_teilstring_abgleich() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("d-2", "d-1"), searchDocs("term"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_ohne_treffer_liefert_leere_liste() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertTrue(searchDocs("Zzz").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_liefert_keine_dubletten_wenn_mehrere_spalten_treffen() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
// "e" trifft bei p-2 gleichzeitig Vor- und Nachnamen (Erika / Musterfrau).
|
||||||
|
val ids = searchDocs("e")
|
||||||
|
|
||||||
|
assertEquals(listOf("d-2", "d-3", "d-1", "d-4"), ids)
|
||||||
|
assertEquals(ids.size, ids.toSet().size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_findet_keine_patienten_ohne_dokumentation() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertTrue(searchDocs("Zora").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_mit_leerem_text_liefert_alle_dokumentationen_der_schutz_davor_liegt_im_repository() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("d-2", "d-3", "d-1", "d-4"), searchDocs(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_sucht_gegen_den_aktuellen_namen_und_nicht_gegen_den_snapshot() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
patientDao.update(patientDao.getById("p-1")!!.copy(lastName = "Neuname"))
|
||||||
|
|
||||||
|
assertEquals(listOf("d-2", "d-1"), searchDocs("Neuname"))
|
||||||
|
assertTrue(searchDocs("Mustermann").isEmpty())
|
||||||
|
// Die gelieferten Zeilen zeigen weiterhin den eingefrorenen Namen.
|
||||||
|
val hits = documentationDao.search("Neuname").first()
|
||||||
|
assertTrue(hits.all { it.patientLastNameSnapshot == "Mustermann" })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_behandelt_sql_sonderzeichen_als_normalen_text() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertTrue(searchDocs("'; DROP TABLE patients; --").isEmpty())
|
||||||
|
assertTrue(searchDocs("\"").isEmpty())
|
||||||
|
assertTrue(searchDocs("O'Brien").isEmpty())
|
||||||
|
assertEquals(4, countRows("patients"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dokumentationssuche_aktualisiert_sich_bei_neuen_dokumentationen_und_namensaenderungen() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
val afterNewDoc = async { documentationDao.search("Muster").awaitMatching { it.size == 4 } }
|
||||||
|
seedDoc("d-5", "p-1", 4_000L, "Max", "Mustermann", "1980-03-14")
|
||||||
|
assertEquals("d-5", afterNewDoc.await().first().id)
|
||||||
|
|
||||||
|
val afterRename = async { documentationDao.search("Neuname").awaitMatching { it.size == 3 } }
|
||||||
|
patientDao.update(patientDao.getById("p-1")!!.copy(lastName = "Neuname"))
|
||||||
|
assertEquals(listOf("d-5", "d-2", "d-1"), afterRename.await().map { it.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Patientensuche ------------------------------------------------------------------
|
||||||
|
|
||||||
|
private suspend fun searchPatients(query: String) = patientDao.searchWithLastDocumentation(query).first()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_sortiert_treffer_nach_nachname_und_liefert_letztes_dokumentationsdatum() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
val result = searchPatients("Muster")
|
||||||
|
|
||||||
|
assertEquals(listOf("p-2", "p-1"), result.map { it.patient.id }) // Musterfrau vor Mustermann
|
||||||
|
assertEquals(listOf(2_000L, 3_000L), result.map { it.lastDocumentationAt })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_findet_ueber_vorname_nachname_und_geburtsdatum() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("p-3"), searchPatients("han").map { it.patient.id })
|
||||||
|
assertEquals(listOf("p-3"), searchPatients("beispiel").map { it.patient.id })
|
||||||
|
assertEquals(listOf("p-2"), searchPatients("1975").map { it.patient.id })
|
||||||
|
assertTrue(searchPatients("Zzz").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_findet_auch_patienten_ohne_dokumentation_mit_null_datum() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
val result = searchPatients("Zora").single()
|
||||||
|
|
||||||
|
assertEquals("p-4", result.patient.id)
|
||||||
|
assertNull(result.lastDocumentationAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_liefert_keine_dubletten_trotz_mehrerer_dokumentationen_und_treffer_spalten() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
val ids = searchPatients("e").map { it.patient.id }
|
||||||
|
|
||||||
|
assertEquals(ids.size, ids.toSet().size)
|
||||||
|
assertEquals(listOf("p-3", "p-2", "p-1", "p-4"), ids) // Beispiel, Musterfrau, Mustermann, Zimmer
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_mit_leerem_text_liefert_alle_patienten_sortiert() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertEquals(listOf("p-3", "p-2", "p-1", "p-4"), searchPatients("").map { it.patient.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_sortiert_bei_gleichem_nachnamen_nach_vornamen() = runDbTest {
|
||||||
|
seedPatient("p-1", "Max", "Müller", "1980-01-01")
|
||||||
|
seedPatient("p-2", "Anna", "Müller", "1981-01-01")
|
||||||
|
|
||||||
|
assertEquals(listOf("p-2", "p-1"), searchPatients("Müller").map { it.patient.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_behandelt_sql_sonderzeichen_als_normalen_text() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
assertTrue(searchPatients("'; DROP TABLE patients; --").isEmpty())
|
||||||
|
assertEquals(4, countRows("patients"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun patientensuche_aktualisiert_sich_bei_neuen_patienten_und_dokumentationen() = runDbTest {
|
||||||
|
seedScenario()
|
||||||
|
|
||||||
|
val afterNewPatient = async { patientDao.searchWithLastDocumentation("Muster").awaitMatching { it.size == 3 } }
|
||||||
|
seedPatient("p-5", "Karl", "Musterkind", "2010-05-05")
|
||||||
|
assertEquals(3, afterNewPatient.await().size)
|
||||||
|
|
||||||
|
val afterNewDoc = async {
|
||||||
|
patientDao.searchWithLastDocumentation("Musterkind").awaitMatching { list -> list.singleOrNull()?.lastDocumentationAt == 8_000L }
|
||||||
|
}
|
||||||
|
seedDoc("d-9", "p-5", 8_000L, "Karl", "Musterkind", "2010-05-05")
|
||||||
|
afterNewDoc.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package de.medapp.wunddoku.data.dao
|
||||||
|
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import de.medapp.wunddoku.testutil.woundCarerEntity
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** DAO-Tests für [WoundCarerDao] (Wundversorger-Register). */
|
||||||
|
class WoundCarerDaoTest : RoomTestBase() {
|
||||||
|
|
||||||
|
// --- insertOrUpdate ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertOrUpdate_legt_neuen_eintrag_an() = runDbTest {
|
||||||
|
val carer = woundCarerEntity(updatedAt = 5_000L)
|
||||||
|
|
||||||
|
woundCarerDao.insertOrUpdate(carer)
|
||||||
|
|
||||||
|
assertEquals(carer, woundCarerDao.getMostRecentlyUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertOrUpdate_ersetzt_vorhandenen_eintrag_mit_gleicher_id_vollstaendig() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna Schmidt", signatureFileName = "alt.png", updatedAt = 1_000L))
|
||||||
|
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna Schmidt-Neu", signatureFileName = "neu.png", updatedAt = 2_000L))
|
||||||
|
|
||||||
|
assertEquals(1, countRows("wound_carers"))
|
||||||
|
val loaded = woundCarerDao.getMostRecentlyUsed()!!
|
||||||
|
assertEquals("Anna Schmidt-Neu", loaded.name)
|
||||||
|
assertEquals("neu.png", loaded.signatureFileName)
|
||||||
|
assertEquals(2_000L, loaded.updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertOrUpdate_mit_verschiedenen_ids_und_gleichem_namen_legt_zwei_eintraege_an() = runDbTest {
|
||||||
|
// Namens-Eindeutigkeit stellt das Repository (findByName) sicher, nicht die Datenbank.
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna"))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-2", name = "Anna"))
|
||||||
|
|
||||||
|
assertEquals(2, countRows("wound_carers"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- findByName ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findByName_ist_ein_exakter_abgleich_ohne_beachtung_der_gross_und_kleinschreibung() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna Schmidt"))
|
||||||
|
|
||||||
|
assertEquals("w-1", woundCarerDao.findByName("Anna Schmidt")?.id)
|
||||||
|
assertEquals("w-1", woundCarerDao.findByName("anna schmidt")?.id)
|
||||||
|
assertEquals("w-1", woundCarerDao.findByName("ANNA SCHMIDT")?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findByName_trifft_nicht_bei_teilstring_oder_praefix() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(name = "Anna Schmidt"))
|
||||||
|
|
||||||
|
assertNull(woundCarerDao.findByName("Anna"))
|
||||||
|
assertNull(woundCarerDao.findByName("Schmidt"))
|
||||||
|
assertNull(woundCarerDao.findByName("Anna Schmidt "))
|
||||||
|
assertNull(woundCarerDao.findByName("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- searchByNamePrefix --------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchByNamePrefix_liefert_praefix_treffer_ohne_beachtung_der_gross_und_kleinschreibung() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna Schmidt", updatedAt = 1_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-2", name = "Andreas Müller", updatedAt = 2_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-3", name = "Bernd Beispiel", updatedAt = 3_000L))
|
||||||
|
|
||||||
|
assertEquals(listOf("w-2", "w-1"), woundCarerDao.searchByNamePrefix("an", 10).map { it.id })
|
||||||
|
assertEquals(listOf("w-2", "w-1"), woundCarerDao.searchByNamePrefix("AN", 10).map { it.id })
|
||||||
|
assertEquals(listOf("w-3"), woundCarerDao.searchByNamePrefix("Bern", 10).map { it.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchByNamePrefix_ist_kein_teilstring_abgleich() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(name = "Anna Schmidt"))
|
||||||
|
|
||||||
|
assertTrue(woundCarerDao.searchByNamePrefix("Schmidt", 10).isEmpty())
|
||||||
|
assertTrue(woundCarerDao.searchByNamePrefix("nna", 10).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchByNamePrefix_sortiert_nach_letzter_verwendung_absteigend_und_beachtet_das_limit() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna A", updatedAt = 1_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-2", name = "Anna B", updatedAt = 4_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-3", name = "Anna C", updatedAt = 3_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-4", name = "Anna D", updatedAt = 2_000L))
|
||||||
|
|
||||||
|
assertEquals(listOf("w-2", "w-3", "w-4", "w-1"), woundCarerDao.searchByNamePrefix("Anna", 10).map { it.id })
|
||||||
|
assertEquals(listOf("w-2", "w-3"), woundCarerDao.searchByNamePrefix("Anna", 2).map { it.id })
|
||||||
|
assertTrue(woundCarerDao.searchByNamePrefix("Anna", 0).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchByNamePrefix_mit_leerem_praefix_liefert_alle_eintraege_der_schutz_davor_liegt_im_repository() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna"))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-2", name = "Bernd", updatedAt = 2_000L))
|
||||||
|
|
||||||
|
assertEquals(2, woundCarerDao.searchByNamePrefix("", 10).size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- getMostRecentlyUsed -------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getMostRecentlyUsed_ist_null_bei_leerer_tabelle() = runDbTest {
|
||||||
|
assertNull(woundCarerDao.getMostRecentlyUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getMostRecentlyUsed_liefert_den_eintrag_mit_dem_hoechsten_updatedAt() = runDbTest {
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna", updatedAt = 1_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-2", name = "Bernd", updatedAt = 3_000L))
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-3", name = "Clara", updatedAt = 2_000L))
|
||||||
|
|
||||||
|
assertEquals("w-2", woundCarerDao.getMostRecentlyUsed()?.id)
|
||||||
|
|
||||||
|
woundCarerDao.insertOrUpdate(woundCarerEntity(id = "w-1", name = "Anna", updatedAt = 9_000L))
|
||||||
|
|
||||||
|
assertEquals("w-1", woundCarerDao.getMostRecentlyUsed()?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package de.medapp.wunddoku.data.database
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import androidx.room.withTransaction
|
||||||
|
import de.medapp.wunddoku.testutil.RoomTestBase
|
||||||
|
import de.medapp.wunddoku.testutil.blockStateEntity
|
||||||
|
import de.medapp.wunddoku.testutil.checkItemStateEntity
|
||||||
|
import de.medapp.wunddoku.testutil.documentationEntity
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.photoEntity
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction-Tests (QA-002): Room-Transaktionen (`db.withTransaction`) müssen Mehrtabellen-
|
||||||
|
* Schreibvorgänge - wie in DocumentationRepository.createEmpty()/continueDocumentation() -
|
||||||
|
* atomar machen.
|
||||||
|
*
|
||||||
|
* Hinweis: In-Memory-Datenbanken haben nur eine Verbindung. Deshalb wird innerhalb einer
|
||||||
|
* Transaktion ausschließlich über DAOs gelesen, nie über Roh-SQL des Test-Threads.
|
||||||
|
*/
|
||||||
|
class DatabaseTransactionTest : RoomTestBase() {
|
||||||
|
|
||||||
|
// --- Commit --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun commit_macht_alle_schreibvorgaenge_sichtbar() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity())
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity()))
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity()))
|
||||||
|
photoDao.insert(photoEntity())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, countRows("documentations"))
|
||||||
|
assertEquals(1, countRows("block_states"))
|
||||||
|
assertEquals(1, countRows("check_item_states"))
|
||||||
|
assertEquals(1, countRows("photos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun transaktion_gibt_das_ergebnis_des_blocks_zurueck() = runDbTest {
|
||||||
|
val result = db.withTransaction {
|
||||||
|
seedPatient()
|
||||||
|
42
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(42, result)
|
||||||
|
assertNotNull(patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun innerhalb_der_transaktion_sind_die_eigenen_schreibvorgaenge_bereits_lesbar() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity())
|
||||||
|
assertNotNull(documentationDao.getById("d-1"))
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity()))
|
||||||
|
assertEquals(1, blockStateDao.getForDocumentation("d-1").size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rollback ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exception_im_block_verwirft_alle_schreibvorgaenge_und_wird_weitergereicht() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
val error = expectThrows<IllegalStateException> {
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity())
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity()))
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity()))
|
||||||
|
error("Abbruch mitten in der Transaktion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("Abbruch mitten in der Transaktion", error.message)
|
||||||
|
assertNull(documentationDao.getById("d-1"))
|
||||||
|
assertEquals(0, countRows("block_states"))
|
||||||
|
assertEquals(0, countRows("check_item_states"))
|
||||||
|
assertNotNull("Daten vor der Transaktion bleiben bestehen", patientDao.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun constraint_verletzung_in_einem_spaeteren_schritt_verwirft_auch_die_frueheren_inserts() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity())
|
||||||
|
photoDao.insert(photoEntity(documentationId = "gibt-es-nicht")) // FK-Verletzung
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(documentationDao.getById("d-1"))
|
||||||
|
assertEquals(0, countRows("photos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertAll_ist_atomar_ein_fehlerhafter_eintrag_verwirft_die_ganze_liste() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> {
|
||||||
|
blockStateDao.insertAll(
|
||||||
|
listOf(
|
||||||
|
blockStateEntity(id = "b-1", blockDefinitionId = "wundart"),
|
||||||
|
blockStateEntity(id = "b-2", blockDefinitionId = "groesse"),
|
||||||
|
blockStateEntity(id = "b-3", blockDefinitionId = "wundart"), // verletzt UNIQUE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, countRows("block_states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rollback_nimmt_ein_cascade_loeschen_wieder_zurueck() = runDbTest {
|
||||||
|
val doc = seedDocumentation()
|
||||||
|
blockStateDao.insertAll(listOf(blockStateEntity()))
|
||||||
|
checkItemStateDao.insertAll(listOf(checkItemStateEntity()))
|
||||||
|
photoDao.insert(photoEntity())
|
||||||
|
|
||||||
|
expectThrows<IllegalStateException> {
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.delete(doc)
|
||||||
|
assertNull(documentationDao.getById("d-1"))
|
||||||
|
error("Abbruch nach dem Löschen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(documentationDao.getById("d-1"))
|
||||||
|
assertEquals(1, countRows("block_states"))
|
||||||
|
assertEquals(1, countRows("check_item_states"))
|
||||||
|
assertEquals(1, countRows("photos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun datenbank_bleibt_nach_einer_fehlgeschlagenen_transaktion_voll_nutzbar() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
expectThrows<IllegalStateException> {
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-verloren"))
|
||||||
|
error("Abbruch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.withTransaction { documentationDao.insert(documentationEntity(id = "d-ok")) }
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-direkt"))
|
||||||
|
|
||||||
|
assertNull(documentationDao.getById("d-verloren"))
|
||||||
|
assertNotNull(documentationDao.getById("d-ok"))
|
||||||
|
assertNotNull(documentationDao.getById("d-direkt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Verschachtelung -----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun verschachtelte_transaktionen_commiten_gemeinsam() = runDbTest {
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-aussen"))
|
||||||
|
db.withTransaction { documentationDao.insert(documentationEntity(id = "d-innen")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(2, countRows("documentations"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fehler_in_der_inneren_transaktion_rollt_auch_die_aeussere_zurueck_selbst_wenn_er_gefangen_wird() = runDbTest {
|
||||||
|
// SQLite/Android: Wird eine verschachtelte Transaktion nicht als erfolgreich beendet,
|
||||||
|
// wird die äußere ebenfalls zurückgerollt (SQLiteDatabase.beginTransaction-Doku).
|
||||||
|
seedPatient()
|
||||||
|
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-aussen"))
|
||||||
|
try {
|
||||||
|
db.withTransaction {
|
||||||
|
documentationDao.insert(documentationEntity(id = "d-innen"))
|
||||||
|
error("Fehler innen")
|
||||||
|
}
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// bewusst gefangen: "e" ist unbenutzt, der Fehler soll die äußere Transaktion nicht abbrechen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, countRows("documentations"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Nebenläufigkeit -----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parallele_transaktionen_werden_serialisiert_und_verlieren_keine_schreibvorgaenge() = runDbTest {
|
||||||
|
seedDocumentation()
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
listOf("a", "b", "c").map { prefix ->
|
||||||
|
async(Dispatchers.Default) {
|
||||||
|
repeat(20) { i ->
|
||||||
|
db.withTransaction {
|
||||||
|
photoDao.insert(photoEntity(id = "$prefix-$i", fileName = "$prefix$i.jpg", orderIndex = i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(60, countRows("photos"))
|
||||||
|
assertEquals(60, photoDao.getForDocumentation("d-1").size)
|
||||||
|
}
|
||||||
|
}
|
||||||
+378
@@ -0,0 +1,378 @@
|
|||||||
|
package de.medapp.wunddoku.data.repository
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import de.medapp.wunddoku.data.entity.BlockStateEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.CheckItemStateEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.DocumentationEntity
|
||||||
|
import de.medapp.wunddoku.domain.catalog.DocumentationCatalog
|
||||||
|
import de.medapp.wunddoku.domain.model.CheckItem
|
||||||
|
import de.medapp.wunddoku.domain.model.Documentation
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationStatus
|
||||||
|
import de.medapp.wunddoku.testutil.DuplicateCall
|
||||||
|
import de.medapp.wunddoku.testutil.TestEnvironment
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.firstPlainItem
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import de.medapp.wunddoku.testutil.signatureBitmap
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fortsetzen-Tests: "Dokumentation fortsetzen" erzeugt aus dem Stand einer vorhandenen
|
||||||
|
* Dokumentation eine NEUE; die Quelle bleibt unverändert.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ContinueDocumentationTest {
|
||||||
|
|
||||||
|
private lateinit var env: TestEnvironment
|
||||||
|
private val repo get() = env.documentationRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
env = TestEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
env.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alle Items eines Dokuments als Map definitionId (inkl. Blockbezug) -> Item. */
|
||||||
|
private fun Documentation.itemsByKey(): Map<String, CheckItem> =
|
||||||
|
blocks.flatMap { block -> block.items.map { "${block.definitionId}/${it.definitionId}" to it } }.toMap()
|
||||||
|
|
||||||
|
/** Bereitet eine Quelle mit einigen Eingaben vor und liefert sie frisch geladen zurück. */
|
||||||
|
private suspend fun prepareSourceWithInput(): Documentation {
|
||||||
|
env.insertPatient()
|
||||||
|
val created = repo.createEmpty("patient-1")
|
||||||
|
val firstBlock = created.blocks.first()
|
||||||
|
repo.setChecked(firstBlock.firstPlainItem().checkItemStateId, true)
|
||||||
|
val freeText = firstBlock.items.last()
|
||||||
|
repo.setChecked(freeText.checkItemStateId, true)
|
||||||
|
repo.updateFreeText(freeText.checkItemStateId, "Rötung am Wundrand")
|
||||||
|
return repo.getFullDocumentation(created.id)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Inhalt wird übernommen ----------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_uebernimmt_bloecke_und_optionen_inklusive_haken_und_freitext() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
assertEquals(source.blocks.size, copy.blocks.size)
|
||||||
|
for (sourceBlock in source.blocks) {
|
||||||
|
val copyBlock = copy.blocks.single { it.definitionId == sourceBlock.definitionId }
|
||||||
|
assertEquals(sourceBlock.title, copyBlock.title)
|
||||||
|
assertEquals(sourceBlock.order, copyBlock.order)
|
||||||
|
assertEquals(sourceBlock.isEdited, copyBlock.isEdited)
|
||||||
|
}
|
||||||
|
// Option für Option: gleiche Inhalte (Label, Reihenfolge, Haken, Freitext, Hinweis, strukturierte Eingabe).
|
||||||
|
val sourceItems = source.itemsByKey()
|
||||||
|
val copyItems = copy.itemsByKey()
|
||||||
|
assertEquals(sourceItems.keys, copyItems.keys)
|
||||||
|
for ((key, sourceItem) in sourceItems) {
|
||||||
|
val copyItem = copyItems.getValue(key)
|
||||||
|
assertEquals(key, sourceItem.label, copyItem.label)
|
||||||
|
assertEquals(key, sourceItem.order, copyItem.order)
|
||||||
|
assertEquals(key, sourceItem.checked, copyItem.checked)
|
||||||
|
assertEquals(key, sourceItem.isFreeText, copyItem.isFreeText)
|
||||||
|
assertEquals(key, sourceItem.freeTextValue, copyItem.freeTextValue)
|
||||||
|
assertEquals(key, sourceItem.isStaticHint, copyItem.isStaticHint)
|
||||||
|
assertEquals(key, sourceItem.inputSpec, copyItem.inputSpec)
|
||||||
|
}
|
||||||
|
assertEquals(1, copy.blocks.count { it.isEdited })
|
||||||
|
assertEquals("Rötung am Wundrand", copy.blocks.first().items.last().freeTextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_erzeugt_ausschliesslich_neue_ids() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
assertNotEquals(source.id, copy.id)
|
||||||
|
val sourceBlockIds = source.blocks.map { it.blockStateId }.toSet()
|
||||||
|
val sourceItemIds = source.blocks.flatMap { b -> b.items.map { it.checkItemStateId } }.toSet()
|
||||||
|
assertTrue(copy.blocks.none { it.blockStateId in sourceBlockIds })
|
||||||
|
assertTrue(copy.blocks.flatMap { b -> b.items.map { it.checkItemStateId } }.none { it in sourceItemIds })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_setzt_metadaten_der_neuen_dokumentation_korrekt() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
env.setStatus(source.id, DocumentationStatus.COMPLETED)
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
assertEquals(DocumentationStatus.IN_PROGRESS, copy.status)
|
||||||
|
assertEquals(source.id, copy.continuedFromDocumentationId)
|
||||||
|
assertEquals(source.patientId, copy.patientId)
|
||||||
|
assertEquals(copy.createdAt, copy.updatedAt)
|
||||||
|
assertEquals(DocumentationStatus.COMPLETED, repo.getFullDocumentation(source.id)!!.status)
|
||||||
|
assertEquals(2, env.db.documentationDao().countForPatient("patient-1"))
|
||||||
|
assertTrue(env.patientRepository.hasDocumentations("patient-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Quelle bleibt unverändert / Unabhängigkeit --------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_laesst_die_quelldokumentation_unveraendert() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
|
||||||
|
repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
val sourceAfter = repo.getFullDocumentation(source.id)!!
|
||||||
|
assertEquals(source.status, sourceAfter.status)
|
||||||
|
assertEquals(source.createdAt, sourceAfter.createdAt)
|
||||||
|
assertEquals(source.updatedAt, sourceAfter.updatedAt)
|
||||||
|
assertNull(sourceAfter.continuedFromDocumentationId)
|
||||||
|
assertEquals(source.blocks, sourceAfter.blocks)
|
||||||
|
assertEquals(source.photos, sourceAfter.photos)
|
||||||
|
assertEquals(source.signatures, sourceAfter.signatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aenderungen_an_der_kopie_veraendern_die_quelle_nicht_und_umgekehrt() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
// Kopie: den in der Quelle gesetzten Haken entfernen und Freitext ändern.
|
||||||
|
val copyFirstBlock = copy.blocks.first()
|
||||||
|
repo.setChecked(copyFirstBlock.firstPlainItem().checkItemStateId, false)
|
||||||
|
repo.updateFreeText(copyFirstBlock.items.last().checkItemStateId, "Neuer Text")
|
||||||
|
|
||||||
|
val sourceAfter = repo.getFullDocumentation(source.id)!!
|
||||||
|
assertTrue(sourceAfter.blocks.first().firstPlainItem().checked)
|
||||||
|
assertEquals("Rötung am Wundrand", sourceAfter.blocks.first().items.last().freeTextValue)
|
||||||
|
|
||||||
|
// Quelle: weiteren Haken in einem anderen Block setzen.
|
||||||
|
val otherSourceBlock = source.blocks[1]
|
||||||
|
repo.setChecked(otherSourceBlock.firstPlainItem().checkItemStateId, true)
|
||||||
|
|
||||||
|
val copyAfter = repo.getFullDocumentation(copy.id)!!
|
||||||
|
assertFalse(copyAfter.blocks.single { it.definitionId == otherSourceBlock.definitionId }.isEdited)
|
||||||
|
assertEquals("Neuer Text", copyAfter.blocks.first().items.last().freeTextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Patienten-Snapshot --------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_bildet_den_patienten_snapshot_frisch_aus_dem_aktuellen_patienten() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
val patientDao = env.db.patientDao()
|
||||||
|
patientDao.update(
|
||||||
|
patientDao.getById("patient-1")!!.copy(
|
||||||
|
lastName = "Musterfrau", street = "Neue Straße 7", postalCode = "99999", city = "Neustadt",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
assertEquals("Musterfrau", copy.patientSnapshot.lastName)
|
||||||
|
assertEquals("Neue Straße 7", copy.patientSnapshot.street)
|
||||||
|
assertEquals("99999", copy.patientSnapshot.postalCode)
|
||||||
|
assertEquals("Neustadt", copy.patientSnapshot.city)
|
||||||
|
assertEquals(LocalDate.of(1980, 3, 14), copy.patientSnapshot.birthDate)
|
||||||
|
// Die Quelle behält ihren damaligen Stand.
|
||||||
|
val sourceSnapshot = repo.getFullDocumentation(source.id)!!.patientSnapshot
|
||||||
|
assertEquals("Mustermann", sourceSnapshot.lastName)
|
||||||
|
assertEquals("Musterstraße 1", sourceSnapshot.street)
|
||||||
|
assertEquals("Musterstadt", sourceSnapshot.city)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Historische Snapshots / Katalogversion ------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_bewahrt_historische_titel_labels_und_katalogversion() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val now = 5_000L
|
||||||
|
val historicDocId = "historic-doc"
|
||||||
|
env.db.documentationDao().insert(
|
||||||
|
DocumentationEntity(
|
||||||
|
id = historicDocId, patientId = "patient-1", status = DocumentationStatus.COMPLETED,
|
||||||
|
createdAt = now, updatedAt = now, continuedFromDocumentationId = null, pdfLastGeneratedAt = null,
|
||||||
|
catalogVersion = 1,
|
||||||
|
patientFirstNameSnapshot = "Max", patientLastNameSnapshot = "Mustermann",
|
||||||
|
patientBirthDateSnapshot = "1980-03-14", patientStreetSnapshot = "Musterstraße 1",
|
||||||
|
patientPostalCodeSnapshot = "12345", patientCitySnapshot = "Musterstadt",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
env.db.blockStateDao().insertAll(
|
||||||
|
listOf(BlockStateEntity("hist-block", historicDocId, "gibt_es_nicht_mehr", "Alter Titel", 1, now))
|
||||||
|
)
|
||||||
|
env.db.checkItemStateDao().insertAll(
|
||||||
|
listOf(
|
||||||
|
CheckItemStateEntity("hist-item", "hist-block", "alter_eintrag", "Alter Eintrag", 1, checked = true),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertNotEquals("Voraussetzung: aktuelle Katalogversion weicht ab", 1, DocumentationCatalog.CATALOG_VERSION)
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(historicDocId)
|
||||||
|
|
||||||
|
assertEquals(1, copy.catalogVersion)
|
||||||
|
val block = copy.blocks.single()
|
||||||
|
assertEquals("gibt_es_nicht_mehr", block.definitionId)
|
||||||
|
assertEquals("Alter Titel", block.title)
|
||||||
|
assertEquals("Alter Eintrag", block.items.single().label)
|
||||||
|
assertTrue(block.items.single().checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Unterschriften ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_kopiert_keine_unterschriften() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
repo.saveSignature(source.id, "Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
assertTrue(copy.signatures.isEmpty())
|
||||||
|
assertEquals(1, repo.getFullDocumentation(source.id)!!.signatures.size)
|
||||||
|
assertEquals(1, env.signatureStorage.saved.size) // keine zusätzliche Datei angelegt
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fotos ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_kopiert_fotos_standardmaessig_als_eigenstaendige_dateien() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
val p1 = repo.addPhoto(source.id, Uri.parse("content://test/1"))
|
||||||
|
val p2 = repo.addPhoto(source.id, Uri.parse("content://test/2"))
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
assertEquals(listOf(0, 1), copy.photos.map { it.orderIndex })
|
||||||
|
assertTrue(copy.photos.none { it.id == p1.id || it.id == p2.id })
|
||||||
|
assertTrue("Dateien müssen physisch eigenständig sein", copy.photos.none { it.fileName == p1.fileName || it.fileName == p2.fileName })
|
||||||
|
assertTrue(copy.photos.all { it.documentationId == copy.id })
|
||||||
|
assertEquals(
|
||||||
|
setOf(
|
||||||
|
DuplicateCall(source.id, p1.fileName, copy.id),
|
||||||
|
DuplicateCall(source.id, p2.fileName, copy.id),
|
||||||
|
),
|
||||||
|
env.photoStorage.duplicated.toSet(),
|
||||||
|
)
|
||||||
|
assertEquals(2, env.photoStorage.duplicated.size)
|
||||||
|
// Quelle behält ihre Fotos.
|
||||||
|
assertEquals(listOf(p1.fileName, p2.fileName), repo.getFullDocumentation(source.id)!!.photos.map { it.fileName })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_mit_copyPhotos_false_uebernimmt_keine_fotos() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
repo.addPhoto(source.id, Uri.parse("content://test/1"))
|
||||||
|
|
||||||
|
val copy = repo.continueDocumentation(source.id, copyPhotos = false)
|
||||||
|
|
||||||
|
assertTrue(copy.photos.isEmpty())
|
||||||
|
assertTrue(env.photoStorage.duplicated.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun foto_loeschen_in_der_kopie_betrifft_die_quelle_nicht() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
repo.addPhoto(source.id, Uri.parse("content://test/1"))
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
repo.removePhoto(copy.photos.single().id)
|
||||||
|
|
||||||
|
assertTrue(repo.getFullDocumentation(copy.id)!!.photos.isEmpty())
|
||||||
|
assertEquals(1, repo.getFullDocumentation(source.id)!!.photos.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fehlerfälle / Atomarität --------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_einer_unbekannten_dokumentation_wirft_und_legt_nichts_an() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val error = expectThrows<IllegalStateException> { repo.continueDocumentation("unbekannt") }
|
||||||
|
|
||||||
|
assertTrue(error.message.orEmpty().contains("unbekannt"))
|
||||||
|
assertTrue(env.db.documentationDao().observeAll().first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fortsetzen_ist_atomar_ein_fehler_beim_foto_kopieren_hinterlaesst_keine_halbe_dokumentation() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
repo.addPhoto(source.id, Uri.parse("content://test/1"))
|
||||||
|
repo.addPhoto(source.id, Uri.parse("content://test/2"))
|
||||||
|
env.photoStorage.failOnDuplicateCall = 2 // zweites Foto schlägt fehl
|
||||||
|
|
||||||
|
expectThrows<IOException> { repo.continueDocumentation(source.id) }
|
||||||
|
|
||||||
|
val all = env.db.documentationDao().observeAll().first()
|
||||||
|
assertEquals(listOf(source.id), all.map { it.id })
|
||||||
|
assertEquals(1, env.db.documentationDao().countForPatient("patient-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loeschen_der_quelle_laesst_die_fortgesetzte_dokumentation_bestehen() = runDbTest {
|
||||||
|
val source = prepareSourceWithInput()
|
||||||
|
repo.addPhoto(source.id, Uri.parse("content://test/1"))
|
||||||
|
val copy = repo.continueDocumentation(source.id)
|
||||||
|
|
||||||
|
repo.delete(source.id)
|
||||||
|
|
||||||
|
val copyAfter = repo.getFullDocumentation(copy.id)!!
|
||||||
|
assertNull("Verweis auf die gelöschte Quelle wird auf null gesetzt", copyAfter.continuedFromDocumentationId)
|
||||||
|
assertEquals(copy.blocks.size, copyAfter.blocks.size)
|
||||||
|
assertEquals(1, copyAfter.photos.size)
|
||||||
|
assertEquals(listOf(source.id), env.photoStorage.deletedAll) // nur der Ordner der Quelle
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mehrfaches_fortsetzen_bildet_eine_kette() = runDbTest {
|
||||||
|
val first = prepareSourceWithInput()
|
||||||
|
|
||||||
|
val second = repo.continueDocumentation(first.id)
|
||||||
|
val third = repo.continueDocumentation(second.id)
|
||||||
|
|
||||||
|
assertEquals(first.id, second.continuedFromDocumentationId)
|
||||||
|
assertEquals(second.id, third.continuedFromDocumentationId)
|
||||||
|
assertEquals(1, third.blocks.count { it.isEdited })
|
||||||
|
assertEquals(3, env.db.documentationDao().countForPatient("patient-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- continueLatestForPatient --------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun continueLatestForPatient_liefert_null_wenn_der_patient_noch_keine_dokumentation_hat() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
assertNull(repo.continueLatestForPatient("patient-1"))
|
||||||
|
assertTrue(env.db.documentationDao().observeAll().first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun continueLatestForPatient_setzt_die_zuletzt_erstellte_dokumentation_des_patienten_fort() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
env.insertPatient(id = "patient-2", firstName = "Erika", lastName = "Musterfrau")
|
||||||
|
val older = repo.createEmpty("patient-1")
|
||||||
|
val newer = repo.createEmpty("patient-1")
|
||||||
|
val otherPatientsDoc = repo.createEmpty("patient-2")
|
||||||
|
env.setCreatedAt(older.id, 1_000L)
|
||||||
|
env.setCreatedAt(newer.id, 2_000L)
|
||||||
|
env.setCreatedAt(otherPatientsDoc.id, 3_000L) // neuer, aber anderer Patient
|
||||||
|
repo.setChecked(newer.blocks.first().firstPlainItem().checkItemStateId, true)
|
||||||
|
|
||||||
|
val copy = repo.continueLatestForPatient("patient-1")!!
|
||||||
|
|
||||||
|
assertEquals(newer.id, copy.continuedFromDocumentationId)
|
||||||
|
assertEquals("patient-1", copy.patientId)
|
||||||
|
assertEquals(1, copy.blocks.count { it.isEdited })
|
||||||
|
}
|
||||||
|
}
|
||||||
+545
@@ -0,0 +1,545 @@
|
|||||||
|
package de.medapp.wunddoku.data.repository
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import de.medapp.wunddoku.domain.catalog.DocumentationCatalog
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationStatus
|
||||||
|
import de.medapp.wunddoku.domain.model.InputSpecCodec
|
||||||
|
import de.medapp.wunddoku.pdf.PdfExportError
|
||||||
|
import de.medapp.wunddoku.pdf.PdfExportResult
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesResult
|
||||||
|
import de.medapp.wunddoku.testutil.TestEnvironment
|
||||||
|
import de.medapp.wunddoku.testutil.blankBitmap
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.firstPlainItem
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import de.medapp.wunddoku.testutil.signatureBitmap
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.File
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository-Tests für [DocumentationRepository] gegen eine echte In-Memory-Room-Datenbank.
|
||||||
|
* Dateispeicher und PDF-Erzeugung sind durch Fakes ersetzt (s. testutil/Fakes.kt).
|
||||||
|
* "Fortsetzen" wird separat in [ContinueDocumentationTest] getestet.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DocumentationRepositoryTest {
|
||||||
|
|
||||||
|
private lateinit var env: TestEnvironment
|
||||||
|
private val repo get() = env.documentationRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
env = TestEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
env.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- createEmpty ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_legt_neue_dokumentation_im_status_in_bearbeitung_an() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
assertEquals("patient-1", doc.patientId)
|
||||||
|
assertEquals(DocumentationStatus.IN_PROGRESS, doc.status)
|
||||||
|
assertNull(doc.continuedFromDocumentationId)
|
||||||
|
assertEquals(DocumentationCatalog.CATALOG_VERSION, doc.catalogVersion)
|
||||||
|
assertEquals(doc.createdAt, doc.updatedAt)
|
||||||
|
assertTrue(doc.photos.isEmpty())
|
||||||
|
assertTrue(doc.signatures.isEmpty())
|
||||||
|
assertEquals(doc.id, repo.getFullDocumentation(doc.id)?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_uebernimmt_den_patienten_snapshot() = runDbTest {
|
||||||
|
env.insertPatient(
|
||||||
|
firstName = "Erika", lastName = "Musterfrau", birthDate = "1975-12-01",
|
||||||
|
street = "Hauptstraße 5", postalCode = "54321", city = "Beispielstadt",
|
||||||
|
)
|
||||||
|
|
||||||
|
val snapshot = repo.createEmpty("patient-1").patientSnapshot
|
||||||
|
|
||||||
|
assertEquals("Erika", snapshot.firstName)
|
||||||
|
assertEquals("Musterfrau", snapshot.lastName)
|
||||||
|
assertEquals(LocalDate.of(1975, 12, 1), snapshot.birthDate)
|
||||||
|
assertEquals("Hauptstraße 5", snapshot.street)
|
||||||
|
assertEquals("54321", snapshot.postalCode)
|
||||||
|
assertEquals("Beispielstadt", snapshot.city)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_erzeugt_pro_katalogblock_einen_block_mit_titel_und_reihenfolge_snapshot() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
assertEquals(DocumentationCatalog.blocks.size, doc.blocks.size)
|
||||||
|
for (def in DocumentationCatalog.blocks) {
|
||||||
|
val block = doc.blocks.single { it.definitionId == def.id }
|
||||||
|
assertEquals(def.title, block.title)
|
||||||
|
assertEquals(def.order, block.order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_haengt_pro_block_die_generische_freitext_option_als_letztes_element_an() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
for (def in DocumentationCatalog.blocks) {
|
||||||
|
val block = doc.blocks.single { it.definitionId == def.id }
|
||||||
|
assertEquals(def.items.size + 1, block.items.size)
|
||||||
|
|
||||||
|
val generic = block.items.last()
|
||||||
|
assertEquals(def.freeTextDefinitionId, generic.definitionId)
|
||||||
|
assertEquals(DocumentationCatalog.FREE_TEXT_LABEL, generic.label)
|
||||||
|
assertEquals((def.items.maxOfOrNull { it.order } ?: 0) + 1, generic.order)
|
||||||
|
assertTrue(generic.isFreeText)
|
||||||
|
assertFalse(generic.isStaticHint)
|
||||||
|
assertFalse(generic.checked)
|
||||||
|
assertEquals("", generic.freeTextValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_uebernimmt_katalogeintraege_1_zu_1_und_hinweistexte_starten_angehakt() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
for (def in DocumentationCatalog.blocks) {
|
||||||
|
val block = doc.blocks.single { it.definitionId == def.id }
|
||||||
|
for (itemDef in def.items) {
|
||||||
|
val state = block.items.single { it.definitionId == itemDef.id }
|
||||||
|
assertEquals(itemDef.label, state.label)
|
||||||
|
assertEquals(itemDef.order, state.order)
|
||||||
|
assertEquals(itemDef.isFreeText, state.isFreeText)
|
||||||
|
assertEquals(itemDef.isStaticHint, state.isStaticHint)
|
||||||
|
// Nur Hinweistexte starten angehakt, alles andere unangehakt/leer.
|
||||||
|
assertEquals(itemDef.isStaticHint, state.checked)
|
||||||
|
assertEquals("", state.freeTextValue)
|
||||||
|
val expectedSpec = itemDef.inputSpec?.let(InputSpecCodec::encode) ?: ""
|
||||||
|
assertEquals(expectedSpec, state.inputSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_ergibt_keinen_bearbeiteten_block_auch_nicht_durch_hinweistexte() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
assertTrue(doc.blocks.none { it.isEdited })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_vergibt_fuer_jede_dokumentation_neue_ids() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
|
||||||
|
val first = repo.createEmpty("patient-1")
|
||||||
|
val second = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
assertTrue(first.id != second.id)
|
||||||
|
val allBlockIds = (first.blocks + second.blocks).map { it.blockStateId }
|
||||||
|
assertEquals(allBlockIds.size, allBlockIds.toSet().size)
|
||||||
|
assertEquals(2, env.db.documentationDao().countForPatient("patient-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createEmpty_mit_unbekanntem_patienten_wirft_und_legt_nichts_an() = runDbTest {
|
||||||
|
val error = expectThrows<IllegalStateException> { repo.createEmpty("gibt-es-nicht") }
|
||||||
|
|
||||||
|
assertTrue(error.message.orEmpty().contains("gibt-es-nicht"))
|
||||||
|
assertTrue(env.db.documentationDao().observeAll().first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lesen / Liste / Suche -----------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getFullDocumentation_liefert_null_fuer_unbekannte_id() = runDbTest {
|
||||||
|
assertNull(repo.getFullDocumentation("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAll_liefert_neueste_dokumentation_zuerst_mit_snapshot_namen() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val older = repo.createEmpty("patient-1")
|
||||||
|
val newer = repo.createEmpty("patient-1")
|
||||||
|
env.setCreatedAt(older.id, 1_000L)
|
||||||
|
env.setCreatedAt(newer.id, 2_000L)
|
||||||
|
|
||||||
|
val list = repo.observeAll().first()
|
||||||
|
|
||||||
|
assertEquals(listOf(newer.id, older.id), list.map { it.id })
|
||||||
|
assertEquals("Max Mustermann", list.first().patientFullName)
|
||||||
|
assertEquals(DocumentationStatus.IN_PROGRESS, list.first().status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_sucht_gegen_den_aktuellen_patientennamen_zeigt_aber_den_snapshot_namen() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val patientDao = env.db.patientDao()
|
||||||
|
patientDao.update(patientDao.getById("patient-1")!!.copy(lastName = "Musterfrau"))
|
||||||
|
|
||||||
|
val found = repo.search("Musterfrau").first()
|
||||||
|
|
||||||
|
assertEquals(listOf(doc.id), found.map { it.id })
|
||||||
|
assertEquals("Max Mustermann", found.single().patientFullName)
|
||||||
|
assertTrue(repo.search("Mustermann").first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_findet_auch_ueber_vornamen_und_geburtsdatum() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
assertEquals(listOf(doc.id), repo.search("Ma").first().map { it.id })
|
||||||
|
assertEquals(listOf(doc.id), repo.search("1980-03").first().map { it.id })
|
||||||
|
assertTrue(repo.search("Zzz").first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Autosave / Blockstatus ----------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setChecked_persistiert_und_macht_den_block_bearbeitet() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val block = doc.blocks.first()
|
||||||
|
val item = block.firstPlainItem()
|
||||||
|
|
||||||
|
repo.setChecked(item.checkItemStateId, true)
|
||||||
|
|
||||||
|
val reloaded = repo.getFullDocumentation(doc.id)!!
|
||||||
|
val reloadedBlock = reloaded.blocks.single { it.blockStateId == block.blockStateId }
|
||||||
|
assertTrue(reloadedBlock.items.single { it.checkItemStateId == item.checkItemStateId }.checked)
|
||||||
|
assertTrue(reloadedBlock.isEdited)
|
||||||
|
assertEquals(1, reloaded.blocks.count { it.isEdited })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setChecked_false_nimmt_den_bearbeitet_status_wieder_zurueck() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val block = doc.blocks.first()
|
||||||
|
val item = block.firstPlainItem()
|
||||||
|
repo.setChecked(item.checkItemStateId, true)
|
||||||
|
|
||||||
|
repo.setChecked(item.checkItemStateId, false)
|
||||||
|
|
||||||
|
val reloaded = repo.getFullDocumentation(doc.id)!!
|
||||||
|
assertTrue(reloaded.blocks.none { it.isEdited })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun freitext_wird_gespeichert_und_zaehlt_erst_mit_haken_als_bearbeitung() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val block = doc.blocks.first()
|
||||||
|
val freeText = block.items.last()
|
||||||
|
val text = "Größe 3,5 × 2 cm\nzweite Zeile – Rötung"
|
||||||
|
|
||||||
|
repo.updateFreeText(freeText.checkItemStateId, text)
|
||||||
|
|
||||||
|
val textOnly = repo.getFullDocumentation(doc.id)!!
|
||||||
|
assertEquals(text, textOnly.blocks.single { it.blockStateId == block.blockStateId }.items.last().freeTextValue)
|
||||||
|
assertTrue("Freitext ohne Haken darf den Block nicht bearbeitet machen", textOnly.blocks.none { it.isEdited })
|
||||||
|
|
||||||
|
repo.setChecked(freeText.checkItemStateId, true)
|
||||||
|
|
||||||
|
val checked = repo.getFullDocumentation(doc.id)!!
|
||||||
|
val reloadedBlock = checked.blocks.single { it.blockStateId == block.blockStateId }
|
||||||
|
assertTrue(reloadedBlock.items.last().checked)
|
||||||
|
assertEquals(text, reloadedBlock.items.last().freeTextValue)
|
||||||
|
assertTrue(reloadedBlock.isEdited)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aenderungen_in_einer_dokumentation_beruehren_eine_andere_nicht() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val first = repo.createEmpty("patient-1")
|
||||||
|
val second = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
repo.setChecked(first.blocks.first().firstPlainItem().checkItemStateId, true)
|
||||||
|
|
||||||
|
assertTrue(repo.getFullDocumentation(second.id)!!.blocks.none { it.isEdited })
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fotos ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addPhoto_vergibt_fortlaufenden_orderIndex_und_speichert_den_storage_dateinamen() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val uri1 = Uri.parse("content://test/1")
|
||||||
|
val uri2 = Uri.parse("content://test/2")
|
||||||
|
|
||||||
|
val p1 = repo.addPhoto(doc.id, uri1)
|
||||||
|
val p2 = repo.addPhoto(doc.id, uri2)
|
||||||
|
|
||||||
|
assertEquals(0, p1.orderIndex)
|
||||||
|
assertEquals(1, p2.orderIndex)
|
||||||
|
assertEquals(listOf(doc.id to uri1, doc.id to uri2), env.photoStorage.imported)
|
||||||
|
val photos = repo.getFullDocumentation(doc.id)!!.photos
|
||||||
|
assertEquals(listOf(p1.fileName, p2.fileName), photos.map { it.fileName })
|
||||||
|
assertEquals(listOf(0, 1), photos.map { it.orderIndex })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removePhoto_loescht_db_zeile_und_datei_und_vergibt_orderIndex_nicht_neu() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val p1 = repo.addPhoto(doc.id, Uri.parse("content://test/1"))
|
||||||
|
val p2 = repo.addPhoto(doc.id, Uri.parse("content://test/2"))
|
||||||
|
|
||||||
|
repo.removePhoto(p1.id)
|
||||||
|
|
||||||
|
assertEquals(listOf(p2.id), repo.getFullDocumentation(doc.id)!!.photos.map { it.id })
|
||||||
|
assertEquals(listOf(doc.id to p1.fileName), env.photoStorage.deleted)
|
||||||
|
val p3 = repo.addPhoto(doc.id, Uri.parse("content://test/3"))
|
||||||
|
assertEquals(2, p3.orderIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removePhoto_mit_unbekannter_id_tut_nichts() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
repo.removePhoto("unbekannt")
|
||||||
|
|
||||||
|
assertTrue(env.photoStorage.deleted.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removePhoto_entfernt_die_db_zeile_auch_wenn_die_datei_nicht_geloescht_werden_kann() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val photo = repo.addPhoto(doc.id, Uri.parse("content://test/1"))
|
||||||
|
env.photoStorage.failOnDelete = true
|
||||||
|
|
||||||
|
repo.removePhoto(photo.id)
|
||||||
|
|
||||||
|
assertTrue(repo.getFullDocumentation(doc.id)!!.photos.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Signaturen ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveSignature_trimmt_den_namen_und_vergibt_fortlaufenden_orderIndex() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
val first = repo.saveSignature(doc.id, " Anna Schmidt ", signatureBitmap())
|
||||||
|
val second = repo.saveSignature(doc.id, "Bernd Beispiel", signatureBitmap())
|
||||||
|
|
||||||
|
assertEquals("Anna Schmidt", first.signerName)
|
||||||
|
assertEquals(0, first.orderIndex)
|
||||||
|
assertEquals(1, second.orderIndex)
|
||||||
|
val signatures = repo.getFullDocumentation(doc.id)!!.signatures
|
||||||
|
assertEquals(listOf("Anna Schmidt", "Bernd Beispiel"), signatures.map { it.signerName })
|
||||||
|
assertEquals(listOf(first.fileName, second.fileName), signatures.map { it.fileName })
|
||||||
|
assertEquals(listOf(doc.id to first.fileName, doc.id to second.fileName), env.signatureStorage.saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveSignature_aktualisiert_das_wundversorger_register() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
repo.saveSignature(doc.id, "Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
assertEquals("Anna Schmidt", env.woundCarerRepository.getMostRecentlyUsed()?.name)
|
||||||
|
assertEquals(1, env.woundCarerSignatureStorage.saved.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveSignature_mit_leerem_namen_wirft_und_hinterlaesst_keine_datei() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
expectThrows<IllegalArgumentException> { repo.saveSignature(doc.id, " ", signatureBitmap()) }
|
||||||
|
|
||||||
|
assertTrue(env.signatureStorage.saved.isEmpty())
|
||||||
|
assertTrue(env.db.signatureDao().getForDocumentation(doc.id).isEmpty())
|
||||||
|
assertNull(env.woundCarerRepository.getMostRecentlyUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveSignature_mit_leerer_zeichnung_wirft_und_hinterlaesst_keine_datei() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
expectThrows<IllegalArgumentException> { repo.saveSignature(doc.id, "Anna Schmidt", blankBitmap()) }
|
||||||
|
|
||||||
|
assertTrue(env.signatureStorage.saved.isEmpty())
|
||||||
|
assertTrue(env.db.signatureDao().getForDocumentation(doc.id).isEmpty())
|
||||||
|
assertNull(env.woundCarerRepository.getMostRecentlyUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeSignature_loescht_db_zeile_und_datei() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val signature = repo.saveSignature(doc.id, "Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
repo.removeSignature(signature.id)
|
||||||
|
|
||||||
|
assertTrue(repo.getFullDocumentation(doc.id)!!.signatures.isEmpty())
|
||||||
|
assertEquals(listOf(doc.id to signature.fileName), env.signatureStorage.deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeSignature_mit_unbekannter_id_tut_nichts_und_toleriert_storage_fehler() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
|
||||||
|
repo.removeSignature("unbekannt")
|
||||||
|
assertTrue(env.signatureStorage.deleted.isEmpty())
|
||||||
|
|
||||||
|
val signature = repo.saveSignature(doc.id, "Anna Schmidt", signatureBitmap())
|
||||||
|
env.signatureStorage.failOnDelete = true
|
||||||
|
repo.removeSignature(signature.id)
|
||||||
|
|
||||||
|
assertTrue(repo.getFullDocumentation(doc.id)!!.signatures.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Löschen -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_entfernt_dokumentation_samt_bloecken_fotos_und_signaturen_sowie_dateiordner() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
repo.addPhoto(doc.id, Uri.parse("content://test/1"))
|
||||||
|
repo.saveSignature(doc.id, "Anna Schmidt", signatureBitmap())
|
||||||
|
val blockStateIds = doc.blocks.map { it.blockStateId }
|
||||||
|
|
||||||
|
repo.delete(doc.id)
|
||||||
|
|
||||||
|
assertNull(repo.getFullDocumentation(doc.id))
|
||||||
|
assertTrue(env.db.blockStateDao().getForDocumentation(doc.id).isEmpty())
|
||||||
|
assertTrue(env.db.photoDao().getForDocumentation(doc.id).isEmpty())
|
||||||
|
assertTrue(env.db.signatureDao().getForDocumentation(doc.id).isEmpty())
|
||||||
|
for (blockStateId in blockStateIds) {
|
||||||
|
assertTrue(env.db.checkItemStateDao().getForBlockState(blockStateId).isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(listOf(doc.id), env.photoStorage.deletedAll)
|
||||||
|
assertEquals(listOf(doc.id), env.signatureStorage.deletedAll)
|
||||||
|
assertNotNull("Der Patient darf nicht mitgelöscht werden", env.patientRepository.getById("patient-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_beruehrt_das_wundversorger_register_nicht() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
repo.saveSignature(doc.id, "Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
repo.delete(doc.id)
|
||||||
|
|
||||||
|
assertEquals("Anna Schmidt", env.woundCarerRepository.getMostRecentlyUsed()?.name)
|
||||||
|
assertTrue(env.woundCarerSignatureStorage.deletedAll.isEmpty())
|
||||||
|
assertTrue(env.woundCarerSignatureStorage.deleted.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_mit_unbekannter_id_tut_nichts() = runDbTest {
|
||||||
|
repo.delete("unbekannt")
|
||||||
|
|
||||||
|
assertTrue(env.photoStorage.deletedAll.isEmpty())
|
||||||
|
assertTrue(env.signatureStorage.deletedAll.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_entfernt_die_dokumentation_auch_wenn_dateiordner_nicht_geloescht_werden_koennen() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
env.photoStorage.failOnDeleteAll = true
|
||||||
|
env.signatureStorage.failOnDeleteAll = true
|
||||||
|
|
||||||
|
repo.delete(doc.id)
|
||||||
|
|
||||||
|
assertNull(repo.getFullDocumentation(doc.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PDF-Export ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exportPdf_fuer_unbekannte_dokumentation_liefert_fachlichen_fehler_ohne_exporter_aufruf() = runDbTest {
|
||||||
|
val result = repo.exportPdf("unbekannt")
|
||||||
|
|
||||||
|
assertEquals(PdfExportResult.Failure(PdfExportError.DocumentationNotFound), result)
|
||||||
|
assertTrue(env.pdfExporter.models.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exportPdf_uebergibt_nur_bearbeitete_bloecke_und_den_schemakonformen_dateinamen() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val block = doc.blocks.first()
|
||||||
|
val item = block.firstPlainItem()
|
||||||
|
repo.setChecked(item.checkItemStateId, true)
|
||||||
|
|
||||||
|
val result = repo.exportPdf(doc.id)
|
||||||
|
|
||||||
|
assertTrue(result is PdfExportResult.Success)
|
||||||
|
val model = env.pdfExporter.models.single()
|
||||||
|
assertEquals(doc.id, model.documentationId)
|
||||||
|
assertEquals(listOf(block.title), model.blocks.map { it.title })
|
||||||
|
assertTrue(model.blocks.single().items.any { it.label == item.label })
|
||||||
|
val createdDate = doc.createdAt.atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
|
assertEquals("${createdDate}_Max_Mustermann_1980-03-14.pdf", model.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exportPdf_verwendet_den_snapshot_und_nicht_den_aktuellen_patientendatensatz() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val patientDao = env.db.patientDao()
|
||||||
|
patientDao.update(patientDao.getById("patient-1")!!.copy(lastName = "Musterfrau", city = "Neustadt"))
|
||||||
|
|
||||||
|
repo.exportPdf(doc.id)
|
||||||
|
|
||||||
|
val model = env.pdfExporter.models.single()
|
||||||
|
assertEquals("Max Mustermann", model.patientFullName)
|
||||||
|
assertEquals("Musterstraße 1, 12345 Musterstadt", model.patientDisplayAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exportPdf_reicht_das_exporter_ergebnis_unveraendert_durch() = runDbTest {
|
||||||
|
env.insertPatient()
|
||||||
|
val doc = repo.createEmpty("patient-1")
|
||||||
|
val failure = PdfExportResult.Failure(PdfExportError.WriteFailed("Speicher voll"))
|
||||||
|
env.pdfExporter.result = { failure }
|
||||||
|
|
||||||
|
assertEquals(failure, repo.exportPdf(doc.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun convertPdfToImages_delegiert_an_den_converter() = runDbTest {
|
||||||
|
val pdf = File("/fake/pdfs/test.pdf")
|
||||||
|
env.pdfToImagesConverter.result = PdfToImagesResult.Failure.FileNotFound
|
||||||
|
|
||||||
|
val result = repo.convertPdfToImages("doc-1", pdf)
|
||||||
|
|
||||||
|
assertEquals(PdfToImagesResult.Failure.FileNotFound, result)
|
||||||
|
assertEquals(listOf("doc-1" to pdf), env.pdfToImagesConverter.calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package de.medapp.wunddoku.data.repository
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import de.medapp.wunddoku.domain.model.Patient
|
||||||
|
import de.medapp.wunddoku.testutil.TestEnvironment
|
||||||
|
import de.medapp.wunddoku.testutil.expectThrows
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/** Repository-Tests für [PatientRepository] gegen eine echte In-Memory-Room-Datenbank. */
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PatientRepositoryTest {
|
||||||
|
|
||||||
|
private lateinit var env: TestEnvironment
|
||||||
|
private val repo get() = env.patientRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
env = TestEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
env.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patient(
|
||||||
|
id: String = "p-1",
|
||||||
|
firstName: String = "Max",
|
||||||
|
lastName: String = "Mustermann",
|
||||||
|
birthDate: LocalDate = LocalDate.of(1980, 3, 14),
|
||||||
|
) = Patient(
|
||||||
|
id = id,
|
||||||
|
firstName = firstName,
|
||||||
|
lastName = lastName,
|
||||||
|
birthDate = birthDate,
|
||||||
|
street = "Musterstraße 1",
|
||||||
|
postalCode = "12345",
|
||||||
|
city = "Musterstadt",
|
||||||
|
phone1 = "0123 456789",
|
||||||
|
phone2 = "0987 654321",
|
||||||
|
phone3 = null,
|
||||||
|
createdAt = Instant.EPOCH,
|
||||||
|
updatedAt = Instant.EPOCH,
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- upsert / getById ----------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upsert_legt_neuen_patienten_an_und_setzt_zeitstempel_zentral() = runDbTest {
|
||||||
|
val before = Instant.now().toEpochMilli()
|
||||||
|
|
||||||
|
val saved = repo.upsert(patient())
|
||||||
|
|
||||||
|
assertEquals(saved.createdAt, saved.updatedAt)
|
||||||
|
assertTrue(saved.createdAt.toEpochMilli() >= before)
|
||||||
|
val loaded = repo.getById("p-1")!!
|
||||||
|
assertEquals("Max", loaded.firstName)
|
||||||
|
assertEquals("Mustermann", loaded.lastName)
|
||||||
|
assertEquals(LocalDate.of(1980, 3, 14), loaded.birthDate)
|
||||||
|
assertEquals("Musterstraße 1", loaded.street)
|
||||||
|
assertEquals("12345", loaded.postalCode)
|
||||||
|
assertEquals("Musterstadt", loaded.city)
|
||||||
|
assertEquals("0123 456789", loaded.phone1)
|
||||||
|
assertEquals("0987 654321", loaded.phone2)
|
||||||
|
assertNull(loaded.phone3)
|
||||||
|
// Die Datenbank speichert Millisekunden, upsert() liefert Instant.now() mit höherer Genauigkeit.
|
||||||
|
assertEquals(saved.createdAt.toEpochMilli(), loaded.createdAt.toEpochMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upsert_aktualisiert_bestehenden_patienten_und_behaelt_createdAt() = runDbTest {
|
||||||
|
val first = repo.upsert(patient())
|
||||||
|
|
||||||
|
// createdAt im übergebenen Objekt wird bewusst ignoriert (Instant.EPOCH aus dem Helper).
|
||||||
|
val second = repo.upsert(patient(lastName = "Musterfrau"))
|
||||||
|
|
||||||
|
assertEquals(first.createdAt.toEpochMilli(), second.createdAt.toEpochMilli())
|
||||||
|
assertTrue(second.updatedAt.toEpochMilli() >= first.updatedAt.toEpochMilli())
|
||||||
|
val loaded = repo.getById("p-1")!!
|
||||||
|
assertEquals("Musterfrau", loaded.lastName)
|
||||||
|
assertEquals(first.createdAt.toEpochMilli(), loaded.createdAt.toEpochMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun upsert_erzeugt_bei_unterschiedlichen_ids_getrennte_datensaetze() = runDbTest {
|
||||||
|
repo.upsert(patient(id = "p-1"))
|
||||||
|
repo.upsert(patient(id = "p-2", firstName = "Erika", lastName = "Musterfrau"))
|
||||||
|
|
||||||
|
assertEquals("Max", repo.getById("p-1")!!.firstName)
|
||||||
|
assertEquals("Erika", repo.getById("p-2")!!.firstName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getById_liefert_null_fuer_unbekannte_id() = runDbTest {
|
||||||
|
assertNull(repo.getById("unbekannt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun newId_liefert_gueltige_und_eindeutige_uuids() {
|
||||||
|
val a = repo.newId()
|
||||||
|
val b = repo.newId()
|
||||||
|
|
||||||
|
assertEquals(a, UUID.fromString(a).toString())
|
||||||
|
assertTrue(a != b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- findExisting --------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findExisting_trifft_nur_bei_gleichem_namen_und_geburtsdatum() = runDbTest {
|
||||||
|
repo.upsert(patient())
|
||||||
|
|
||||||
|
assertEquals("p-1", repo.findExisting("Max", "Mustermann", LocalDate.of(1980, 3, 14))?.id)
|
||||||
|
assertNull(repo.findExisting("Max", "Mustermann", LocalDate.of(1980, 3, 15)))
|
||||||
|
assertNull(repo.findExisting("Moritz", "Mustermann", LocalDate.of(1980, 3, 14)))
|
||||||
|
assertNull(repo.findExisting("Max", "Musterfrau", LocalDate.of(1980, 3, 14)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dokumentationen / Löschen -------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasDocumentations_ist_erst_nach_dem_anlegen_einer_dokumentation_true() = runDbTest {
|
||||||
|
repo.upsert(patient())
|
||||||
|
assertFalse(repo.hasDocumentations("p-1"))
|
||||||
|
|
||||||
|
env.documentationRepository.createEmpty("p-1")
|
||||||
|
|
||||||
|
assertTrue(repo.hasDocumentations("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_entfernt_patienten_ohne_dokumentation() = runDbTest {
|
||||||
|
repo.upsert(patient())
|
||||||
|
|
||||||
|
repo.delete("p-1")
|
||||||
|
|
||||||
|
assertNull(repo.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_wird_bei_vorhandener_dokumentation_von_der_datenbank_verhindert() = runDbTest {
|
||||||
|
repo.upsert(patient())
|
||||||
|
val doc = env.documentationRepository.createEmpty("p-1")
|
||||||
|
|
||||||
|
expectThrows<SQLiteConstraintException> { repo.delete("p-1") }
|
||||||
|
|
||||||
|
assertNotNull(repo.getById("p-1"))
|
||||||
|
assertNotNull(env.documentationRepository.getFullDocumentation(doc.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delete_mit_unbekannter_id_tut_nichts() = runDbTest {
|
||||||
|
repo.upsert(patient())
|
||||||
|
|
||||||
|
repo.delete("unbekannt")
|
||||||
|
|
||||||
|
assertNotNull(repo.getById("p-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Liste / Suche -------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAll_sortiert_nach_nachname_dann_vorname() = runDbTest {
|
||||||
|
repo.upsert(patient(id = "p-1", firstName = "Max", lastName = "Müller"))
|
||||||
|
repo.upsert(patient(id = "p-2", firstName = "Anna", lastName = "Müller"))
|
||||||
|
repo.upsert(patient(id = "p-3", firstName = "Zoe", lastName = "Adler"))
|
||||||
|
|
||||||
|
val list = repo.observeAll().first()
|
||||||
|
|
||||||
|
assertEquals(listOf("p-3", "p-2", "p-1"), list.map { it.id })
|
||||||
|
assertEquals("Zoe Adler", list.first().fullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun observeAll_zeigt_datum_der_letzten_dokumentation_oder_null() = runDbTest {
|
||||||
|
repo.upsert(patient(id = "p-1"))
|
||||||
|
repo.upsert(patient(id = "p-2", firstName = "Erika", lastName = "Musterfrau"))
|
||||||
|
val older = env.documentationRepository.createEmpty("p-1")
|
||||||
|
val newer = env.documentationRepository.createEmpty("p-1")
|
||||||
|
env.setCreatedAt(older.id, 1_000L)
|
||||||
|
env.setCreatedAt(newer.id, 2_000L)
|
||||||
|
|
||||||
|
val byId = repo.observeAll().first().associateBy { it.id }
|
||||||
|
|
||||||
|
assertEquals(Instant.ofEpochMilli(2_000L), byId.getValue("p-1").lastDocumentationAt)
|
||||||
|
assertNull(byId.getValue("p-2").lastDocumentationAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_findet_ueber_nachname_vorname_und_geburtsdatum() = runDbTest {
|
||||||
|
repo.upsert(patient(id = "p-1", firstName = "Max", lastName = "Mustermann"))
|
||||||
|
repo.upsert(patient(id = "p-2", firstName = "Erika", lastName = "Musterfrau", birthDate = LocalDate.of(1975, 12, 1)))
|
||||||
|
|
||||||
|
// Beide Nachnamen beginnen mit "Muster": sortiert nach Nachname -> Musterfrau vor Mustermann.
|
||||||
|
assertEquals(listOf("p-2", "p-1"), repo.search("Muster").first().map { it.id })
|
||||||
|
assertEquals(listOf("p-1"), repo.search("Max").first().map { it.id })
|
||||||
|
assertEquals(listOf("p-2"), repo.search("Erika").first().map { it.id })
|
||||||
|
assertEquals(listOf("p-2"), repo.search("1975-12").first().map { it.id })
|
||||||
|
assertTrue(repo.search("Zzz").first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_ist_unabhaengig_von_gross_und_kleinschreibung() = runDbTest {
|
||||||
|
repo.upsert(patient())
|
||||||
|
|
||||||
|
assertEquals(listOf("p-1"), repo.search("mustermann").first().map { it.id })
|
||||||
|
assertEquals(listOf("p-1"), repo.search("MAX").first().map { it.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
package de.medapp.wunddoku.data.repository
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import de.medapp.wunddoku.testutil.TestEnvironment
|
||||||
|
import de.medapp.wunddoku.testutil.runDbTest
|
||||||
|
import de.medapp.wunddoku.testutil.signatureBitmap
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/** Repository-Tests für das Wundversorger-Register ([WoundCarerRepository]). */
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class WoundCarerRepositoryTest {
|
||||||
|
|
||||||
|
private lateinit var env: TestEnvironment
|
||||||
|
private val repo get() = env.woundCarerRepository
|
||||||
|
private val storage get() = env.woundCarerSignatureStorage
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
env = TestEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
env.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** updatedAt basiert auf System.currentTimeMillis() - kurze Pause für eindeutige Reihenfolge. */
|
||||||
|
private fun tick() = Thread.sleep(5)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getMostRecentlyUsed_ist_null_solange_nie_eine_signatur_gespeichert_wurde() = runDbTest {
|
||||||
|
assertNull(repo.getMostRecentlyUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recordSignatureUsage_legt_neuen_eintrag_mit_eigener_signaturdatei_an() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
val carer = repo.getMostRecentlyUsed()!!
|
||||||
|
assertEquals("Anna Schmidt", carer.name)
|
||||||
|
assertEquals(1, storage.saved.size)
|
||||||
|
assertEquals(carer.id to carer.signatureFileName, storage.saved.single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recordSignatureUsage_aktualisiert_bekannten_namen_case_insensitiv_und_ersetzt_die_signatur() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
val first = repo.getMostRecentlyUsed()!!
|
||||||
|
tick()
|
||||||
|
|
||||||
|
repo.recordSignatureUsage("anna schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
val second = repo.getMostRecentlyUsed()!!
|
||||||
|
assertEquals(first.id, second.id)
|
||||||
|
assertEquals("anna schmidt", second.name) // zuletzt getippte Schreibweise
|
||||||
|
assertNotEquals(first.signatureFileName, second.signatureFileName)
|
||||||
|
assertTrue(second.updatedAt >= first.updatedAt)
|
||||||
|
assertEquals(1, repo.search("a").size)
|
||||||
|
// Alte Signaturdatei wurde entfernt, genau eine Datei bleibt zugeordnet.
|
||||||
|
assertEquals(listOf(first.id to first.signatureFileName), storage.deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recordSignatureUsage_ignoriert_fehler_beim_loeschen_der_alten_signaturdatei() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
storage.failOnDelete = true
|
||||||
|
tick()
|
||||||
|
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
assertEquals(1, repo.search("Anna").size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getMostRecentlyUsed_liefert_den_zuletzt_aktualisierten_wundversorger() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
tick()
|
||||||
|
repo.recordSignatureUsage("Bernd Beispiel", signatureBitmap())
|
||||||
|
assertEquals("Bernd Beispiel", repo.getMostRecentlyUsed()?.name)
|
||||||
|
|
||||||
|
tick()
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
assertEquals("Anna Schmidt", repo.getMostRecentlyUsed()?.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- search --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_liefert_praefix_treffer_case_insensitiv_neueste_zuerst() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
tick()
|
||||||
|
repo.recordSignatureUsage("Andreas Müller", signatureBitmap())
|
||||||
|
tick()
|
||||||
|
repo.recordSignatureUsage("Bernd Beispiel", signatureBitmap())
|
||||||
|
|
||||||
|
val result = repo.search("an")
|
||||||
|
|
||||||
|
assertEquals(listOf("Andreas Müller", "Anna Schmidt"), result.map { it.name })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_trimmt_die_eingabe_und_liefert_bei_leerer_eingabe_nichts() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
assertEquals(listOf("Anna Schmidt"), repo.search(" Ann ").map { it.name })
|
||||||
|
assertTrue(repo.search("").isEmpty())
|
||||||
|
assertTrue(repo.search(" ").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_ist_ein_praefix_und_kein_teilstring_abgleich() = runDbTest {
|
||||||
|
repo.recordSignatureUsage("Anna Schmidt", signatureBitmap())
|
||||||
|
|
||||||
|
assertTrue(repo.search("Schmidt").isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun search_beachtet_das_limit() = runDbTest {
|
||||||
|
for (name in listOf("Anna A", "Anna B", "Anna C", "Anna D")) {
|
||||||
|
repo.recordSignatureUsage(name, signatureBitmap())
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(2, repo.search("Anna", limit = 2).size)
|
||||||
|
assertEquals(4, repo.search("Anna").size) // Standardlimit 5 deckt alle vier ab
|
||||||
|
assertEquals(listOf("Anna D", "Anna C"), repo.search("Anna", limit = 2).map { it.name })
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- loadSignatureBitmap -------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadSignatureBitmap_delegiert_an_den_wundversorger_speicher() = runDbTest {
|
||||||
|
val bitmap = signatureBitmap()
|
||||||
|
storage.decodeResult = bitmap
|
||||||
|
|
||||||
|
val result = repo.loadSignatureBitmap("carer-1", "sig.png")
|
||||||
|
|
||||||
|
assertEquals(bitmap, result)
|
||||||
|
assertEquals(listOf("carer-1" to "sig.png"), storage.decodeCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadSignatureBitmap_liefert_null_wenn_die_datei_fehlt() = runDbTest {
|
||||||
|
storage.decodeResult = null
|
||||||
|
|
||||||
|
assertNull(repo.loadSignatureBitmap("carer-1", "fehlt.png"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package de.medapp.wunddoku.testutil
|
||||||
|
|
||||||
|
import de.medapp.wunddoku.data.entity.BlockStateEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.CheckItemStateEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.DocumentationEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.PatientEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.PhotoEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.SignatureEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.WoundCarerEntity
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationStatus
|
||||||
|
|
||||||
|
/** Builder für Room-Entities mit sinnvollen Standardwerten - Tests überschreiben nur, was sie brauchen. */
|
||||||
|
|
||||||
|
fun patientEntity(
|
||||||
|
id: String = "p-1",
|
||||||
|
firstName: String = "Max",
|
||||||
|
lastName: String = "Mustermann",
|
||||||
|
birthDate: String = "1980-03-14",
|
||||||
|
street: String = "Musterstraße 1",
|
||||||
|
postalCode: String = "12345",
|
||||||
|
city: String = "Musterstadt",
|
||||||
|
phone1: String = "0123 456789",
|
||||||
|
phone2: String? = null,
|
||||||
|
phone3: String? = null,
|
||||||
|
createdAt: Long = 1_000L,
|
||||||
|
updatedAt: Long = createdAt,
|
||||||
|
) = PatientEntity(
|
||||||
|
id = id,
|
||||||
|
firstName = firstName,
|
||||||
|
lastName = lastName,
|
||||||
|
birthDate = birthDate,
|
||||||
|
street = street,
|
||||||
|
postalCode = postalCode,
|
||||||
|
city = city,
|
||||||
|
phone1 = phone1,
|
||||||
|
phone2 = phone2,
|
||||||
|
phone3 = phone3,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun documentationEntity(
|
||||||
|
id: String = "d-1",
|
||||||
|
patientId: String = "p-1",
|
||||||
|
status: DocumentationStatus = DocumentationStatus.IN_PROGRESS,
|
||||||
|
createdAt: Long = 1_000L,
|
||||||
|
updatedAt: Long = createdAt,
|
||||||
|
continuedFromDocumentationId: String? = null,
|
||||||
|
pdfLastGeneratedAt: Long? = null,
|
||||||
|
catalogVersion: Int = 4,
|
||||||
|
firstNameSnapshot: String = "Max",
|
||||||
|
lastNameSnapshot: String = "Mustermann",
|
||||||
|
birthDateSnapshot: String = "1980-03-14",
|
||||||
|
streetSnapshot: String = "Musterstraße 1",
|
||||||
|
postalCodeSnapshot: String = "12345",
|
||||||
|
citySnapshot: String = "Musterstadt",
|
||||||
|
) = DocumentationEntity(
|
||||||
|
id = id,
|
||||||
|
patientId = patientId,
|
||||||
|
status = status,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
continuedFromDocumentationId = continuedFromDocumentationId,
|
||||||
|
pdfLastGeneratedAt = pdfLastGeneratedAt,
|
||||||
|
catalogVersion = catalogVersion,
|
||||||
|
patientFirstNameSnapshot = firstNameSnapshot,
|
||||||
|
patientLastNameSnapshot = lastNameSnapshot,
|
||||||
|
patientBirthDateSnapshot = birthDateSnapshot,
|
||||||
|
patientStreetSnapshot = streetSnapshot,
|
||||||
|
patientPostalCodeSnapshot = postalCodeSnapshot,
|
||||||
|
patientCitySnapshot = citySnapshot,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun blockStateEntity(
|
||||||
|
id: String = "b-1",
|
||||||
|
documentationId: String = "d-1",
|
||||||
|
blockDefinitionId: String = "wundart",
|
||||||
|
title: String = "Wundart",
|
||||||
|
order: Int = 1,
|
||||||
|
updatedAt: Long = 1_000L,
|
||||||
|
) = BlockStateEntity(
|
||||||
|
id = id,
|
||||||
|
documentationId = documentationId,
|
||||||
|
blockDefinitionId = blockDefinitionId,
|
||||||
|
titleSnapshot = title,
|
||||||
|
blockOrder = order,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun checkItemStateEntity(
|
||||||
|
id: String = "c-1",
|
||||||
|
blockStateId: String = "b-1",
|
||||||
|
definitionId: String = "item_1",
|
||||||
|
label: String = "Eintrag 1",
|
||||||
|
order: Int = 1,
|
||||||
|
checked: Boolean = false,
|
||||||
|
isFreeText: Boolean = false,
|
||||||
|
freeTextValue: String = "",
|
||||||
|
isStaticHint: Boolean = false,
|
||||||
|
inputSpec: String = "",
|
||||||
|
) = CheckItemStateEntity(
|
||||||
|
id = id,
|
||||||
|
blockStateId = blockStateId,
|
||||||
|
checkItemDefinitionId = definitionId,
|
||||||
|
labelSnapshot = label,
|
||||||
|
itemOrder = order,
|
||||||
|
checked = checked,
|
||||||
|
isFreeText = isFreeText,
|
||||||
|
freeTextValue = freeTextValue,
|
||||||
|
isStaticHint = isStaticHint,
|
||||||
|
inputSpec = inputSpec,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun photoEntity(
|
||||||
|
id: String = "ph-1",
|
||||||
|
documentationId: String = "d-1",
|
||||||
|
fileName: String = "foto.jpg",
|
||||||
|
orderIndex: Int = 0,
|
||||||
|
createdAt: Long = 1_000L,
|
||||||
|
) = PhotoEntity(
|
||||||
|
id = id,
|
||||||
|
documentationId = documentationId,
|
||||||
|
fileName = fileName,
|
||||||
|
orderIndex = orderIndex,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun signatureEntity(
|
||||||
|
id: String = "s-1",
|
||||||
|
documentationId: String = "d-1",
|
||||||
|
signerName: String = "Anna Schmidt",
|
||||||
|
fileName: String = "unterschrift.png",
|
||||||
|
orderIndex: Int = 0,
|
||||||
|
createdAt: Long = 1_000L,
|
||||||
|
) = SignatureEntity(
|
||||||
|
id = id,
|
||||||
|
documentationId = documentationId,
|
||||||
|
signerName = signerName,
|
||||||
|
fileName = fileName,
|
||||||
|
orderIndex = orderIndex,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun woundCarerEntity(
|
||||||
|
id: String = "w-1",
|
||||||
|
name: String = "Anna Schmidt",
|
||||||
|
signatureFileName: String = "w-sig.png",
|
||||||
|
updatedAt: Long = 1_000L,
|
||||||
|
) = WoundCarerEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
signatureFileName = signatureFileName,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
)
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package de.medapp.wunddoku.testutil
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import de.medapp.wunddoku.media.PhotoStorage
|
||||||
|
import de.medapp.wunddoku.pdf.DocumentationPdfExporter
|
||||||
|
import de.medapp.wunddoku.pdf.PdfExportModel
|
||||||
|
import de.medapp.wunddoku.pdf.PdfExportResult
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesConverter
|
||||||
|
import de.medapp.wunddoku.pdf.PdfToImagesResult
|
||||||
|
import de.medapp.wunddoku.signature.SignatureStorage
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/** Aufgezeichneter Aufruf von [PhotoStorage.duplicate]. */
|
||||||
|
data class DuplicateCall(
|
||||||
|
val sourceDocumentationId: String,
|
||||||
|
val sourceFileName: String,
|
||||||
|
val targetDocumentationId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-Memory-Fake für [PhotoStorage]: schreibt keine Dateien, zeichnet aber alle Aufrufe auf und
|
||||||
|
* vergibt eindeutige Dateinamen. Fehler lassen sich gezielt auslösen.
|
||||||
|
*/
|
||||||
|
class FakePhotoStorage : PhotoStorage {
|
||||||
|
val imported = mutableListOf<Pair<String, Uri>>()
|
||||||
|
val duplicated = mutableListOf<DuplicateCall>()
|
||||||
|
val deleted = mutableListOf<Pair<String, String>>()
|
||||||
|
val deletedAll = mutableListOf<String>()
|
||||||
|
|
||||||
|
var failOnDelete = false
|
||||||
|
var failOnDeleteAll = false
|
||||||
|
|
||||||
|
/** 1-basierte Nummer des [duplicate]-Aufrufs, der mit einer [IOException] fehlschlagen soll. */
|
||||||
|
var failOnDuplicateCall: Int? = null
|
||||||
|
|
||||||
|
private var counter = 0
|
||||||
|
|
||||||
|
override suspend fun importFrom(documentationId: String, sourceUri: Uri): String {
|
||||||
|
imported += documentationId to sourceUri
|
||||||
|
return "imported_${++counter}.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun duplicate(
|
||||||
|
sourceDocumentationId: String,
|
||||||
|
sourceFileName: String,
|
||||||
|
targetDocumentationId: String,
|
||||||
|
): String {
|
||||||
|
duplicated += DuplicateCall(sourceDocumentationId, sourceFileName, targetDocumentationId)
|
||||||
|
if (failOnDuplicateCall == duplicated.size) throw IOException("Kopieren fehlgeschlagen (Test)")
|
||||||
|
return "copy_${++counter}.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(documentationId: String, fileName: String) {
|
||||||
|
if (failOnDelete) throw IOException("Löschen fehlgeschlagen (Test)")
|
||||||
|
deleted += documentationId to fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAll(documentationId: String) {
|
||||||
|
if (failOnDeleteAll) throw IOException("Ordner löschen fehlgeschlagen (Test)")
|
||||||
|
deletedAll += documentationId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(documentationId: String, fileName: String): File =
|
||||||
|
File("/fake/photos/$documentationId/$fileName")
|
||||||
|
|
||||||
|
override suspend fun decodeThumbnail(documentationId: String, fileName: String, maxDimensionPx: Int): Bitmap? = null
|
||||||
|
|
||||||
|
override suspend fun decodeFitted(
|
||||||
|
documentationId: String,
|
||||||
|
fileName: String,
|
||||||
|
maxWidthPx: Int,
|
||||||
|
maxHeightPx: Int,
|
||||||
|
): Bitmap? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** In-Memory-Fake für [SignatureStorage] (analog [FakePhotoStorage]). */
|
||||||
|
class FakeSignatureStorage : SignatureStorage {
|
||||||
|
val saved = mutableListOf<Pair<String, String>>() // key (documentationId bzw. woundCarerId) -> fileName
|
||||||
|
val deleted = mutableListOf<Pair<String, String>>()
|
||||||
|
val deletedAll = mutableListOf<String>()
|
||||||
|
val decodeCalls = mutableListOf<Pair<String, String>>()
|
||||||
|
|
||||||
|
var failOnDelete = false
|
||||||
|
var failOnDeleteAll = false
|
||||||
|
var decodeResult: Bitmap? = null
|
||||||
|
|
||||||
|
private var counter = 0
|
||||||
|
|
||||||
|
override suspend fun save(documentationId: String, bitmap: Bitmap): String {
|
||||||
|
val fileName = "sig_${++counter}.png"
|
||||||
|
saved += documentationId to fileName
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(documentationId: String, fileName: String) {
|
||||||
|
if (failOnDelete) throw IOException("Löschen fehlgeschlagen (Test)")
|
||||||
|
deleted += documentationId to fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAll(documentationId: String) {
|
||||||
|
if (failOnDeleteAll) throw IOException("Ordner löschen fehlgeschlagen (Test)")
|
||||||
|
deletedAll += documentationId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(documentationId: String, fileName: String): File =
|
||||||
|
File("/fake/signatures/$documentationId/$fileName")
|
||||||
|
|
||||||
|
override suspend fun decode(documentationId: String, fileName: String): Bitmap? {
|
||||||
|
decodeCalls += documentationId to fileName
|
||||||
|
return decodeResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeichnet die an den Exporter übergebenen Modelle auf und liefert ein konfigurierbares Ergebnis. */
|
||||||
|
class FakePdfExporter : DocumentationPdfExporter {
|
||||||
|
val models = mutableListOf<PdfExportModel>()
|
||||||
|
var result: ((PdfExportModel) -> PdfExportResult) = { model ->
|
||||||
|
PdfExportResult.Success(File("/fake/pdfs/${model.fileName}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun export(model: PdfExportModel): PdfExportResult {
|
||||||
|
models += model
|
||||||
|
return result(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeichnet Aufrufe auf und liefert ein konfigurierbares Ergebnis. */
|
||||||
|
class FakePdfToImagesConverter : PdfToImagesConverter {
|
||||||
|
val calls = mutableListOf<Pair<String, File>>()
|
||||||
|
var result: PdfToImagesResult = PdfToImagesResult.Success(emptyList())
|
||||||
|
|
||||||
|
override suspend fun convert(documentationId: String, pdfFile: File): PdfToImagesResult {
|
||||||
|
calls += documentationId to pdfFile
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package de.medapp.wunddoku.testutil
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import de.medapp.wunddoku.data.dao.BlockStateDao
|
||||||
|
import de.medapp.wunddoku.data.dao.CheckItemStateDao
|
||||||
|
import de.medapp.wunddoku.data.dao.DocumentationDao
|
||||||
|
import de.medapp.wunddoku.data.dao.PatientDao
|
||||||
|
import de.medapp.wunddoku.data.dao.PhotoDao
|
||||||
|
import de.medapp.wunddoku.data.dao.SignatureDao
|
||||||
|
import de.medapp.wunddoku.data.dao.WoundCarerDao
|
||||||
|
import de.medapp.wunddoku.data.database.AppDatabase
|
||||||
|
import de.medapp.wunddoku.data.entity.DocumentationEntity
|
||||||
|
import de.medapp.wunddoku.data.entity.PatientEntity
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationStatus
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basisklasse für alle Room-Tests (QA-002): echte In-Memory-Datenbank mit demselben Schema,
|
||||||
|
* denselben Foreign Keys und denselben Indizes wie in Produktion. Jeder Test bekommt eine
|
||||||
|
* frische, leere Datenbank.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
abstract class RoomTestBase {
|
||||||
|
|
||||||
|
protected lateinit var db: AppDatabase
|
||||||
|
|
||||||
|
protected val patientDao: PatientDao get() = db.patientDao()
|
||||||
|
protected val documentationDao: DocumentationDao get() = db.documentationDao()
|
||||||
|
protected val blockStateDao: BlockStateDao get() = db.blockStateDao()
|
||||||
|
protected val checkItemStateDao: CheckItemStateDao get() = db.checkItemStateDao()
|
||||||
|
protected val photoDao: PhotoDao get() = db.photoDao()
|
||||||
|
protected val signatureDao: SignatureDao get() = db.signatureDao()
|
||||||
|
protected val woundCarerDao: WoundCarerDao get() = db.woundCarerDao()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun createDatabase() {
|
||||||
|
db = Room.inMemoryDatabaseBuilder(
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun closeDatabase() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Seed-Helfer ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
protected suspend fun seedPatient(
|
||||||
|
id: String = "p-1",
|
||||||
|
firstName: String = "Max",
|
||||||
|
lastName: String = "Mustermann",
|
||||||
|
birthDate: String = "1980-03-14",
|
||||||
|
): PatientEntity {
|
||||||
|
val entity = patientEntity(id = id, firstName = firstName, lastName = lastName, birthDate = birthDate)
|
||||||
|
patientDao.insert(entity)
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legt eine Dokumentation an; fehlt der Patient, wird er mit Standarddaten angelegt. */
|
||||||
|
protected suspend fun seedDocumentation(
|
||||||
|
id: String = "d-1",
|
||||||
|
patientId: String = "p-1",
|
||||||
|
createdAt: Long = 1_000L,
|
||||||
|
status: DocumentationStatus = DocumentationStatus.IN_PROGRESS,
|
||||||
|
): DocumentationEntity {
|
||||||
|
if (patientDao.getById(patientId) == null) seedPatient(id = patientId)
|
||||||
|
val entity = documentationEntity(id = id, patientId = patientId, createdAt = createdAt, status = status)
|
||||||
|
documentationDao.insert(entity)
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Roh-SQL-Helfer (unabhängig von DAO-Logik prüfen, was wirklich in der DB steht) ----
|
||||||
|
|
||||||
|
protected fun queryInt(sql: String): Int =
|
||||||
|
db.openHelper.readableDatabase.query(sql).use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun queryString(sql: String): String =
|
||||||
|
db.openHelper.readableDatabase.query(sql).use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getString(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun countRows(table: String): Int = queryInt("SELECT COUNT(*) FROM $table")
|
||||||
|
|
||||||
|
protected fun execSql(sql: String) {
|
||||||
|
db.openHelper.writableDatabase.execSQL(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wartet (mit Timeout), bis ein Flow einen Wert liefert, der [predicate] erfüllt.
|
||||||
|
* Robust für Reaktivitäts-Tests, egal ob Änderung vor oder nach dem ersten Emit passiert.
|
||||||
|
*/
|
||||||
|
suspend fun <T> Flow<T>.awaitMatching(timeoutMs: Long = 5_000L, predicate: (T) -> Boolean): T =
|
||||||
|
withTimeout(timeoutMs) { first { predicate(it) } }
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package de.medapp.wunddoku.testutil
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import de.medapp.wunddoku.data.database.AppDatabase
|
||||||
|
import de.medapp.wunddoku.data.entity.PatientEntity
|
||||||
|
import de.medapp.wunddoku.data.repository.DocumentationRepository
|
||||||
|
import de.medapp.wunddoku.data.repository.PatientRepository
|
||||||
|
import de.medapp.wunddoku.data.repository.WoundCarerRepository
|
||||||
|
import de.medapp.wunddoku.domain.model.CheckItem
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationBlock
|
||||||
|
import de.medapp.wunddoku.domain.model.DocumentationStatus
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemeinsame Testumgebung für alle Instrumented-Tests: echte Room-Datenbank im Arbeitsspeicher
|
||||||
|
* (gleiche Entities/DAOs/Foreign Keys wie in Produktion), aber Fakes für Dateispeicher und
|
||||||
|
* PDF-Erzeugung. Jeder Test erzeugt eine eigene Instanz und ruft am Ende [close] auf.
|
||||||
|
*/
|
||||||
|
class TestEnvironment {
|
||||||
|
|
||||||
|
val db: AppDatabase = Room.inMemoryDatabaseBuilder(
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
).build()
|
||||||
|
|
||||||
|
val photoStorage = FakePhotoStorage()
|
||||||
|
val signatureStorage = FakeSignatureStorage()
|
||||||
|
val woundCarerSignatureStorage = FakeSignatureStorage()
|
||||||
|
val pdfExporter = FakePdfExporter()
|
||||||
|
val pdfToImagesConverter = FakePdfToImagesConverter()
|
||||||
|
|
||||||
|
val patientRepository = PatientRepository(patientDao = db.patientDao())
|
||||||
|
|
||||||
|
val woundCarerRepository = WoundCarerRepository(
|
||||||
|
woundCarerDao = db.woundCarerDao(),
|
||||||
|
woundCarerSignatureStorage = woundCarerSignatureStorage,
|
||||||
|
)
|
||||||
|
|
||||||
|
val documentationRepository = DocumentationRepository(
|
||||||
|
documentationDao = db.documentationDao(),
|
||||||
|
blockStateDao = db.blockStateDao(),
|
||||||
|
checkItemStateDao = db.checkItemStateDao(),
|
||||||
|
photoDao = db.photoDao(),
|
||||||
|
signatureDao = db.signatureDao(),
|
||||||
|
patientDao = db.patientDao(),
|
||||||
|
photoStorage = photoStorage,
|
||||||
|
signatureStorage = signatureStorage,
|
||||||
|
woundCarerRepository = woundCarerRepository,
|
||||||
|
pdfExporter = pdfExporter,
|
||||||
|
pdfToImagesConverter = pdfToImagesConverter,
|
||||||
|
db = db,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun close() = db.close()
|
||||||
|
|
||||||
|
/** Legt direkt über das DAO einen Patienten an (ohne Repository-Logik). */
|
||||||
|
suspend fun insertPatient(
|
||||||
|
id: String = "patient-1",
|
||||||
|
firstName: String = "Max",
|
||||||
|
lastName: String = "Mustermann",
|
||||||
|
birthDate: String = "1980-03-14",
|
||||||
|
street: String = "Musterstraße 1",
|
||||||
|
postalCode: String = "12345",
|
||||||
|
city: String = "Musterstadt",
|
||||||
|
): PatientEntity {
|
||||||
|
val entity = PatientEntity(
|
||||||
|
id = id,
|
||||||
|
firstName = firstName,
|
||||||
|
lastName = lastName,
|
||||||
|
birthDate = birthDate,
|
||||||
|
street = street,
|
||||||
|
postalCode = postalCode,
|
||||||
|
city = city,
|
||||||
|
phone1 = "0123 456789",
|
||||||
|
phone2 = null,
|
||||||
|
phone3 = null,
|
||||||
|
createdAt = 1_000L,
|
||||||
|
updatedAt = 1_000L,
|
||||||
|
)
|
||||||
|
db.patientDao().insert(entity)
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt createdAt einer Dokumentation gezielt (vermeidet zeitabhängige Tests). */
|
||||||
|
suspend fun setCreatedAt(documentationId: String, createdAt: Long) {
|
||||||
|
val dao = db.documentationDao()
|
||||||
|
dao.update(dao.getById(documentationId)!!.copy(createdAt = createdAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setStatus(documentationId: String, status: DocumentationStatus) {
|
||||||
|
val dao = db.documentationDao()
|
||||||
|
dao.update(dao.getById(documentationId)!!.copy(status = status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Führt einen Test mit Suspend-Aufrufen aus; Rückgabetyp Unit, damit JUnit die Methode akzeptiert. */
|
||||||
|
fun runDbTest(block: suspend CoroutineScope.() -> Unit) {
|
||||||
|
runBlocking { block() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erstes Item eines Blocks, das eine normale Checkbox ist (kein Freitext, hint oder strukturierte Eingabe). */
|
||||||
|
fun DocumentationBlock.firstPlainItem(): CheckItem =
|
||||||
|
items.first { !it.isFreeText && !it.isStaticHint && it.inputSpec.isEmpty() }
|
||||||
|
|
||||||
|
/** Weißes Bitmap ohne jeden Strich - gilt als "leere Signatur". */
|
||||||
|
fun blankBitmap(): Bitmap =
|
||||||
|
Bitmap.createBitmap(40, 20, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.WHITE) }
|
||||||
|
|
||||||
|
/** Weißes Bitmap mit einem schwarzen Strich - gilt als gültige Signatur. */
|
||||||
|
fun signatureBitmap(): Bitmap = blankBitmap().also { bitmap ->
|
||||||
|
val paint = Paint().apply {
|
||||||
|
color = Color.BLACK
|
||||||
|
strokeWidth = 3f
|
||||||
|
}
|
||||||
|
Canvas(bitmap).drawLine(2f, 2f, 38f, 18f, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erwartet, dass [block] eine Exception vom Typ [T] wirft, und liefert sie zurück. */
|
||||||
|
inline fun <reified T : Throwable> expectThrows(block: () -> Unit): T {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (e is T) return e
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
throw AssertionError("Erwartet: ${T::class.java.simpleName}, es wurde aber keine Exception geworfen")
|
||||||
|
}
|
||||||
@@ -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("???", "***"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user