data: add ColumnReader.toEventDetailCore() and toAttendee() mappers
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import android.provider.CalendarContract
|
||||
import android.util.Log
|
||||
import de.jeanlucmakiola.calendula.domain.Attendee
|
||||
import de.jeanlucmakiola.calendula.domain.AttendeeStatus
|
||||
import de.jeanlucmakiola.calendula.domain.EventDetail
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
|
||||
private const val TAG = "EventDetailMapper"
|
||||
|
||||
internal fun ColumnReader.toEventDetailCore(attendees: List<Attendee>): EventDetail? {
|
||||
val begin = getLong(EventDetailProjection.IDX_DTSTART)
|
||||
val end = getLong(EventDetailProjection.IDX_DTEND)
|
||||
|
||||
if (begin < 0L) {
|
||||
Log.w(TAG, "Dropping event with negative dtstart=$begin")
|
||||
return null
|
||||
}
|
||||
if (end < begin) {
|
||||
Log.w(TAG, "Dropping event with dtend=$end < dtstart=$begin")
|
||||
return null
|
||||
}
|
||||
|
||||
val rawTitle = getString(EventDetailProjection.IDX_TITLE)
|
||||
val title = if (rawTitle.isNullOrEmpty()) Fallbacks.UNTITLED_EVENT else rawTitle
|
||||
|
||||
val color = if (isNull(EventDetailProjection.IDX_EVENT_COLOR)) {
|
||||
getInt(EventDetailProjection.IDX_CALENDAR_COLOR)
|
||||
} else {
|
||||
getInt(EventDetailProjection.IDX_EVENT_COLOR)
|
||||
}
|
||||
|
||||
val eventId = getLong(EventDetailProjection.IDX_EVENT_ID)
|
||||
val instance = EventInstance(
|
||||
instanceId = eventId,
|
||||
eventId = eventId,
|
||||
calendarId = getLong(EventDetailProjection.IDX_CALENDAR_ID),
|
||||
title = title,
|
||||
start = begin.toKotlinInstantFromEpochMillis(),
|
||||
end = end.toKotlinInstantFromEpochMillis(),
|
||||
isAllDay = getInt(EventDetailProjection.IDX_ALL_DAY) != 0,
|
||||
color = color,
|
||||
location = getString(EventDetailProjection.IDX_LOCATION),
|
||||
)
|
||||
|
||||
return EventDetail(
|
||||
instance = instance,
|
||||
description = getString(EventDetailProjection.IDX_DESCRIPTION),
|
||||
organizer = getString(EventDetailProjection.IDX_ORGANIZER),
|
||||
attendees = attendees,
|
||||
rrule = getString(EventDetailProjection.IDX_RRULE),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun ColumnReader.toAttendee(): Attendee = Attendee(
|
||||
name = getString(AttendeeProjection.IDX_NAME).orEmpty(),
|
||||
email = getString(AttendeeProjection.IDX_EMAIL),
|
||||
status = mapAttendeeStatus(getInt(AttendeeProjection.IDX_STATUS)),
|
||||
)
|
||||
|
||||
internal fun mapAttendeeStatus(raw: Int): AttendeeStatus = when (raw) {
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED -> AttendeeStatus.Accepted
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED -> AttendeeStatus.Declined
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE -> AttendeeStatus.Tentative
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_INVITED -> AttendeeStatus.NeedsAction
|
||||
else -> AttendeeStatus.Unknown
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.domain.AttendeeStatus
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class EventDetailMapperTest {
|
||||
|
||||
private fun detailReader(
|
||||
eventId: Long = 1L,
|
||||
title: String? = "Meet",
|
||||
description: String? = "Body",
|
||||
organizer: String? = "x@y",
|
||||
rrule: String? = null,
|
||||
eventColor: Any? = null,
|
||||
calendarColor: Int = 0xFFAABBCC.toInt(),
|
||||
dtstart: Long = 1_000_000_000L,
|
||||
dtend: Long = 1_000_003_600L,
|
||||
allDay: Int = 0,
|
||||
location: String? = "Berlin",
|
||||
calendarId: Long = 7L,
|
||||
): MapColumnReader = MapColumnReader(
|
||||
EventDetailProjection.IDX_EVENT_ID to eventId,
|
||||
EventDetailProjection.IDX_TITLE to title,
|
||||
EventDetailProjection.IDX_DESCRIPTION to description,
|
||||
EventDetailProjection.IDX_ORGANIZER to organizer,
|
||||
EventDetailProjection.IDX_RRULE to rrule,
|
||||
EventDetailProjection.IDX_EVENT_COLOR to eventColor,
|
||||
EventDetailProjection.IDX_CALENDAR_COLOR to calendarColor,
|
||||
EventDetailProjection.IDX_DTSTART to dtstart,
|
||||
EventDetailProjection.IDX_DTEND to dtend,
|
||||
EventDetailProjection.IDX_ALL_DAY to allDay,
|
||||
EventDetailProjection.IDX_LOCATION to location,
|
||||
EventDetailProjection.IDX_CALENDAR_ID to calendarId,
|
||||
)
|
||||
|
||||
private fun attendeeReader(name: String?, email: String?, status: Int): MapColumnReader =
|
||||
MapColumnReader(
|
||||
AttendeeProjection.IDX_NAME to name,
|
||||
AttendeeProjection.IDX_EMAIL to email,
|
||||
AttendeeProjection.IDX_STATUS to status,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `happy path detail maps all fields and embeds matching EventInstance`() {
|
||||
val detail = detailReader().toEventDetailCore(attendees = emptyList())
|
||||
assertThat(detail).isNotNull()
|
||||
assertThat(detail!!.description).isEqualTo("Body")
|
||||
assertThat(detail.organizer).isEqualTo("x@y")
|
||||
assertThat(detail.instance.title).isEqualTo("Meet")
|
||||
assertThat(detail.instance.location).isEqualTo("Berlin")
|
||||
assertThat(detail.attendees).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `event color falls back to calendar color when null`() {
|
||||
val detail = detailReader(eventColor = null, calendarColor = 0xFF112233.toInt())
|
||||
.toEventDetailCore(attendees = emptyList())
|
||||
assertThat(detail!!.instance.color).isEqualTo(0xFF112233.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dtend before dtstart drops detail`() {
|
||||
val detail = detailReader(dtstart = 2000L, dtend = 1000L)
|
||||
.toEventDetailCore(attendees = emptyList())
|
||||
assertThat(detail).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rrule passes through when present`() {
|
||||
val detail = detailReader(rrule = "FREQ=WEEKLY;BYDAY=MO")
|
||||
.toEventDetailCore(attendees = emptyList())
|
||||
assertThat(detail!!.rrule).isEqualTo("FREQ=WEEKLY;BYDAY=MO")
|
||||
}
|
||||
|
||||
// Raw CalendarContract.Attendees status integer constants from the Android
|
||||
// source (kept inline so the test doesn't depend on the mockable.jar's
|
||||
// possibly-stubbed constants):
|
||||
// ACCEPTED=1, DECLINED=2, INVITED=3, TENTATIVE=4, NONE=0
|
||||
@Test
|
||||
fun `attendee status maps known integer codes`() {
|
||||
assertThat(attendeeReader("A", "a@x", 1).toAttendee().status)
|
||||
.isEqualTo(AttendeeStatus.Accepted)
|
||||
assertThat(attendeeReader("B", "b@x", 2).toAttendee().status)
|
||||
.isEqualTo(AttendeeStatus.Declined)
|
||||
assertThat(attendeeReader("C", "c@x", 4).toAttendee().status)
|
||||
.isEqualTo(AttendeeStatus.Tentative)
|
||||
assertThat(attendeeReader("D", "d@x", 3).toAttendee().status)
|
||||
.isEqualTo(AttendeeStatus.NeedsAction)
|
||||
assertThat(attendeeReader("E", "e@x", 0).toAttendee().status)
|
||||
.isEqualTo(AttendeeStatus.Unknown)
|
||||
assertThat(attendeeReader("F", "f@x", 99).toAttendee().status)
|
||||
.isEqualTo(AttendeeStatus.Unknown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attendee with null name maps to empty string`() {
|
||||
val a = attendeeReader(null, "alice@x", 1).toAttendee()
|
||||
assertThat(a.name).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attendee email passes through nullably`() {
|
||||
assertThat(attendeeReader("A", null, 1).toAttendee().email).isNull()
|
||||
assertThat(attendeeReader("A", "x@y", 1).toAttendee().email).isEqualTo("x@y")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user