data: add ColumnReader abstraction + Cursor.toCalendarSource mapper

Deviation from Plan 02: the JVM mockable-android.jar stubs every Cursor
method even with isReturnDefaultValues=true (returns null/0 regardless of
the underlying MatrixCursor backing). Introduce an internal ColumnReader
interface so mappers stay pure-Kotlin and JVM-testable via MapColumnReader,
while production reads through CursorColumnReader.
This commit is contained in:
2026-06-08 17:40:37 +02:00
parent fb723fba68
commit 0e4c47febe
4 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
package de.jeanlucmakiola.calendula.data.calendar
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
class CalendarMapperTest {
private fun reader(
id: Long = 1L,
displayName: String? = "Cal",
accountName: String? = "x@y",
accountType: String? = "LOCAL",
color: Int = 0,
visible: Int = 1,
): MapColumnReader = MapColumnReader(
CalendarProjection.IDX_ID to id,
CalendarProjection.IDX_DISPLAY_NAME to displayName,
CalendarProjection.IDX_ACCOUNT_NAME to accountName,
CalendarProjection.IDX_ACCOUNT_TYPE to accountType,
CalendarProjection.IDX_COLOR to color,
CalendarProjection.IDX_VISIBLE to visible,
)
@Test
fun `happy path maps all six columns`() {
val src = reader(
id = 42L,
displayName = "Work",
accountName = "x@y",
accountType = "com.google",
color = 0xFF112233.toInt(),
visible = 1,
).toCalendarSource()
assertThat(src).isEqualTo(
de.jeanlucmakiola.calendula.domain.CalendarSource(
id = 42L,
displayName = "Work",
accountName = "x@y",
accountType = "com.google",
color = 0xFF112233.toInt(),
isVisibleInSystem = true,
)
)
}
@Test
fun `null displayName falls back to placeholder`() {
val src = reader(displayName = null).toCalendarSource()
assertThat(src.displayName).isEqualTo(Fallbacks.UNNAMED_CALENDAR)
}
@Test
fun `visible flag 0 maps to false`() {
assertThat(reader(visible = 0).toCalendarSource().isVisibleInSystem).isFalse()
}
@Test
fun `visible flag 1 maps to true`() {
assertThat(reader(visible = 1).toCalendarSource().isVisibleInSystem).isTrue()
}
@Test
fun `null accountName and accountType coerce to empty string`() {
val src = reader(accountName = null, accountType = null).toCalendarSource()
assertThat(src.accountName).isEqualTo("")
assertThat(src.accountType).isEqualTo("")
}
}

View File

@@ -0,0 +1,29 @@
package de.jeanlucmakiola.calendula.data.calendar
/**
* Test-only ColumnReader. Backed by a Map<Int, Any?>; any missing index is
* treated as null. Numeric getters coerce via toLong/toInt; non-numeric values
* yield zero (matching Android Cursor behavior for type-mismatched reads).
*/
internal class MapColumnReader(values: Map<Int, Any?>) : ColumnReader {
private val data: Map<Int, Any?> = values
constructor(vararg pairs: Pair<Int, Any?>) : this(pairs.toMap())
override fun getLong(index: Int): Long = when (val v = data[index]) {
is Number -> v.toLong()
is String -> v.toLongOrNull() ?: 0L
else -> 0L
}
override fun getString(index: Int): String? = data[index]?.toString()
override fun getInt(index: Int): Int = when (val v = data[index]) {
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: 0
else -> 0
}
override fun isNull(index: Int): Boolean = data[index] == null
}