using System.Net; using System.Text; using LehrerApp.Core.Models; using LehrerApp.Desktop.Services; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class UntisReportCacheServiceTests { private static readonly DateOnly Today = new(2026, 8, 25); private static readonly DateOnly HotStart = Today.AddDays(-14); // ── Plan(): reine Entscheidungslogik, keine Repositories/HTTP nötig ────────── [Fact] public void Plan_ErsterAufruf_LaedtHeissesFensterUndDeckungReichtBisFensteranfang() { var plan = UntisReportCacheService.Plan(null, Today, Today, Today, forceRefresh: false, DateTime.UtcNow); Assert.True(plan.RefreshHotWindow); Assert.Equal(HotStart, plan.HotWindowStart); Assert.Equal(Today, plan.HotWindowEnd); Assert.False(plan.FetchColdRange); Assert.Equal(Int(HotStart), plan.ResultingColdCoverageStartDate); } [Fact] public void Plan_InnerhalbEinerStunde_KeinErneutesHeissesFenster() { var state = new UntisCacheFetchState { ClassName = "10c", Kind = UntisCacheKind.Absences, HotWindowFetchedAt = DateTime.UtcNow.AddMinutes(-10), ColdCoverageStartDate = Int(HotStart), }; var plan = UntisReportCacheService.Plan(state, Today, Today, Today, forceRefresh: false, DateTime.UtcNow); Assert.False(plan.RefreshHotWindow); Assert.False(plan.FetchColdRange); } [Fact] public void Plan_ForceRefresh_LoestTrotzKuerzlichemAbrufAus() { var state = new UntisCacheFetchState { ClassName = "10c", Kind = UntisCacheKind.Absences, HotWindowFetchedAt = DateTime.UtcNow.AddMinutes(-10), ColdCoverageStartDate = Int(HotStart), }; var plan = UntisReportCacheService.Plan(state, Today, Today, Today, forceRefresh: true, DateTime.UtcNow); Assert.True(plan.RefreshHotWindow); } [Fact] public void Plan_MehrAlsEineStundeVergangen_FrischtHeissesFensterErneutAuf() { var state = new UntisCacheFetchState { ClassName = "10c", Kind = UntisCacheKind.Absences, HotWindowFetchedAt = DateTime.UtcNow.AddHours(-2), ColdCoverageStartDate = Int(HotStart), }; var plan = UntisReportCacheService.Plan(state, Today, Today, Today, forceRefresh: false, DateTime.UtcNow); Assert.True(plan.RefreshHotWindow); } [Fact] public void Plan_AnfrageVorDemHeissenFenster_KeinHeissesFensterAberKalterErstabruf() { var requestStart = HotStart.AddDays(-30); var requestEnd = HotStart.AddDays(-5); var plan = UntisReportCacheService.Plan(null, requestStart, requestEnd, Today, forceRefresh: false, DateTime.UtcNow); Assert.False(plan.RefreshHotWindow); Assert.True(plan.FetchColdRange); Assert.Equal(requestStart, plan.ColdRangeStart); Assert.Equal(HotStart.AddDays(-1), plan.ColdRangeEnd); Assert.Equal(Int(requestStart), plan.ResultingColdCoverageStartDate); } [Fact] public void Plan_BereitsKaltAbgedeckterBereich_KeinAbrufNoetig() { var coveredSince = HotStart.AddDays(-60); var state = new UntisCacheFetchState { ClassName = "10c", Kind = UntisCacheKind.Absences, HotWindowFetchedAt = DateTime.UtcNow, ColdCoverageStartDate = Int(coveredSince), }; var plan = UntisReportCacheService.Plan(state, coveredSince.AddDays(10), HotStart.AddDays(-20), Today, forceRefresh: false, DateTime.UtcNow); Assert.False(plan.RefreshHotWindow); Assert.False(plan.FetchColdRange); } [Fact] public void Plan_ErweitertKalteAbdeckungNurBisZumBenoetigtenStart() { var coveredSince = HotStart.AddDays(-30); var earlierStart = HotStart.AddDays(-90); var state = new UntisCacheFetchState { ClassName = "10c", Kind = UntisCacheKind.Absences, HotWindowFetchedAt = DateTime.UtcNow, ColdCoverageStartDate = Int(coveredSince), }; var plan = UntisReportCacheService.Plan(state, earlierStart, HotStart.AddDays(-40), Today, forceRefresh: false, DateTime.UtcNow); Assert.False(plan.RefreshHotWindow); Assert.True(plan.FetchColdRange); Assert.Equal(earlierStart, plan.ColdRangeStart); Assert.Equal(coveredSince.AddDays(-1), plan.ColdRangeEnd); Assert.Equal(Int(earlierStart), plan.ResultingColdCoverageStartDate); } private static int Int(DateOnly date) => date.Year * 10000 + date.Month * 100 + date.Day; // ── Ende-zu-Ende: erster Aufruf ruft WebUntis ab, zweiter Aufruf binnen einer Stunde nicht ── [Fact] public async Task GetClassRegisterEventsAsync_ZweiterAufrufBinnenEinerStunde_RuftWebUntisNichtErneutAb() { var todayLabel = DateTime.Today.ToString("dd.MM.yy"); var csv = Encoding.UTF8.GetBytes( "Klasse\tDatum\tFach\tName\tBenutzer\tEintragskategorie\tKategoriegruppe\tText\r\n" + $"10c\t{todayLabel}\tNAT\tMuster Max\tmueller\tMitarb. über Erwart.\tPositiv\tArbeitet gut mit.\r\n"); var handler = new QueueHandler( Json("{\"result\":{\"sessionId\":\"s\"}}"), Json("{\"result\":[{\"id\":1,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"), Json("{\"data\":{\"finished\":true,\"error\":false," + "\"reportParams\":\"get=rpt.tmp&name=ClassregEventPerStudent&format=csv\"}}"), new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(csv) }); var settings = TestSupport.BuildWebUntisSettingsService(); await using var untis = new WebUntisIntegrationService(new HttpClient(handler), settings); await untis.ConnectAsync(new WebUntisCredentials("bk-ostvest", "arche.webuntis.com", "lehrkraft", "geheim")); var absenceCache = new FakeUntisAbsenceCache(); var classRegisterCache = new FakeUntisClassRegisterCache(); var fetchStates = new FakeUntisCacheFetchStates(); var cache = new UntisReportCacheService(untis, absenceCache, classRegisterCache, fetchStates); var today = DateOnly.FromDateTime(DateTime.Today); var first = await cache.GetClassRegisterEventsAsync("10c", today, today); Assert.Single(first); Assert.Equal(4, handler.Requests.Count); var second = await cache.GetClassRegisterEventsAsync("10c", today, today); Assert.Single(second); Assert.Equal(4, handler.Requests.Count); // keine weiteren Anfragen - aus dem Cache bedient } private static HttpResponseMessage Json(string json) => new(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json"), }; private sealed class QueueHandler(params HttpResponseMessage[] responses) : HttpMessageHandler { private readonly Queue _responses = new(responses); public List Requests { get; } = []; protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Requests.Add(request.RequestUri?.ToString() ?? ""); await Task.Yield(); return _responses.Dequeue(); } } }