data: add ColumnReader.toEventInstance() with defensive validation (§8)
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
|
||||||
|
private const val TAG = "InstanceMapper"
|
||||||
|
|
||||||
|
internal fun ColumnReader.toEventInstance(): EventInstance? {
|
||||||
|
val begin = getLong(InstanceProjection.IDX_BEGIN)
|
||||||
|
val end = getLong(InstanceProjection.IDX_END)
|
||||||
|
|
||||||
|
if (begin < 0L) {
|
||||||
|
Log.w(TAG, "Dropping row with negative begin=$begin")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (end < begin) {
|
||||||
|
Log.w(TAG, "Dropping row with end=$end < begin=$begin")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawTitle = getString(InstanceProjection.IDX_TITLE)
|
||||||
|
val title = if (rawTitle.isNullOrEmpty()) Fallbacks.UNTITLED_EVENT else rawTitle
|
||||||
|
|
||||||
|
val color = if (isNull(InstanceProjection.IDX_EVENT_COLOR)) {
|
||||||
|
getInt(InstanceProjection.IDX_CALENDAR_COLOR)
|
||||||
|
} else {
|
||||||
|
getInt(InstanceProjection.IDX_EVENT_COLOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
return EventInstance(
|
||||||
|
instanceId = getLong(InstanceProjection.IDX_INSTANCE_ID),
|
||||||
|
eventId = getLong(InstanceProjection.IDX_EVENT_ID),
|
||||||
|
calendarId = getLong(InstanceProjection.IDX_CALENDAR_ID),
|
||||||
|
title = title,
|
||||||
|
start = begin.toKotlinInstantFromEpochMillis(),
|
||||||
|
end = end.toKotlinInstantFromEpochMillis(),
|
||||||
|
isAllDay = getInt(InstanceProjection.IDX_ALL_DAY) != 0,
|
||||||
|
color = color,
|
||||||
|
location = getString(InstanceProjection.IDX_LOCATION),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class InstanceMapperTest {
|
||||||
|
|
||||||
|
private fun reader(
|
||||||
|
instanceId: Long = 10L,
|
||||||
|
eventId: Long = 1L,
|
||||||
|
calendarId: Long = 1L,
|
||||||
|
title: String? = "Meet",
|
||||||
|
begin: Long = 1_000_000_000L,
|
||||||
|
end: Long = 1_000_003_600L,
|
||||||
|
allDay: Int = 0,
|
||||||
|
eventColor: Any? = null,
|
||||||
|
calendarColor: Int = 0xFFAABBCC.toInt(),
|
||||||
|
location: String? = null,
|
||||||
|
): MapColumnReader = MapColumnReader(
|
||||||
|
InstanceProjection.IDX_INSTANCE_ID to instanceId,
|
||||||
|
InstanceProjection.IDX_EVENT_ID to eventId,
|
||||||
|
InstanceProjection.IDX_CALENDAR_ID to calendarId,
|
||||||
|
InstanceProjection.IDX_TITLE to title,
|
||||||
|
InstanceProjection.IDX_BEGIN to begin,
|
||||||
|
InstanceProjection.IDX_END to end,
|
||||||
|
InstanceProjection.IDX_ALL_DAY to allDay,
|
||||||
|
InstanceProjection.IDX_EVENT_COLOR to eventColor,
|
||||||
|
InstanceProjection.IDX_CALENDAR_COLOR to calendarColor,
|
||||||
|
InstanceProjection.IDX_LOCATION to location,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `happy path - non-allday event`() {
|
||||||
|
val inst = reader().toEventInstance()
|
||||||
|
assertThat(inst).isNotNull()
|
||||||
|
assertThat(inst!!.title).isEqualTo("Meet")
|
||||||
|
assertThat(inst.isAllDay).isFalse()
|
||||||
|
assertThat(inst.start).isEqualTo(Instant.fromEpochMilliseconds(1_000_000_000L))
|
||||||
|
assertThat(inst.end).isEqualTo(Instant.fromEpochMilliseconds(1_000_003_600L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `event color falls back to calendar color when null`() {
|
||||||
|
val inst = reader(eventColor = null, calendarColor = 0xFF112233.toInt()).toEventInstance()
|
||||||
|
assertThat(inst!!.color).isEqualTo(0xFF112233.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `event color wins over calendar color when present`() {
|
||||||
|
val inst = reader(
|
||||||
|
eventColor = 0xFFDEADBE.toInt(),
|
||||||
|
calendarColor = 0xFF112233.toInt(),
|
||||||
|
).toEventInstance()
|
||||||
|
assertThat(inst!!.color).isEqualTo(0xFFDEADBE.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `null title falls back to placeholder`() {
|
||||||
|
val inst = reader(title = null).toEventInstance()
|
||||||
|
assertThat(inst!!.title).isEqualTo(Fallbacks.UNTITLED_EVENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty title falls back to placeholder`() {
|
||||||
|
val inst = reader(title = "").toEventInstance()
|
||||||
|
assertThat(inst!!.title).isEqualTo(Fallbacks.UNTITLED_EVENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dtend before dtstart drops the row`() {
|
||||||
|
val inst = reader(begin = 2000L, end = 1000L).toEventInstance()
|
||||||
|
assertThat(inst).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dtstart before unix epoch drops the row`() {
|
||||||
|
val inst = reader(begin = -1L, end = 1000L).toEventInstance()
|
||||||
|
assertThat(inst).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all-day flag 1 maps to true`() {
|
||||||
|
val inst = reader(allDay = 1).toEventInstance()
|
||||||
|
assertThat(inst!!.isAllDay).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `location passes through when present`() {
|
||||||
|
val inst = reader(location = "Berlin").toEventInstance()
|
||||||
|
assertThat(inst!!.location).isEqualTo("Berlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user