using LehrerApp.Core.Services; using Xunit; namespace LehrerApp.Tests; /// Tests gegen synthetische, aber am realen WebUntis-Export orientierte ICS-Fixtures (siehe /// Planungsdokument "WebUntis-iCal-Abgleich") — kein Rückgriff auf den echten Nutzer-Feed. public class IcsParserTests { private const string CalendarHeader = "BEGIN:VCALENDAR\nPRODID:-//Ben Fortuna//iCal4j 1.0//EN\nVERSION:2.0\nCALSCALE:GREGORIAN\n"; private const string CalendarFooter = "END:VCALENDAR\n"; private static string Wrap(string vevents) => CalendarHeader + vevents + CalendarFooter; [Fact] public void Parse_LiestGrundlegendesVeventKorrekt() { var ics = Wrap( "BEGIN:VEVENT\n" + "DTSTAMP:20260822T195925Z\n" + "UID:38926-2013812-2013815\n" + "STATUS:CONFIRMED\n" + "DTSTART;TZID=Europe/Berlin:20260817T075000\n" + "DTEND;TZID=Europe/Berlin:20260817T092000\n" + "SUMMARY:SOL\n" + "LOCATION:Medien\n" + "DESCRIPTION:10c HED\n" + "END:VEVENT\n"); var events = IcsParser.Parse(ics); var evt = Assert.Single(events); Assert.Equal("38926-2013812-2013815", evt.Uid); Assert.Equal(new DateOnly(2026, 8, 17), evt.Date); Assert.Equal(new TimeOnly(7, 50), evt.StartTime); Assert.Equal(new TimeOnly(9, 20), evt.EndTime); Assert.Equal("SOL", evt.Summary); Assert.Equal("Medien", evt.Location); Assert.Equal("10c HED", evt.Description); Assert.Equal("CONFIRMED", evt.Status); } [Fact] public void Parse_LiestMehrereVeventsUnabhaengigVoneinander() { var ics = Wrap( "BEGIN:VEVENT\nUID:1\nDTSTART;TZID=Europe/Berlin:20260817T075000\nSUMMARY:NAT\nEND:VEVENT\n" + "BEGIN:VEVENT\nUID:2\nDTSTART;TZID=Europe/Berlin:20260818T094000\nSUMMARY:Mat_E\nEND:VEVENT\n"); var events = IcsParser.Parse(ics); Assert.Equal(2, events.Count); Assert.Equal(["1", "2"], events.Select(e => e.Uid)); Assert.Equal(["NAT", "Mat_E"], events.Select(e => e.Summary)); } [Fact] public void Parse_EventOhneSummary_SummaryBleibtNull() { var ics = Wrap( "BEGIN:VEVENT\n" + "UID:38229-1957614\n" + "DTSTART;TZID=Europe/Berlin:20260818T092000\n" + "DTEND;TZID=Europe/Berlin:20260818T094000\n" + "LOCATION:Zwingli\n" + "DESCRIPTION:HED\n" + "END:VEVENT\n"); var evt = Assert.Single(IcsParser.Parse(ics)); Assert.Null(evt.Summary); Assert.Equal("HED", evt.Description); } [Fact] public void Parse_EscapedSemikolaInDescription_WirdAufgeloest() { var ics = Wrap( "BEGIN:VEVENT\n" + "UID:38806-2002907-2002910\n" + "DTSTART;TZID=Europe/Berlin:20260818T094000\n" + "SUMMARY:Mat_E\n" + "DESCRIPTION:10a\\; 10b\\; 10c\\; Gastro HED\n" + "END:VEVENT\n"); var evt = Assert.Single(IcsParser.Parse(ics)); Assert.Equal("10a; 10b; 10c; Gastro HED", evt.Description); } [Fact] public void Parse_UtcZeitstempelMitZ_WirdNachEuropeBerlinKonvertiert() { // 20260817T055000Z UTC = 07:50 Europe/Berlin im Sommer (UTC+2) — defensiver Fall, // im echten WebUntis-Export nicht beobachtet (der nutzt durchgehend TZID=Europe/Berlin). var ics = Wrap( "BEGIN:VEVENT\nUID:1\nDTSTART:20260817T055000Z\nEND:VEVENT\n"); var evt = Assert.Single(IcsParser.Parse(ics)); Assert.Equal(new DateOnly(2026, 8, 17), evt.Date); Assert.Equal(new TimeOnly(7, 50), evt.StartTime); } [Fact] public void Parse_StatusCancelled_WirdUebernommen() { var ics = Wrap( "BEGIN:VEVENT\nUID:1\nSTATUS:CANCELLED\nDTSTART;TZID=Europe/Berlin:20260817T075000\nEND:VEVENT\n"); var evt = Assert.Single(IcsParser.Parse(ics)); Assert.Equal("CANCELLED", evt.Status); } [Fact] public void Parse_VeventOhneUidOderOhneDtstart_WirdUebersprungen() { var ics = Wrap( "BEGIN:VEVENT\nDTSTART;TZID=Europe/Berlin:20260817T075000\nSUMMARY:OhneUid\nEND:VEVENT\n" + "BEGIN:VEVENT\nUID:ohne-dtstart\nSUMMARY:X\nEND:VEVENT\n" + "BEGIN:VEVENT\nUID:defektesDatum\nDTSTART;TZID=Europe/Berlin:keinDatum\nEND:VEVENT\n" + "BEGIN:VEVENT\nUID:gueltig\nDTSTART;TZID=Europe/Berlin:20260817T075000\nEND:VEVENT\n"); var events = IcsParser.Parse(ics); var evt = Assert.Single(events); Assert.Equal("gueltig", evt.Uid); } [Fact] public void Parse_GefalteteFortsetzungszeile_WirdAngehaengt() { // RFC-5545 Line-Folding: Fortsetzungszeile beginnt mit einem Leerzeichen. var ics = Wrap( "BEGIN:VEVENT\nUID:1\nDTSTART;TZID=Europe/Berlin:20260817T075000\n" + "DESCRIPTION:10a\\; 10b\\; \n 10c\\; Gastro HED\nEND:VEVENT\n"); var evt = Assert.Single(IcsParser.Parse(ics)); Assert.Equal("10a; 10b; 10c; Gastro HED", evt.Description); } [Fact] public void Parse_LeererText_LiefertLeereListe() { Assert.Empty(IcsParser.Parse("")); } }