using System.Net; using System.Text; using LehrerApp.Desktop.Services; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class WebUntisIntegrationServiceTests { [Fact] public async Task ConnectUndAbruf_GehenDirektAnWebUntisUndVerwendenDieselbeSession() { var handler = new QueueHandler( Json("{\"result\":{\"sessionId\":\"desktop-session\"}}"), Json("{\"result\":[{\"id\":1,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"), Json("{\"result\":[{\"id\":42,\"name\":\"KUE\",\"longName\":\"Kürbis\",\"active\":true}]}"), Json("{\"result\":{}}")); var settings = TestSupport.BuildWebUntisSettingsService(); await using var service = new WebUntisIntegrationService(new HttpClient(handler), settings); await service.ConnectAsync(new WebUntisCredentials( "bk-ostvest", "arche.webuntis.com", "lehrkraft", "geheim")); var teachers = await service.GetTeachersAsync(); Assert.Equal("KUE", Assert.Single(teachers).Name); Assert.Equal(3, handler.Requests.Count); Assert.All(handler.Requests, request => Assert.StartsWith("https://arche.webuntis.com/WebUntis/", request.Uri)); Assert.DoesNotContain(handler.Requests, request => request.Uri.Contains("/api/webuntis")); Assert.Single(handler.Requests, request => request.Body.Contains("\"method\":\"authenticate\"")); } [Fact] public async Task GetClassAbsencesAsync_LoestKlasseNamensbasiertAufUndAdressiertSieAlsKLPlusId() { var csv = Encoding.UTF8.GetBytes( "Schüler*innen\tExterne Id\tText\tKlasse\tDatum\tWochentag\tFehlstd.\tFehlmin.\tLehrkraft\tFach\t" + "Abwesenheitsgrund\tText\tENr\tErledigt\tAbwesenheit zählt\tEntschuldigungstext\tStundennr.\tStatus\tFehltage\r\n" + "Muster Max\t12345\t\t10c\t24.08.26\tMo.\t1\t45\tHED\tChe\tAbsent\tKrank gemeldet\t555\t24.08.26\ttrue\t\t3\tnicht entsch.\t1\r\n"); var handler = new QueueHandler( Json("{\"result\":{\"sessionId\":\"desktop-session\"}}"), // authenticate (ConnectAsync) Json("{\"result\":[{\"id\":7,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"), // getSchoolyears (ConnectAsync-Validierung) Json("{\"result\":[{\"id\":7,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"), // getSchoolyears (ResolveClassIdAsync) Json("{\"result\":[{\"id\":874,\"name\":\"10c\"}]}"), // getKlassen Json("{\"data\":{\"finished\":true,\"error\":false," + "\"reportParams\":\"get=rpt.tmp&name=AbsencePerStudent&format=csv\"}}"), new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(csv) }, Json("{\"result\":{}}")); // logout (Dispose) var settings = TestSupport.BuildWebUntisSettingsService(); await using var service = new WebUntisIntegrationService(new HttpClient(handler), settings); await service.ConnectAsync(new WebUntisCredentials( "bk-ostvest", "arche.webuntis.com", "lehrkraft", "geheim")); var entries = await service.GetClassAbsencesAsync("10c", new DateOnly(2026, 8, 24), new DateOnly(2026, 8, 28)); var entry = Assert.Single(entries); Assert.Equal("Muster Max", entry.StudentName); Assert.Equal("nicht entsch.", entry.Status); // Zweimal getSchoolyears: einmal als ConnectAsync-Validierung, einmal in ResolveClassIdAsync. Assert.Equal(2, handler.Requests.Count(request => request.Body.Contains("\"method\":\"getSchoolyears\""))); Assert.Single(handler.Requests, request => request.Body.Contains("\"method\":\"getKlassen\"")); Assert.Single(handler.Requests, request => request.Uri.Contains("reports.do?name=AbsencePerStudent") && request.Uri.Contains("klasseOrStudentgroupId=KL874")); } 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(new(request.RequestUri?.ToString() ?? "", request.Content is null ? "" : await request.Content.ReadAsStringAsync(cancellationToken))); return _responses.Dequeue(); } } private sealed record CapturedRequest(string Uri, string Body); }