package nl.eddydevink.buschauffeurswekker.caldav
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class IcalTest {
private val amsterdam = ZoneId.of("Europe/Amsterdam")
@Test
fun `extractCalendarBlocks haalt blokken met elk namespace-prefix`() {
val xml = """
/remote.php/dav/calendars/eddy/ebs_diensten-1/abc.ics
BEGIN:VCALENDAR${'\n'}X:een
BEGIN:VCALENDAR${'\n'}X:twee
BEGIN:VCALENDAR${'\n'}X:drie
""".trimIndent()
val blocks = Ical.extractCalendarBlocks(xml)
assertEquals(3, blocks.size)
assertEquals("BEGIN:VCALENDAR${'\n'}X:drie", blocks[2])
}
@Test
fun `DTSTART met TZID lost op naar lokale tijd in die zone`() {
val block = buildVEvent(
summary = "1234",
dtstart = "DTSTART;TZID=Europe/Amsterdam:20261015T050000",
)
val event = Ical.parseVEvent(block, amsterdam)!!
assertEquals(LocalDateTime.of(2026, 10, 15, 5, 0), event.start.toLocalDateTime())
assertEquals(amsterdam, event.start.zone)
assertEquals("1234", event.summary)
}
@Test
fun `DTSTART in UTC wordt omgerekend naar de lokale zone`() {
// 05:00 UTC op 15 juni = 07:00 zomertijd Amsterdam
val block = buildVEvent(
summary = "1234",
dtstart = "DTSTART:20260615T050000Z",
)
val event = Ical.parseVEvent(block, amsterdam)!!
val local = event.start.withZoneSameInstant(amsterdam)
assertEquals(LocalDateTime.of(2026, 6, 15, 7, 0), local.toLocalDateTime())
}
@Test
fun `all-day-event geeft geen diensttijd`() {
val block = buildVEvent(
summary = "vrij",
dtstart = "DTSTART;VALUE=DATE:20261015",
)
assertNull(Ical.parseVEvent(block, amsterdam))
}
@Test
fun `geannuleerd event wordt overgeslagen`() {
val block = buildVEvent(
summary = "1234",
dtstart = "DTSTART;TZID=Europe/Amsterdam:20261015T050000",
extra = "STATUS:CANCELLED",
)
assertNull(Ical.parseVEvent(block, amsterdam))
}
@Test
fun `eerste event is de vroegste aanvang`() {
val vcalendar = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud//NEXTCLOUD//NL
BEGIN:VEVENT
UID:laat
SUMMARY:2222
DTSTART;TZID=Europe/Amsterdam:20261015T140000
DTEND;TZID=Europe/Amsterdam:20261015T210000
END:VEVENT
BEGIN:VEVENT
UID:vroeg
SUMMARY:1111
DTSTART;TZID=Europe/Amsterdam:20261015T043000
DTEND;TZID=Europe/Amsterdam:20261015T120000
END:VEVENT
END:VCALENDAR
""".trimIndent()
val event = Ical.firstTimedEvent(vcalendar)!!
assertEquals("1111", event.summary)
assertEquals(LocalDateTime.of(2026, 10, 15, 4, 30), event.start.toLocalDateTime())
}
@Test
fun `unfold voegt gevouwen regels samen`() {
// RFC 5545: CRLF + spatie/tab na de break is het vouwteken en wordt verwijderd.
val lines = Ical.unfold("SUMMARY:een \r\n lange samenvatting\r\nDTSTART:20260615T050000")
assertEquals("SUMMARY:een lange samenvatting", lines[0])
assertEquals("DTSTART:20260615T050000", lines[1])
}
@Test
fun `XML-ampex wordt ontsnapt in calendar-data`() {
val xml = ("" +
"BEGIN:VCALENDAR\nBEGIN:VEVENT\nSUMMARY:Dienst & meer\n" +
"DTSTART;TZID=Europe/Amsterdam:20261015T050000\nEND:VEVENT\nEND:VCALENDAR" +
"")
val block = Ical.extractCalendarBlocks(xml).single()
val event = Ical.parseVEvent(block, amsterdam)!!
assertEquals("Dienst & meer", event.summary)
}
private fun buildVEvent(summary: String, dtstart: String, extra: String = ""): String {
val extraLine = if (extra.isBlank()) "" else "$extra\n"
return """
BEGIN:VEVENT
UID:test-1
SUMMARY:$summary
$dtstart
${extraLine}END:VEVENT
""".trimIndent() + "\n"
}
}