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