Jahresplanimport und Ablgeich von Untis
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class AnnualPlanSettingsServiceTests
|
||||
{
|
||||
private const string SampleUrl = "https://example.org/export.php?type=ics&token=intern";
|
||||
|
||||
private static string TempPath()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-annualplansettingssvc-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IcalUrl_WirdVerschluesseltPersistiertUndWiederGeladen()
|
||||
{
|
||||
var path = TempPath();
|
||||
var service = new AnnualPlanSettingsService(path);
|
||||
service.SetIcalUrl(SampleUrl);
|
||||
service.SetEnabled(true);
|
||||
|
||||
var raw = File.ReadAllText(Path.Combine(path, "annual-plan-settings.json"));
|
||||
var reloaded = new AnnualPlanSettingsService(path);
|
||||
|
||||
Assert.DoesNotContain("token=intern", raw);
|
||||
Assert.Equal(SampleUrl, reloaded.GetIcalUrl());
|
||||
Assert.True(reloaded.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearIcalUrl_EntferntUrlUndDeaktiviertDenImport()
|
||||
{
|
||||
var service = new AnnualPlanSettingsService(TempPath());
|
||||
service.SetIcalUrl(SampleUrl);
|
||||
service.SetEnabled(true);
|
||||
|
||||
service.ClearIcalUrl();
|
||||
|
||||
Assert.False(service.IsConfigured);
|
||||
Assert.False(service.Enabled);
|
||||
Assert.Null(service.GetIcalUrl());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class AnnualPlanSyncServiceTests
|
||||
{
|
||||
private static string Calendar(params string[] events) =>
|
||||
"BEGIN:VCALENDAR\nVERSION:2.0\n" + string.Join("", events) + "END:VCALENDAR\n";
|
||||
|
||||
private static string Event(string uid, string title, string date = "20260823") =>
|
||||
$"BEGIN:VEVENT\nUID:{uid}\nDTSTART;VALUE=DATE:{date}\nSUMMARY:{title}\nEND:VEVENT\n";
|
||||
|
||||
private static AnnualPlanSyncService Build(FakeAnnualPlanEvents events) =>
|
||||
new(new HttpClient(), TestSupport.BuildAnnualPlanSettingsService(), events);
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_LegtAn_AktualisiertIdempotent_UndEntferntVerschwundeneTermine()
|
||||
{
|
||||
var events = new FakeAnnualPlanEvents();
|
||||
using var service = Build(events);
|
||||
|
||||
var first = service.ProcessIcsText(Calendar(Event("1", "Konferenz"), Event("2", "Messe")));
|
||||
var originalId = events.GetByExternalId("1")!.Id;
|
||||
var unchanged = service.ProcessIcsText(Calendar(Event("1", "Konferenz"), Event("2", "Messe")));
|
||||
var changed = service.ProcessIcsText(Calendar(Event("1", "Konferenz verschoben", "20260824")));
|
||||
|
||||
Assert.Equal((2, 0, 0), (first.Added, first.Updated, first.Deleted));
|
||||
Assert.Equal(0, unchanged.ChangeCount);
|
||||
Assert.Equal((0, 1, 1), (changed.Added, changed.Updated, changed.Deleted));
|
||||
Assert.Equal(originalId, events.GetByExternalId("1")!.Id);
|
||||
Assert.Null(events.GetByExternalId("2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_DefekterTeilimport_VeraendertBestehendenBestandNicht()
|
||||
{
|
||||
var events = new FakeAnnualPlanEvents();
|
||||
events.Add(new AnnualPlanEvent
|
||||
{
|
||||
ExternalId = "bestand", Title = "Bestehend",
|
||||
StartDate = new DateOnly(2026, 8, 23), EndDate = new DateOnly(2026, 8, 23), IsAllDay = true,
|
||||
});
|
||||
using var service = Build(events);
|
||||
var broken = Calendar(Event("neu", "Neu"),
|
||||
"BEGIN:VEVENT\nUID:defekt\nSUMMARY:Ohne Datum\nEND:VEVENT\n");
|
||||
|
||||
Assert.Throws<FormatException>(() => service.ProcessIcsText(broken));
|
||||
|
||||
var remaining = Assert.Single(events.GetAll());
|
||||
Assert.Equal("bestand", remaining.ExternalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_EntferntDenLokalenFeedCache()
|
||||
{
|
||||
var events = new FakeAnnualPlanEvents();
|
||||
events.Add(new AnnualPlanEvent
|
||||
{
|
||||
ExternalId = "1", StartDate = new DateOnly(2026, 8, 23),
|
||||
EndDate = new DateOnly(2026, 8, 23), IsAllDay = true,
|
||||
});
|
||||
using var service = Build(events);
|
||||
var notified = false;
|
||||
service.DataChanged += () => notified = true;
|
||||
|
||||
service.Clear();
|
||||
|
||||
Assert.Empty(events.GetAll());
|
||||
Assert.True(notified);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,8 @@ public sealed class DashboardViewModelTests
|
||||
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
|
||||
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
|
||||
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
||||
FakeSessions? sessions = null, FakeEntries? entries = null)
|
||||
FakeSessions? sessions = null, FakeEntries? entries = null,
|
||||
FakeAnnualPlanEvents? annualPlanEvents = null)
|
||||
{
|
||||
lessons ??= new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
@@ -55,7 +56,7 @@ public sealed class DashboardViewModelTests
|
||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
|
||||
substitutions ?? new FakeSubstitutionEntries());
|
||||
substitutions ?? new FakeSubstitutionEntries(), annualPlanEvents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -244,6 +245,37 @@ public sealed class DashboardViewModelTests
|
||||
e => e.Kind == CalendarEventKind.ParticipationSession && e.Subtitle == "Aufsatz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kalender_ZeigtMehrtaegigenJahresplanParallelZumUnterrichtAn()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var annualPlan = new FakeAnnualPlanEvents();
|
||||
annualPlan.Add(new AnnualPlanEvent
|
||||
{
|
||||
ExternalId = "fahrt", Title = "Klassenfahrt 6a",
|
||||
Description = "Aushang beachten", Location = "Jugendherberge",
|
||||
CalendarGroup = "Lehrkräfte", StartDate = today, EndDate = today.AddDays(2),
|
||||
StartTime = new TimeOnly(13, 30), EndTime = new TimeOnly(15, 0), IsAllDay = false,
|
||||
});
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today, Topic = "Redox" },
|
||||
annualPlanEvents: annualPlan);
|
||||
|
||||
var day = vm.CalendarDays.Single(d => d.Date == today);
|
||||
Assert.True(day.HasLesson);
|
||||
Assert.True(day.HasAnnualPlanEvent);
|
||||
|
||||
vm.SelectCalendarDayCommand.Execute(day);
|
||||
|
||||
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Lesson);
|
||||
var annual = Assert.Single(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.AnnualPlan);
|
||||
Assert.Equal("Klassenfahrt 6a", annual.Title);
|
||||
Assert.Equal("Aushang beachten", annual.Description);
|
||||
Assert.Contains("13:30", annual.Subtitle);
|
||||
Assert.Null(annual.GroupId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kalender_AusStundeErzeugteSitzungErzeugtKeinenDoppelteintrag()
|
||||
{
|
||||
|
||||
@@ -39,6 +39,14 @@ public static class TestSupport
|
||||
return new WebUntisSettingsService(tempPath);
|
||||
}
|
||||
|
||||
/// Analog zu den übrigen dateibasierten Feed-Einstellungen: eigenes Temp-Verzeichnis.
|
||||
public static AnnualPlanSettingsService BuildAnnualPlanSettingsService()
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-annualplansettings-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
return new AnnualPlanSettingsService(tempPath);
|
||||
}
|
||||
|
||||
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||
public static SyncSettingsService BuildSyncSettingsService()
|
||||
{
|
||||
@@ -453,6 +461,19 @@ public class FakeUntisSlotMappings : IUntisSlotMappingRepository
|
||||
public void Delete(Guid id) => _all.RemoveAll(m => m.Id == id);
|
||||
}
|
||||
|
||||
public class FakeAnnualPlanEvents : IAnnualPlanEventRepository
|
||||
{
|
||||
private readonly List<AnnualPlanEvent> _all = [];
|
||||
public void Add(AnnualPlanEvent e) => _all.Add(e);
|
||||
public List<AnnualPlanEvent> GetAll() => _all.ToList();
|
||||
public List<AnnualPlanEvent> GetByRange(DateOnly from, DateOnly to) =>
|
||||
_all.Where(e => e.StartDate <= to && e.EndDate >= from).ToList();
|
||||
public AnnualPlanEvent? GetByExternalId(string externalId) =>
|
||||
_all.FirstOrDefault(e => e.ExternalId == externalId);
|
||||
public void Save(AnnualPlanEvent entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
||||
}
|
||||
|
||||
public class FakeWorkTasks : IWorkTaskRepository
|
||||
{
|
||||
private readonly List<WorkTask> _all = [];
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class SettingsViewModelTests
|
||||
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
||||
new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildAnnualPlanSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
||||
eventQueue ?? TestSupport.BuildEventQueue(), TestSupport.BuildAppLogger(),
|
||||
syncKeyStatus ?? TestSupport.BuildSyncKeyStatus(),
|
||||
@@ -291,6 +292,7 @@ public sealed class SettingsViewModelTests
|
||||
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildAnnualPlanSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
@@ -317,6 +319,7 @@ public sealed class SettingsViewModelTests
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildAnnualPlanSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
@@ -347,6 +350,7 @@ public sealed class SettingsViewModelTests
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildAnnualPlanSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
|
||||
Reference in New Issue
Block a user