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:
@@ -0,0 +1,13 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||
|
||||
internal fun ColumnReader.toCalendarSource(): CalendarSource = CalendarSource(
|
||||
id = getLong(CalendarProjection.IDX_ID),
|
||||
displayName = getString(CalendarProjection.IDX_DISPLAY_NAME)
|
||||
?: Fallbacks.UNNAMED_CALENDAR,
|
||||
accountName = getString(CalendarProjection.IDX_ACCOUNT_NAME).orEmpty(),
|
||||
accountType = getString(CalendarProjection.IDX_ACCOUNT_TYPE).orEmpty(),
|
||||
color = getInt(CalendarProjection.IDX_COLOR),
|
||||
isVisibleInSystem = getInt(CalendarProjection.IDX_VISIBLE) != 0,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
/**
|
||||
* Read-only view over a single row's columns by index. Lets the mappers work
|
||||
* on pure-Kotlin test fixtures (MapColumnReader) on the JVM, while the
|
||||
* production path adapts an Android Cursor row via CursorColumnReader.
|
||||
*/
|
||||
internal interface ColumnReader {
|
||||
fun getLong(index: Int): Long
|
||||
fun getString(index: Int): String?
|
||||
fun getInt(index: Int): Int
|
||||
fun isNull(index: Int): Boolean
|
||||
}
|
||||
|
||||
internal class CursorColumnReader(private val cursor: Cursor) : ColumnReader {
|
||||
override fun getLong(index: Int): Long = cursor.getLong(index)
|
||||
override fun getString(index: Int): String? = cursor.getString(index)
|
||||
override fun getInt(index: Int): Int = cursor.getInt(index)
|
||||
override fun isNull(index: Int): Boolean = cursor.isNull(index)
|
||||
}
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user