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" } }