using LehrerApp.Core.Services; using Xunit; namespace LehrerApp.Tests; /// Synthetische Fixtures nach dem am 23.08.2026 geprüften ClassyPlan-Export. public sealed class AnnualPlanIcsParserTests { private static string Calendar(params string[] events) => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:ClassyPlan\n" + string.Join("", events) + "END:VCALENDAR\n"; [Fact] public void Parse_Ganztagstermin_NutztExklusivesDtendAlsInklusivesEnddatum() { var ics = Calendar( "BEGIN:VEVENT\nUID:100-ClassyPlan.app\n" + "DTSTART;VALUE=DATE:20260615\nDTEND;VALUE=DATE:20260620\n" + "SUMMARY:Klassenfahrt 6a 6b 6d\nX-GROUPNAME:Lehrkräfte\nX-COLOR:#4B9FD8\n" + "END:VEVENT\n"); var entry = Assert.Single(AnnualPlanIcsParser.Parse(ics)); Assert.True(entry.IsAllDay); Assert.Equal(new DateOnly(2026, 6, 15), entry.StartDate); Assert.Equal(new DateOnly(2026, 6, 19), entry.EndDate); Assert.Null(entry.StartTime); Assert.Equal("Lehrkräfte", entry.CalendarGroup); } [Fact] public void Parse_UtcTermin_WirdUnterBeruecksichtigungDerSommerzeitNachBerlinKonvertiert() { var ics = Calendar( "BEGIN:VEVENT\nUID:200-ClassyPlan.app\n" + "DTSTART:20260929T113000Z\nDTEND:20260929T130000Z\n" + "LAST-MODIFIED:20260814T095236Z\nSEQUENCE:2\nSTATUS:CONFIRMED\n" + "SUMMARY:Teams 5–10\nLOCATION:R 101\nX-GROUPNAME:Öffentlich\nEND:VEVENT\n"); var entry = Assert.Single(AnnualPlanIcsParser.Parse(ics)); Assert.False(entry.IsAllDay); Assert.Equal(new TimeOnly(13, 30), entry.StartTime); Assert.Equal(new TimeOnly(15, 0), entry.EndTime); Assert.Equal(2, entry.Sequence); Assert.Equal(new DateTime(2026, 8, 14, 9, 52, 36, DateTimeKind.Utc), entry.SourceLastModifiedUtc); } [Fact] public void Parse_EscapingUndZeilenfaltung_WerdenAufgeloest() { var ics = Calendar( "BEGIN:VEVENT\nUID:300-ClassyPlan.app\nDTSTART;VALUE=DATE:20260823\n" + "DESCRIPTION:Erste Zeile\\nZweite Zeile mit sehr langem \n Text\\, Komma\\; Semikolon\n" + "SUMMARY:Fortbildung\nEND:VEVENT\n"); var entry = Assert.Single(AnnualPlanIcsParser.Parse(ics)); Assert.Equal("Erste Zeile\nZweite Zeile mit sehr langem Text, Komma; Semikolon", entry.Description); } [Fact] public void Parse_EinDefektesVevent_LaesstNichtUnbemerktEinenTeilimportZu() { var ics = Calendar( "BEGIN:VEVENT\nUID:gueltig\nDTSTART;VALUE=DATE:20260823\nEND:VEVENT\n", "BEGIN:VEVENT\nUID:defekt\nSUMMARY:Ohne Datum\nEND:VEVENT\n"); var error = Assert.Throws(() => AnnualPlanIcsParser.Parse(ics)); Assert.Contains("DTSTART fehlt", error.Message); } [Fact] public void Parse_DoppelteUid_WirdAbgelehnt() { var ics = Calendar( "BEGIN:VEVENT\nUID:doppelt\nDTSTART;VALUE=DATE:20260823\nEND:VEVENT\n", "BEGIN:VEVENT\nUID:doppelt\nDTSTART;VALUE=DATE:20260824\nEND:VEVENT\n"); Assert.Throws(() => AnnualPlanIcsParser.Parse(ics)); } }