WIP (unstable): WebUntis-iCal-Abgleich für Vertretungen/Ausfälle
Erkennt Vertretungen, Ausfälle und Zusatzaufsichten aus dem persönlichen WebUntis-iCal-Feed und schreibt sie automatisch als SubstitutionEntry. Bekannter offener Bug: es tauchen weiterhin falsche Vertretungen für Stunden auf, die real unverändert sind — wird in einem Folge-Commit untersucht, deshalb vorerst auf diesem Branch statt main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,14 @@ public static class TestSupport
|
||||
new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]),
|
||||
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
|
||||
|
||||
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||
public static WebUntisSettingsService BuildWebUntisSettingsService()
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-webuntissettings-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
return new WebUntisSettingsService(tempPath);
|
||||
}
|
||||
|
||||
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||
public static SyncSettingsService BuildSyncSettingsService()
|
||||
{
|
||||
@@ -422,10 +430,29 @@ public class FakeSubstitutionEntries : ISubstitutionEntryRepository
|
||||
public void Add(SubstitutionEntry e) => _all.Add(e);
|
||||
public List<SubstitutionEntry> GetAll() => _all.ToList();
|
||||
public List<SubstitutionEntry> GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList();
|
||||
public SubstitutionEntry? GetByExternalId(string externalId) => _all.FirstOrDefault(e => e.ExternalId == externalId);
|
||||
public void Save(SubstitutionEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
||||
}
|
||||
|
||||
public class FakeUntisSnapshots : IUntisSnapshotRepository
|
||||
{
|
||||
private readonly List<UntisSnapshotEntry> _all = [];
|
||||
public void Add(UntisSnapshotEntry e) => _all.Add(e);
|
||||
public List<UntisSnapshotEntry> GetAll() => _all.ToList();
|
||||
public void Save(UntisSnapshotEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
||||
}
|
||||
|
||||
public class FakeUntisSlotMappings : IUntisSlotMappingRepository
|
||||
{
|
||||
private readonly List<UntisSlotMapping> _all = [];
|
||||
public void Add(UntisSlotMapping m) => _all.Add(m);
|
||||
public List<UntisSlotMapping> GetAll() => _all.ToList();
|
||||
public void Save(UntisSlotMapping mapping) { _all.RemoveAll(m => m.Id == mapping.Id); _all.Add(mapping); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(m => m.Id == id);
|
||||
}
|
||||
|
||||
public class FakeWorkTasks : IWorkTaskRepository
|
||||
{
|
||||
private readonly List<WorkTask> _all = [];
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class SettingsViewModelTests
|
||||
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath),
|
||||
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
||||
new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
||||
eventQueue ?? TestSupport.BuildEventQueue(), TestSupport.BuildAppLogger(),
|
||||
syncKeyStatus ?? TestSupport.BuildSyncKeyStatus(),
|
||||
@@ -289,6 +290,7 @@ public sealed class SettingsViewModelTests
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
@@ -314,6 +316,7 @@ public sealed class SettingsViewModelTests
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
@@ -343,6 +346,7 @@ public sealed class SettingsViewModelTests
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildWebUntisSettingsService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
using Xunit;
|
||||
@@ -12,7 +13,8 @@ public sealed class TimetableViewModelTests
|
||||
FakeTimetableSlots slots, FakeGroups groups, FakeSchoolHolidays? holidays = null,
|
||||
FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null,
|
||||
SchoolCalendarSettingsService? calendarSettings = null,
|
||||
FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null)
|
||||
FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null,
|
||||
FakeUntisSlotMappings? untisMappings = null, WebUntisSettingsService? untisSettings = null)
|
||||
{
|
||||
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf
|
||||
// (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
||||
@@ -25,7 +27,8 @@ public sealed class TimetableViewModelTests
|
||||
holidays ?? new FakeSchoolHolidays(),
|
||||
calendarSettings ?? new SchoolCalendarSettingsService(tempPath),
|
||||
new PublicHolidayService(), new SchoolYearService(),
|
||||
supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries());
|
||||
supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries(),
|
||||
untisMappings ?? new FakeUntisSlotMappings(), untisSettings ?? TestSupport.BuildWebUntisSettingsService());
|
||||
}
|
||||
|
||||
/// Nächstes Datum ab (inkl.) <paramref name="from"/>, das auf einen Wochentag Mo-Fr fällt —
|
||||
@@ -727,6 +730,31 @@ public sealed class TimetableViewModelTests
|
||||
Assert.Equal("Vertretung für Fr. Schmidt", mondayCell.SupervisionLocation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ZusaetzlicheAufsichtInDerFolgewoche_ErscheintImWochenrasterNachNavigation()
|
||||
{
|
||||
// Nutzer-Feedback: "Auch die Extra-Aufsicht ist dann nicht im Plan [...] So macht doch der
|
||||
// Sync nur so halb Sinn" - dieselbe Anzeige-Infrastruktur wie beim Ausfall oben, hier für
|
||||
// eine automatisch erkannte zusätzliche Aufsicht ohne reguläre SupervisionDuty.
|
||||
var dateNextWeek = DateInCurrentWeek(DayOfWeek.Monday).AddDays(7);
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
substitutions.Add(new SubstitutionEntry
|
||||
{
|
||||
Date = dateNextWeek, Kind = SubstitutionKind.Supervision, AfterPeriod = 1,
|
||||
Description = "Automatisch über WebUntis-Abgleich erkannt: Zusätzliche Aufsicht laut WebUntis.",
|
||||
});
|
||||
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
|
||||
|
||||
Assert.DoesNotContain(vm.WeekItems, c => c.IsSupervisionRow && c.Text.Contains("n. 1."));
|
||||
|
||||
vm.NextWeekCommand.Execute(null);
|
||||
|
||||
var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 1."));
|
||||
var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First();
|
||||
Assert.True(mondayCell.IsSubstitutionSupervision);
|
||||
Assert.Contains("Zusätzliche Aufsicht", mondayCell.SupervisionLocation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_Wochenraster_VertretungsstundeUeberschreibtNormaleAnzeige()
|
||||
{
|
||||
@@ -930,6 +958,38 @@ public sealed class TimetableViewModelTests
|
||||
Assert.Equal("6a auf Klassenfahrt", cell.Topic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_AusfallInDerFolgewoche_ErscheintImWochenrasterNachNavigation()
|
||||
{
|
||||
// Nutzer-Feedback: "Nächste Woche habe ich eine Stunde Ausfall [...] wäre es doch auch
|
||||
// schön, wenn das im Stundenplan für die nächste Woche irgendwie erkenntlich ist" - genau
|
||||
// der Weg, den ein automatisch von WebUntis erkannter Ausfall nimmt (derselbe
|
||||
// SubstitutionEntry-Mechanismus wie bei Handeinträgen), hier erstmals mit WeekOffset != 0
|
||||
// geprüft.
|
||||
var subject = new Subject { Name = "Naturwissenschaften", ShortName = "NAT" };
|
||||
var group = new LearningGroup { Name = "6a", SubjectId = subject.Id };
|
||||
var dateNextWeek = DateInCurrentWeek(DayOfWeek.Thursday).AddDays(7);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Thursday, PeriodNumber = 3 });
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
substitutions.Add(new SubstitutionEntry
|
||||
{
|
||||
Date = dateNextWeek, Kind = SubstitutionKind.Cancelled, PeriodNumber = 3,
|
||||
Description = "Automatisch über WebUntis-Abgleich erkannt.",
|
||||
});
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]), subjects: new FakeSubjects([subject]), substitutions: substitutions);
|
||||
|
||||
// Aktuelle Woche: an dieser Stelle regulär, noch kein Ausfall sichtbar.
|
||||
var currentWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Thursday && c.PeriodNumber == 3);
|
||||
Assert.False(currentWeekCell.IsCancelled);
|
||||
|
||||
vm.NextWeekCommand.Execute(null);
|
||||
|
||||
var nextWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Thursday && c.PeriodNumber == 3);
|
||||
Assert.True(nextWeekCell.IsCancelled);
|
||||
Assert.Equal("Automatisch über WebUntis-Abgleich erkannt.", nextWeekCell.Topic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_Wochenraster_KeinAusfall_ZeigtNormaleStunde()
|
||||
{
|
||||
@@ -1041,4 +1101,79 @@ public sealed class TimetableViewModelTests
|
||||
|
||||
Assert.Single(vm.UpcomingExams);
|
||||
}
|
||||
|
||||
// ── WebUntis-Abweichung ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Load_UntisAbgleichNichtAktiviert_ZeigtKeineAbweichung()
|
||||
{
|
||||
var group = new LearningGroup { Name = "10c" };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||
|
||||
Assert.False(vm.HasUntisMismatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_UntisAktivSlotOhneBestaetigteZuordnung_ZeigtAbweichung()
|
||||
{
|
||||
var group = new LearningGroup { Name = "10c" };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||
var untisSettings = TestSupport.BuildWebUntisSettingsService();
|
||||
untisSettings.SetIcalUrl("https://example.org/ical");
|
||||
untisSettings.SetEnabled(true);
|
||||
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]), untisSettings: untisSettings);
|
||||
|
||||
Assert.True(vm.HasUntisMismatch);
|
||||
Assert.Contains("1 Stundenplan-Eintrag", vm.UntisMismatchLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_UntisAktivMitBestaetigterZuordnung_ZeigtKeineAbweichung()
|
||||
{
|
||||
var group = new LearningGroup { Name = "10c" };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Monday, PeriodNumber = 1, GroupId = group.Id, Confirmed = true,
|
||||
});
|
||||
var untisSettings = TestSupport.BuildWebUntisSettingsService();
|
||||
untisSettings.SetIcalUrl("https://example.org/ical");
|
||||
untisSettings.SetEnabled(true);
|
||||
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]), untisMappings: mappings, untisSettings: untisSettings);
|
||||
|
||||
Assert.False(vm.HasUntisMismatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_DoppelstundenZuordnungBestaetigtBeideTimetableSlots()
|
||||
{
|
||||
// Regression: "ich habe aber jetzt alles zugeordnet, und trotzdem erhalte ich die
|
||||
// Warnung, dass 16 Stunden ohne Untis-Zuordnung sind" - eine WebUntis-Doppelstunde
|
||||
// bestätigt beide zugehörigen TimetableSlots, nicht nur den ersten.
|
||||
var group = new LearningGroup { Name = "10c" };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 2 });
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Monday, PeriodNumber = 1, CoveredPeriods = [1, 2],
|
||||
GroupId = group.Id, Confirmed = true,
|
||||
});
|
||||
var untisSettings = TestSupport.BuildWebUntisSettingsService();
|
||||
untisSettings.SetIcalUrl("https://example.org/ical");
|
||||
untisSettings.SetEnabled(true);
|
||||
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]), untisMappings: mappings, untisSettings: untisSettings);
|
||||
|
||||
Assert.False(vm.HasUntisMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class UntisMappingReviewDialogViewModelTests
|
||||
{
|
||||
private static string BuildTempPath()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-untisreview-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static UntisSyncService BuildUntisSyncService(FakeUntisSlotMappings mappings) => new(
|
||||
new HttpClient(), new WebUntisSettingsService(BuildTempPath()),
|
||||
new FakeUntisSnapshots(), mappings, new FakeSubstitutionEntries(), new FakeGroups([]),
|
||||
new FakeTimetableSlots(), new FakeSupervisionDuties(), new FakeSchoolHolidays(),
|
||||
new PublicHolidayService(), new SchoolCalendarSettingsService(BuildTempPath()),
|
||||
new PeriodScheduleService(BuildTempPath()), new UntisMatchingService(), new UntisDiffService());
|
||||
|
||||
private static UntisMappingReviewDialogViewModel BuildDialogVm(FakeUntisSlotMappings mappings,
|
||||
List<LearningGroup>? groups = null) =>
|
||||
new(BuildUntisSyncService(mappings), mappings, groups ?? [], new FakeSubjects([]));
|
||||
|
||||
private static List<UntisGroupOption> Options(params LearningGroup[] groups) =>
|
||||
groups.Select(g => new UntisGroupOption(g, null)).ToList();
|
||||
|
||||
private static UntisSlotMatch BuildLessonMatch(Guid? groupId, string classToken = "10c") => new()
|
||||
{
|
||||
Pattern = new UntisWeeklyPattern
|
||||
{
|
||||
Weekday = DayOfWeek.Monday, StartTime = new TimeOnly(7, 50), EndTime = new TimeOnly(9, 20),
|
||||
Summary = "SOL", ClassTokens = [classToken], OccurrenceCount = 40,
|
||||
},
|
||||
PeriodNumber = 1, SuggestedGroupId = groupId, IsConfident = groupId is not null,
|
||||
};
|
||||
|
||||
private static UntisSlotMatch BuildSupervisionMatch() => new()
|
||||
{
|
||||
Pattern = new UntisWeeklyPattern
|
||||
{
|
||||
Weekday = DayOfWeek.Tuesday, StartTime = new TimeOnly(9, 20), EndTime = new TimeOnly(9, 40),
|
||||
Summary = null, ClassTokens = [], OccurrenceCount = 40,
|
||||
},
|
||||
AfterPeriod = 2,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void UntisMappingRow_VorschlagWirdVorausgewaehlt()
|
||||
{
|
||||
var group = new LearningGroup { Name = "10c" };
|
||||
var row = new UntisMappingRow(BuildLessonMatch(group.Id), Options(group), existing: null);
|
||||
|
||||
Assert.Equal(group.Id, row.SelectedGroup?.Group.Id);
|
||||
Assert.True(row.IsConfident);
|
||||
Assert.False(row.IsSupervisionCandidate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UntisMappingRow_OhneVorschlag_BleibtLeer()
|
||||
{
|
||||
var row = new UntisMappingRow(BuildLessonMatch(null), [], existing: null);
|
||||
|
||||
Assert.Null(row.SelectedGroup);
|
||||
Assert.False(row.IsConfident);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UntisGroupOption_GleicherNameVerschiedeneFaecher_ZeigtFachImDisplayLabel()
|
||||
{
|
||||
// Nutzer-Feedback: "Meine Klasse habe ich 3-mal. Ohne das Fach dabei, kann ich nicht
|
||||
// sicher die richtige Lerngruppe hier auswählen."
|
||||
var option = new UntisGroupOption(new LearningGroup { Name = "10c" }, "Chemie");
|
||||
|
||||
Assert.Equal("10c (Chemie)", option.DisplayLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UntisGroupOption_OhneFach_ZeigtNurDenNamen()
|
||||
{
|
||||
var option = new UntisGroupOption(new LearningGroup { Name = "10c" }, null);
|
||||
|
||||
Assert.Equal("10c", option.DisplayLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UntisMappingRow_AufsichtsMuster_WirdAlsSupervisionCandidateErkanntUndKannAufgeloestWerden()
|
||||
{
|
||||
// Nutzer-Feedback: "Zwei Termine sind meine Aufsichten [...] vom Zeitraster und von der
|
||||
// Dauer her könnten die erfasst werden" - kein Klassenbezug, aber AfterPeriod auflösbar.
|
||||
var row = new UntisMappingRow(BuildSupervisionMatch(), [], existing: null);
|
||||
|
||||
Assert.True(row.IsSupervisionCandidate);
|
||||
Assert.True(row.CanResolve);
|
||||
Assert.False(row.ConfirmAsSupervision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_SchreibtNurZeilenMitAusgewaehlterGruppeAlsBestaetigt()
|
||||
{
|
||||
var groupA = new LearningGroup { Name = "10c" };
|
||||
var groupB = new LearningGroup { Name = "10d" };
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
var vm = BuildDialogVm(mappings, [groupA, groupB]);
|
||||
vm.Rows.Add(new UntisMappingRow(BuildLessonMatch(groupA.Id), Options(groupA, groupB), existing: null));
|
||||
vm.Rows.Add(new UntisMappingRow(BuildLessonMatch(null, "9z"), Options(groupA, groupB), existing: null)); // ignoriert
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
var saved = Assert.Single(mappings.GetAll());
|
||||
Assert.True(saved.Confirmed);
|
||||
Assert.Equal(SubstitutionKind.Lesson, saved.Kind);
|
||||
Assert.Equal(groupA.Id, saved.GroupId);
|
||||
Assert.Equal(1, saved.PeriodNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_AufsichtBestaetigt_SchreibtSupervisionMapping()
|
||||
{
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
var vm = BuildDialogVm(mappings);
|
||||
var row = new UntisMappingRow(BuildSupervisionMatch(), [], existing: null) { ConfirmAsSupervision = true };
|
||||
vm.Rows.Add(row);
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
var saved = Assert.Single(mappings.GetAll());
|
||||
Assert.Equal(SubstitutionKind.Supervision, saved.Kind);
|
||||
Assert.Null(saved.GroupId);
|
||||
Assert.Equal(2, saved.AfterPeriod);
|
||||
Assert.True(saved.Confirmed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_ErneutesSpeichernDerselbenZeile_AktualisiertStattDupliziert()
|
||||
{
|
||||
// Regression für "Kann es sein, dass er meine Verbesserungen gar nicht einspeichert" -
|
||||
// wiederholtes Bestätigen desselben Slots darf keine zweite Zeile anlegen (sonst
|
||||
// ArgumentException in UntisDiffService.Diff beim nächsten Poll, siehe dortigen Test).
|
||||
var groupA = new LearningGroup { Name = "10c" };
|
||||
var groupB = new LearningGroup { Name = "10d" };
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
var vm1 = BuildDialogVm(mappings, [groupA, groupB]);
|
||||
vm1.Rows.Add(new UntisMappingRow(BuildLessonMatch(groupA.Id), Options(groupA, groupB), existing: null));
|
||||
vm1.SaveCommand.Execute(null);
|
||||
|
||||
var firstSaved = Assert.Single(mappings.GetAll());
|
||||
|
||||
// Dialog erneut geöffnet - die bereits bestätigte Gruppe muss vorbefüllt sein.
|
||||
var existing = mappings.GetAll().Single();
|
||||
var row2 = new UntisMappingRow(BuildLessonMatch(groupA.Id), Options(groupA, groupB), existing);
|
||||
Assert.Equal(groupA.Id, row2.SelectedGroup?.Group.Id);
|
||||
|
||||
row2.SelectedGroup = Options(groupB).Single(); // Nutzer korrigiert
|
||||
var vm2 = BuildDialogVm(mappings, [groupA, groupB]);
|
||||
vm2.Rows.Add(row2);
|
||||
vm2.SaveCommand.Execute(null);
|
||||
|
||||
var saved = Assert.Single(mappings.GetAll());
|
||||
Assert.Equal(firstSaved.Id, saved.Id);
|
||||
Assert.Equal(groupB.Id, saved.GroupId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_NutzerAendertVorschlag_UebernimmtNeueAuswahl()
|
||||
{
|
||||
var groupA = new LearningGroup { Name = "10c" };
|
||||
var groupB = new LearningGroup { Name = "10d" };
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
var vm = BuildDialogVm(mappings, [groupA, groupB]);
|
||||
var row = new UntisMappingRow(BuildLessonMatch(groupA.Id), Options(groupA, groupB), existing: null);
|
||||
row.SelectedGroup = Options(groupB).Single();
|
||||
vm.Rows.Add(row);
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.Equal(groupB.Id, Assert.Single(mappings.GetAll()).GroupId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
/// Tests für den HTTP-freien Verarbeitungskern von UntisSyncService — kein echter Abruf nötig,
|
||||
/// synthetischer ICS-Text nach dem realen WebUntis-Format (siehe Planungsdokument).
|
||||
public sealed class UntisSyncServiceTests
|
||||
{
|
||||
private static string BuildIcs(string uid, string dtstart, string? summary, string description, string status = "CONFIRMED") =>
|
||||
"BEGIN:VCALENDAR\nVERSION:2.0\n" +
|
||||
"BEGIN:VEVENT\n" +
|
||||
$"UID:{uid}\n" +
|
||||
$"STATUS:{status}\n" +
|
||||
$"DTSTART;TZID=Europe/Berlin:{dtstart}\n" +
|
||||
$"DTEND;TZID=Europe/Berlin:{dtstart}\n" +
|
||||
(summary is null ? "" : $"SUMMARY:{summary}\n") +
|
||||
$"DESCRIPTION:{description}\n" +
|
||||
"END:VEVENT\nEND:VCALENDAR\n";
|
||||
|
||||
private static string EmptyIcs() => "BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR\n";
|
||||
|
||||
private static UntisSyncService BuildService(FakeUntisSnapshots? snapshots = null,
|
||||
FakeUntisSlotMappings? mappings = null, FakeSubstitutionEntries? substitutions = null,
|
||||
FakeGroups? groups = null, FakeTimetableSlots? timetableSlots = null,
|
||||
FakeSupervisionDuties? supervisionDuties = null, FakeSchoolHolidays? schoolHolidays = null) => new(
|
||||
new HttpClient(), new WebUntisSettingsService(BuildTempPath()),
|
||||
snapshots ?? new FakeUntisSnapshots(), mappings ?? new FakeUntisSlotMappings(),
|
||||
substitutions ?? new FakeSubstitutionEntries(), groups ?? new FakeGroups([]),
|
||||
timetableSlots ?? new FakeTimetableSlots(), supervisionDuties ?? new FakeSupervisionDuties(),
|
||||
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(),
|
||||
new SchoolCalendarSettingsService(BuildTempPath()),
|
||||
new PeriodScheduleService(BuildTempPath()), new UntisMatchingService(), new UntisDiffService());
|
||||
|
||||
private static string BuildTempPath()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-untissync-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_UnveraendertesEreignis_SchreibtKeineVertretungAberSnapshot()
|
||||
{
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Monday, StartTime = new TimeOnly(7, 50), Summary = "SOL",
|
||||
ClassToken = "10c", GroupId = Guid.NewGuid(), PeriodNumber = 1, Confirmed = true,
|
||||
});
|
||||
var snapshots = new FakeUntisSnapshots();
|
||||
var service = BuildService(snapshots: snapshots, mappings: mappings);
|
||||
|
||||
var result = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "SOL", "10c HED"));
|
||||
|
||||
Assert.Equal(1, result.EventCount);
|
||||
Assert.Equal(0, result.SubstitutionCount);
|
||||
Assert.Single(snapshots.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_AbweichendesFach_SchreibtVertretungUndAktualisiertBeiWiederholung()
|
||||
{
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Monday, StartTime = new TimeOnly(7, 50), Summary = "SOL",
|
||||
ClassToken = "10c", GroupId = Guid.NewGuid(), PeriodNumber = 1, Confirmed = true,
|
||||
});
|
||||
var snapshots = new FakeUntisSnapshots();
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(snapshots: snapshots, mappings: mappings, substitutions: substitutions);
|
||||
|
||||
var first = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "NAT", "10c HED"));
|
||||
Assert.Equal(1, first.SubstitutionCount);
|
||||
Assert.Single(substitutions.GetAll());
|
||||
|
||||
// Erneuter Poll mit demselben (noch immer abweichenden) Termin darf keinen zweiten
|
||||
// Eintrag erzeugen - Idempotenz über SubstitutionEntry.ExternalId.
|
||||
var second = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "NAT", "10c HED"));
|
||||
|
||||
Assert.Equal(1, second.SubstitutionCount);
|
||||
Assert.Single(substitutions.GetAll());
|
||||
}
|
||||
|
||||
// ── Regression: "Am 27.08. fällt eine Stunde NAT in der 8c aus. Dieser Ausfall steht nicht
|
||||
// im Plan [...] die Klasse ist dort weg" — end-to-end über ProcessIcsText, inkl. der neuen
|
||||
// Ferien-Ausschluss-Verdrahtung (ISchoolHolidayRepository -> UntisDiffService.freeDates).
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_VonAnfangAnFehlendeStunde_WirdAlsAusfallGemeldet()
|
||||
{
|
||||
var missingDate = DateTime.Today.AddDays(3);
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = missingDate.DayOfWeek, StartTime = new TimeOnly(9, 40), Summary = "NAT",
|
||||
ClassToken = "8c", GroupId = Guid.NewGuid(), PeriodNumber = 3, Confirmed = true,
|
||||
};
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(mapping);
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(mappings: mappings, substitutions: substitutions);
|
||||
|
||||
// Kein Termin für missingDate im Feed (die 8c ist weg) - ein unbeteiligter, etwas
|
||||
// späterer Termin belegt lediglich den Feed-Abdeckungshorizont darüber hinaus.
|
||||
var coverageDate = missingDate.AddDays(2);
|
||||
var result = service.ProcessIcsText(BuildIcs("other", coverageDate.ToString("yyyyMMdd") + "T093000", "ANDERES", "9x HED"));
|
||||
|
||||
Assert.Equal(1, result.SubstitutionCount);
|
||||
var entry = Assert.Single(substitutions.GetAll());
|
||||
Assert.Equal(SubstitutionKind.Cancelled, entry.Kind);
|
||||
Assert.Equal(3, entry.PeriodNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_FehlendeStundeAnFerientag_WirdNichtGemeldet()
|
||||
{
|
||||
var missingDate = DateTime.Today.AddDays(3);
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = missingDate.DayOfWeek, StartTime = new TimeOnly(9, 40), Summary = "NAT",
|
||||
ClassToken = "8c", GroupId = Guid.NewGuid(), PeriodNumber = 3, Confirmed = true,
|
||||
};
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(mapping);
|
||||
var schoolHolidays = new FakeSchoolHolidays();
|
||||
schoolHolidays.Add(new SchoolHoliday
|
||||
{
|
||||
StartDate = DateOnly.FromDateTime(missingDate), EndDate = DateOnly.FromDateTime(missingDate),
|
||||
});
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(mappings: mappings, substitutions: substitutions, schoolHolidays: schoolHolidays);
|
||||
|
||||
var coverageDate = missingDate.AddDays(2);
|
||||
var result = service.ProcessIcsText(BuildIcs("other", coverageDate.ToString("yyyyMMdd") + "T093000", "ANDERES", "9x HED"));
|
||||
|
||||
Assert.Equal(0, result.SubstitutionCount);
|
||||
Assert.Empty(substitutions.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_OhneBestaetigteZuordnung_SchreibtNieEineVertretung()
|
||||
{
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(substitutions: substitutions);
|
||||
|
||||
var result = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "NAT", "10c HED"));
|
||||
|
||||
Assert.Equal(0, result.SubstitutionCount);
|
||||
Assert.Empty(substitutions.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildMatchPreview_LiefertVorschlagFuerBekannteGruppe()
|
||||
{
|
||||
var group = new LearningGroup { Name = "10c" };
|
||||
var service = BuildService(groups: new FakeGroups([group]));
|
||||
|
||||
var preview = service.BuildMatchPreview(BuildIcs("1", "20260817T075000", "SOL", "10c HED"));
|
||||
|
||||
Assert.Equal(1, preview.EventCount);
|
||||
var match = Assert.Single(preview.Matches.Matches);
|
||||
Assert.Equal(group.Id, match.SuggestedGroupId);
|
||||
}
|
||||
|
||||
// ── Nutzer-Feedback: "Ich habe eine Aufsicht vertretungsweise bekommen [...] Die fehlt
|
||||
// natürlich in der Woche drauf wieder. Wird dann bis zum Ende des gültigen Stundenplans diese
|
||||
// Stunde als entfallene Aufsicht geführt?" — die "entfallen"-Meldung ist an die konkrete,
|
||||
// tatsächlich gesehene Snapshot-Zeile eines Datums gekoppelt, nicht an eine dauerhaft
|
||||
// erwartete wöchentliche Wiederholung: sobald sie einmal als entfallen gemeldet und aus dem
|
||||
// Snapshot entfernt wurde, gibt es nichts mehr, das in einer Folgewoche erneut "verschwinden"
|
||||
// könnte, solange WebUntis für diesen Slot keinen neuen Termin mehr listet. Mit einer
|
||||
// passenden regulären SupervisionDuty, damit dieser Test ausschließlich den
|
||||
// Verschwinden-Mechanismus prüft, unabhängig von der "Zusätzliche Aufsicht"-Erkennung unten.
|
||||
[Fact]
|
||||
public void ProcessIcsText_RegulaereAufsichtVerschwindetEinmalig_MeldetEntfallenNurEinmal()
|
||||
{
|
||||
var eventDate = DateTime.Today.AddDays(3); // innerhalb des 14-Tage-Lookaheads
|
||||
var dtstart = eventDate.ToString("yyyyMMdd") + "T093000";
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = eventDate.DayOfWeek, StartTime = new TimeOnly(9, 30),
|
||||
Kind = SubstitutionKind.Supervision, AfterPeriod = 1, Confirmed = true,
|
||||
};
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(mapping);
|
||||
var duties = new FakeSupervisionDuties();
|
||||
duties.Add(new SupervisionDuty { Weekday = eventDate.DayOfWeek, AfterPeriod = 1, Location = "Pausenhof" });
|
||||
var snapshots = new FakeUntisSnapshots();
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(snapshots: snapshots, mappings: mappings, substitutions: substitutions, supervisionDuties: duties);
|
||||
|
||||
// Poll 1: Feed enthält die reguläre Aufsicht wie erwartet - keine Meldung nötig.
|
||||
var first = service.ProcessIcsText(BuildIcs("v1", dtstart, null, "HED"));
|
||||
Assert.Equal(0, first.SubstitutionCount);
|
||||
Assert.Single(snapshots.GetAll());
|
||||
|
||||
// Poll 2: WebUntis listet an diesem Slot keinen Termin mehr (einmalig von jemand anderem
|
||||
// übernommen). Genau EINE "entfallen"-Meldung, Snapshot-Zeile wird bereinigt.
|
||||
var second = service.ProcessIcsText(EmptyIcs());
|
||||
Assert.Equal(1, second.SubstitutionCount);
|
||||
var entry = Assert.Single(substitutions.GetAll());
|
||||
Assert.Equal(SubstitutionKind.Supervision, entry.Kind);
|
||||
Assert.Empty(snapshots.GetAll());
|
||||
|
||||
// Poll 3, 4, ...: weiterhin kein Termin an diesem Slot - keine erneute Meldung, "bis zum
|
||||
// Ende des gültigen Stundenplans" bleibt es bei dem einen Eintrag.
|
||||
var third = service.ProcessIcsText(EmptyIcs());
|
||||
Assert.Equal(0, third.SubstitutionCount);
|
||||
Assert.Single(substitutions.GetAll());
|
||||
}
|
||||
|
||||
// ── Nutzer-Feedback: "Auch die Extra-Aufsicht ist dann nicht im Plan [...] So macht doch der
|
||||
// Sync nur so halb Sinn" — eine bestätigte Aufsichts-Zuordnung OHNE passende reguläre
|
||||
// SupervisionDuty (Einstellungen "Aufsichten") ist selbst schon die meldenswerte Vertretung
|
||||
// und soll deshalb sofort bei jedem Vorkommen als SubstitutionEntry sichtbar werden, nicht
|
||||
// erst bei einer Abweichung davon.
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_ZusaetzlicheAufsichtOhneRegulaereDuty_WirdSofortGemeldet()
|
||||
{
|
||||
var eventDate = DateTime.Today.AddDays(3);
|
||||
var dtstart = eventDate.ToString("yyyyMMdd") + "T093000";
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = eventDate.DayOfWeek, StartTime = new TimeOnly(9, 30),
|
||||
Kind = SubstitutionKind.Supervision, AfterPeriod = 1, Confirmed = true,
|
||||
};
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(mapping);
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(mappings: mappings, substitutions: substitutions);
|
||||
|
||||
var result = service.ProcessIcsText(BuildIcs("v1", dtstart, null, "HED"));
|
||||
|
||||
Assert.Equal(1, result.SubstitutionCount);
|
||||
var entry = Assert.Single(substitutions.GetAll());
|
||||
Assert.Equal(SubstitutionKind.Supervision, entry.Kind);
|
||||
Assert.Equal(1, entry.AfterPeriod);
|
||||
Assert.Contains("Zusätzliche Aufsicht", entry.Description);
|
||||
|
||||
// Erneuter Poll mit demselben, weiterhin unverändert vorhandenen Termin darf keinen
|
||||
// zweiten Eintrag erzeugen (Idempotenz über ExternalId=Uid).
|
||||
var second = service.ProcessIcsText(BuildIcs("v1", dtstart, null, "HED"));
|
||||
Assert.Equal(1, second.SubstitutionCount);
|
||||
Assert.Single(substitutions.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_AufsichtMitRegulaererDuty_MeldetNichtsBeiNormalerAnwesenheit()
|
||||
{
|
||||
var eventDate = DateTime.Today.AddDays(3);
|
||||
var dtstart = eventDate.ToString("yyyyMMdd") + "T093000";
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = eventDate.DayOfWeek, StartTime = new TimeOnly(9, 30),
|
||||
Kind = SubstitutionKind.Supervision, AfterPeriod = 1, Confirmed = true,
|
||||
};
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(mapping);
|
||||
var duties = new FakeSupervisionDuties();
|
||||
duties.Add(new SupervisionDuty { Weekday = eventDate.DayOfWeek, AfterPeriod = 1, Location = "Pausenhof" });
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(mappings: mappings, substitutions: substitutions, supervisionDuties: duties);
|
||||
|
||||
var result = service.ProcessIcsText(BuildIcs("v1", dtstart, null, "HED"));
|
||||
|
||||
Assert.Equal(0, result.SubstitutionCount);
|
||||
Assert.Empty(substitutions.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfirmMappings_SpeichertAlsBestaetigt()
|
||||
{
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
var service = BuildService(mappings: mappings);
|
||||
var mapping = new UntisSlotMapping { Weekday = DayOfWeek.Monday, GroupId = Guid.NewGuid() };
|
||||
|
||||
service.ConfirmMappings([mapping]);
|
||||
|
||||
Assert.True(Assert.Single(mappings.GetAll()).Confirmed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class WebUntisSettingsServiceTests
|
||||
{
|
||||
private const string SampleUrl = "https://example.webuntis.com/WebUntis/ical_export?school=example&id=test&elemId=1&token=geheim";
|
||||
|
||||
private static string BuildTempPath()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-webuntissettingssvc-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetEnabled_PersistiertUeberNeueInstanz()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
new WebUntisSettingsService(path).SetEnabled(true);
|
||||
|
||||
var reloaded = new WebUntisSettingsService(path);
|
||||
|
||||
Assert.True(reloaded.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetIcalUrl_IstVerschluesseltAbrufbar()
|
||||
{
|
||||
var service = new WebUntisSettingsService(BuildTempPath());
|
||||
|
||||
service.SetIcalUrl(SampleUrl);
|
||||
|
||||
Assert.True(service.IsConfigured);
|
||||
Assert.Equal(SampleUrl, service.GetIcalUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IcalUrl_UeberlebtNeueInstanzMitDemselbenPfad()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
new WebUntisSettingsService(path).SetIcalUrl(SampleUrl);
|
||||
|
||||
var reloaded = new WebUntisSettingsService(path);
|
||||
|
||||
Assert.True(reloaded.IsConfigured);
|
||||
Assert.Equal(SampleUrl, reloaded.GetIcalUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KonfigurationsdateiEnthaeltNichtDasTokenImKlartext()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
var service = new WebUntisSettingsService(path);
|
||||
service.SetIcalUrl(SampleUrl);
|
||||
|
||||
var raw = File.ReadAllText(Path.Combine(path, "webuntis-settings.json"));
|
||||
|
||||
Assert.DoesNotContain("token=geheim", raw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearIcalUrl_EntferntUrlUndDeaktiviert()
|
||||
{
|
||||
var service = new WebUntisSettingsService(BuildTempPath());
|
||||
service.SetIcalUrl(SampleUrl);
|
||||
service.SetEnabled(true);
|
||||
|
||||
service.ClearIcalUrl();
|
||||
|
||||
Assert.False(service.IsConfigured);
|
||||
Assert.Null(service.GetIcalUrl());
|
||||
Assert.False(service.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetLastSync_PersistiertZeitstempelUndStatus()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
var at = new DateTime(2026, 8, 22, 12, 0, 0, DateTimeKind.Utc);
|
||||
new WebUntisSettingsService(path).SetLastSync(at, "3 Vertretungen erkannt");
|
||||
|
||||
var reloaded = new WebUntisSettingsService(path);
|
||||
|
||||
Assert.Equal(at, reloaded.LastSyncAt);
|
||||
Assert.Equal("3 Vertretungen erkannt", reloaded.LastSyncStatus);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user