Vorarbeit: WebUntis-API

This commit is contained in:
2026-08-24 12:14:36 +02:00
parent a30aab0fa5
commit 5841d96c5b
10 changed files with 1377 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
using System.Net;
using System.Text;
using Microsoft.Extensions.Options;
using Xunit;
namespace LehrerApp.Api.Tests;
public sealed class WebUntisClientTests
{
[Fact]
public async Task AufeinanderfolgendeAbrufe_VerwendenDieselbeSessionBisZumDispose()
{
var latin1 = Encoding.Latin1.GetBytes(
"id\texternKey\tklasse.name\tlongName\tforeName\r\n" +
"1\t10\t7a\tMüller\tAda\r\n" +
"2\t20\t8b\tMeier\tBerta\r\n");
var handler = new QueueHandler(
Json("{\"jsonrpc\":\"2.0\",\"result\":{\"sessionId\":\"session-1\"}}"),
Json("{\"data\":{\"finished\":true,\"reportParams\":\"foo=bar\"}}"),
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(latin1),
},
Json("{\"jsonrpc\":\"2.0\",\"result\":[{\"id\":7,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"),
Json("{\"jsonrpc\":\"2.0\",\"result\":{}}"));
var client = CreateClient(handler);
var result = await client.GetStudentReportAsync("7a", CancellationToken.None);
var schoolYears = await client.GetSchoolYearsAsync(CancellationToken.None);
var student = Assert.Single(result.Students);
Assert.Equal("Ada Müller", student.DisplayName);
Assert.Equal("7a", result.ClassNameFilter);
Assert.Equal("2026/27", Assert.Single(schoolYears).Name);
Assert.Equal(4, handler.Requests.Count);
Assert.Contains("jsonrpc.do?school=meine-schule", handler.Requests[0].Uri);
Assert.Contains("\"method\":\"authenticate\"", handler.Requests[0].Body);
Assert.Contains("reports.do?name=Student", handler.Requests[1].Uri);
Assert.Contains("schoolname=\"_", handler.Requests[1].Cookie);
Assert.EndsWith("reports.do?foo=bar", handler.Requests[2].Uri);
Assert.Contains("\"method\":\"getSchoolyears\"", handler.Requests[3].Body);
Assert.Single(handler.Requests,
request => request.Body.Contains("\"method\":\"authenticate\""));
await client.DisposeAsync();
Assert.Equal(5, handler.Requests.Count);
Assert.Contains("\"method\":\"logout\"", handler.Requests[4].Body);
}
[Fact]
public async Task GetTimetableAsync_SendetElementUndZeitraumAlsSeparateAbfrage()
{
var handler = new QueueHandler(
Json("{\"result\":{\"sessionId\":\"s\"}}"),
Json("{\"result\":[{\"id\":99,\"date\":20260824,\"startTime\":800,\"endTime\":845," +
"\"kl\":[{\"id\":4,\"name\":\"7a\"}],\"te\":[],\"su\":[],\"ro\":[]}]}"),
Json("{\"result\":{}}"));
var client = CreateClient(handler);
var periods = await client.GetTimetableAsync(UntisTimetableElementType.Teacher, 42,
20260824, 20260828, CancellationToken.None);
var period = Assert.Single(periods);
Assert.Equal(99, period.Id);
Assert.Equal("7a", Assert.Single(period.Classes).Name);
Assert.Contains("\"method\":\"getTimetable\"", handler.Requests[1].Body);
Assert.Contains("\"element\":{\"id\":42,\"type\":2}", handler.Requests[1].Body);
Assert.Contains("\"startDate\":20260824", handler.Requests[1].Body);
await client.DisposeAsync();
}
[Fact]
public async Task FehlendeKonfiguration_BrichtVorHttpRequestAb()
{
var handler = new QueueHandler();
var client = new WebUntisClient(new HttpClient(handler), Options.Create(new WebUntisOptions()));
await Assert.ThrowsAsync<WebUntisConfigurationException>(
() => client.GetSchoolYearsAsync(CancellationToken.None));
Assert.Empty(handler.Requests);
await client.DisposeAsync();
}
[Fact]
public async Task FehlzeitenUndKlassenbuch_WerdenNachSchuelerAbgerufenUndTypisiert()
{
var handler = new QueueHandler(
Json("{\"result\":{\"sessionId\":\"s\"}}"),
Json("{\"result\":{\"periodsWithAbsences\":[" +
"{\"studentId\":9001,\"date\":20260901,\"startTime\":800,\"endTime\":845," +
"\"absentTime\":45,\"checked\":true,\"absenceReason\":\"Krank\"," +
"\"excuseStatus\":\"entschuldigt\",\"subjectId\":12,\"teacherIds\":[\"7\"]}," +
"{\"studentId\":9999,\"date\":20260901,\"startTime\":800,\"endTime\":845," +
"\"absentTime\":45,\"checked\":false}]}}"),
Json("{\"result\":[{\"studentid\":9001,\"surname\":\"Müller\",\"forname\":\"Ada\"," +
"\"date\":20260902,\"subject\":\"MA\",\"categoryId\":3,\"reason\":\"Material\"," +
"\"text\":\"Buch vergessen\"}]}"),
Json("{\"result\":[{\"id\":3,\"name\":\"Material\",\"longName\":\"Material vergessen\"," +
"\"groupId\":2}]}"),
Json("{\"result\":[{\"id\":2,\"name\":\"Organisation\"}]}"),
Json("{\"result\":{}}"));
var client = CreateClient(handler);
var report = await client.GetStudentAbsencesAsync(9001, 20260801, 20270731,
CancellationToken.None);
var entries = await client.GetClassRegisterEntriesAsync(17, 20260801, 20270731,
CancellationToken.None);
var categories = await client.GetClassRegisterCategoriesAsync(CancellationToken.None);
var groups = await client.GetClassRegisterCategoryGroupsAsync(CancellationToken.None);
var absence = Assert.Single(report.Absences);
Assert.Equal(45, report.AbsentMinutes);
Assert.Equal("entschuldigt", absence.ExcuseStatus);
Assert.Equal(7, Assert.Single(absence.TeacherIds));
var entry = Assert.Single(entries);
Assert.Equal("Ada Müller", entry.DisplayName);
Assert.Equal("Buch vergessen", entry.Text);
Assert.Equal(3, Assert.Single(categories).Id);
Assert.Equal("Organisation", Assert.Single(groups).Name);
Assert.Contains("\"method\":\"getTimetableWithAbsences\"", handler.Requests[1].Body);
Assert.Contains("\"method\":\"getClassregEvents\"", handler.Requests[2].Body);
Assert.Contains("\"id\":17,\"type\":5", handler.Requests[2].Body);
Assert.Single(handler.Requests,
request => request.Body.Contains("\"method\":\"authenticate\""));
await client.DisposeAsync();
Assert.Contains("\"method\":\"logout\"", handler.Requests[^1].Body);
}
private static WebUntisClient CreateClient(HttpMessageHandler handler) => new(
new HttpClient(handler),
Options.Create(new WebUntisOptions
{
School = "meine-schule",
Host = "meine-schule.webuntis.com",
Username = "api-user",
Password = "secret",
Client = "tests",
}));
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<HttpResponseMessage> _responses = new(responses);
public List<CapturedRequest> Requests { get; } = [];
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
Requests.Add(new CapturedRequest(
request.RequestUri?.ToString() ?? "",
request.Content is null ? "" : await request.Content.ReadAsStringAsync(cancellationToken),
string.Join("; ", request.Headers.TryGetValues("Cookie", out var cookies) ? cookies : [])));
return _responses.Count > 0
? _responses.Dequeue()
: throw new InvalidOperationException("Keine Testantwort mehr vorhanden.");
}
}
private sealed record CapturedRequest(string Uri, string Body, string Cookie);
}