migrated timetable integration from WebUntis to the MarianumConnect API, implementing a Dio-based client with bearer token authentication, background session validation, and auto-refresh logic.

This commit is contained in:
2026-05-23 17:32:42 +02:00
parent 2858f910c9
commit 93b9929f8f
106 changed files with 2739 additions and 2624 deletions
+67 -80
View File
@@ -1,8 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
import 'package:marianum_mobile/api/webuntis/queries/get_holidays/get_holidays_response.dart';
import 'package:marianum_mobile/api/webuntis/queries/get_timetable/get_timetable_response.dart';
import 'package:marianum_mobile/widget_data/widget_data.dart';
import 'package:marianum_mobile/widget_data/widget_data_mapper.dart';
@@ -23,49 +23,39 @@ CustomTimetableEvent _event({
updatedAt: start,
);
GetTimetableResponseObject _lesson({
required int date,
required int startTime,
required int endTime,
String? code,
McTimetableEntry _lesson({
required DateTime date,
required int startHhmm,
required int endHhmm,
String status = 'REGULAR',
String? subjectName,
String? room,
int teacherId = 1,
String? teacherName,
String? teacherOrgname,
String? substText,
}) => GetTimetableResponseObject(
}) => McTimetableEntry(
id: 1,
date: date,
startTime: startTime,
endTime: endTime,
code: code,
substText: substText,
kl: const [],
te: teacherName != null
startTime: DateTime(1970, 1, 1, startHhmm ~/ 100, startHhmm % 100),
endTime: DateTime(1970, 1, 1, endHhmm ~/ 100, endHhmm % 100),
subjects: subjectName != null ? [subjectName] : const [],
teachers: teacherName != null
? [
GetTimetableResponseObjectTeacher(
teacherId,
teacherName,
teacherName,
teacherOrgname == null ? null : 9,
teacherOrgname,
null,
McTimetableTeacher(
shortName: teacherName,
displayName: teacherName,
originalShortName: teacherOrgname,
originalDisplayName: teacherOrgname,
),
]
: const [],
su: subjectName != null
? [
GetTimetableResponseObjectSubject(
5,
subjectName,
subjectName,
),
]
: const [],
ro: room != null
? [GetTimetableResponseObjectRoom(7, room, room)]
: const [],
rooms: room != null ? [room] : const [],
classNames: const [],
lessonType: 'LESSON',
status: status,
substitutionText: substText,
lessonText: null,
infoText: null,
);
void main() {
@@ -127,8 +117,8 @@ void main() {
test('only includes lessons on the anchor day', () {
final lessons = [
_lesson(date: 20260506, startTime: 800, endTime: 845, subjectName: 'MA'),
_lesson(date: 20260507, startTime: 800, endTime: 845, subjectName: 'EN'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'MA'),
_lesson(date: DateTime(2026, 5, 7), startHhmm: 800, endHhmm: 845, subjectName: 'EN'),
];
final data = WidgetDataMapper.buildDayData(
now: now,
@@ -144,23 +134,24 @@ void main() {
test('classifies cancelled and irregular lessons', () {
final lessons = [
_lesson(
date: 20260506,
startTime: 800,
endTime: 845,
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
code: 'cancelled',
status: 'CANCELLED',
),
_lesson(
date: 20260506,
startTime: 900,
endTime: 945,
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
code: 'irregular',
status: 'IRREGULAR',
teacherName: 'Müller',
),
_lesson(
date: 20260506,
startTime: 1000,
endTime: 1045,
date: DateTime(2026, 5, 6),
startHhmm: 1000,
endHhmm: 1045,
subjectName: 'BIO',
teacherName: 'Müller',
teacherOrgname: 'Schmidt',
@@ -184,15 +175,16 @@ void main() {
});
test('marks day as holiday when in holiday range', () {
final holidays = GetHolidaysResponse({
GetHolidaysResponseObject(
1,
'Pfingsten',
'Pfingstferien',
20260506,
20260510,
),
});
final holidays = TimetableGetHolidaysResponse(
result: [
McHoliday(
shortName: 'Pfingsten',
longName: 'Pfingstferien',
startDate: DateTime(2026, 5, 6),
endDate: DateTime(2026, 5, 10),
),
],
);
final data = WidgetDataMapper.buildDayData(
now: now,
lessons: const [],
@@ -207,21 +199,21 @@ void main() {
test('lessons are sorted by start time', () {
final lessons = [
_lesson(
date: 20260506,
startTime: 1000,
endTime: 1045,
date: DateTime(2026, 5, 6),
startHhmm: 1000,
endHhmm: 1045,
subjectName: 'BIO',
),
_lesson(
date: 20260506,
startTime: 800,
endTime: 845,
date: DateTime(2026, 5, 6),
startHhmm: 800,
endHhmm: 845,
subjectName: 'MA',
),
_lesson(
date: 20260506,
startTime: 900,
endTime: 945,
date: DateTime(2026, 5, 6),
startHhmm: 900,
endHhmm: 945,
subjectName: 'EN',
),
];
@@ -244,9 +236,9 @@ void main() {
test('long event bumps every regular lesson it covers', () {
final lessons = [
_lesson(date: 20260506, startTime: 800, endTime: 845, subjectName: 'MA'),
_lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN'),
_lesson(date: 20260506, startTime: 1000, endTime: 1045, subjectName: 'BIO'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'MA'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 1000, endHhmm: 1045, subjectName: 'BIO'),
];
final events = GetCustomTimetableEventResponse([
_event(
@@ -271,13 +263,9 @@ void main() {
});
test('event + same-slot duplicate regular: kept lesson shows +2', () {
// User scenario: a long custom event covers the slot, and Webuntis
// reports two regular lessons starting at the same time (different
// class group). The user wants "+2" — one for the hidden event, one
// for the parallel regular lesson — not just "+1".
final lessons = [
_lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN'),
_lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'MA'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'MA'),
];
final events = GetCustomTimetableEventResponse([
_event(
@@ -327,7 +315,7 @@ void main() {
test('two events covering the same regular lesson bump it twice', () {
final lessons = [
_lesson(date: 20260506, startTime: 900, endTime: 945, subjectName: 'EN'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 900, endHhmm: 945, subjectName: 'EN'),
];
final events = GetCustomTimetableEventResponse([
_event(
@@ -362,10 +350,10 @@ void main() {
test('contains lessons across the school week', () {
final lessons = [
_lesson(date: 20260504, startTime: 800, endTime: 845, subjectName: 'MO'),
_lesson(date: 20260506, startTime: 800, endTime: 845, subjectName: 'WE'),
_lesson(date: 20260508, startTime: 800, endTime: 845, subjectName: 'FR'),
_lesson(date: 20260511, startTime: 800, endTime: 845, subjectName: 'NEXT'),
_lesson(date: DateTime(2026, 5, 4), startHhmm: 800, endHhmm: 845, subjectName: 'MO'),
_lesson(date: DateTime(2026, 5, 6), startHhmm: 800, endHhmm: 845, subjectName: 'WE'),
_lesson(date: DateTime(2026, 5, 8), startHhmm: 800, endHhmm: 845, subjectName: 'FR'),
_lesson(date: DateTime(2026, 5, 11), startHhmm: 800, endHhmm: 845, subjectName: 'NEXT'),
];
final data = WidgetDataMapper.buildWeekData(
now: now,
@@ -374,7 +362,6 @@ void main() {
rooms: null,
holidays: null,
);
// Anchor is Mon 04.05.; week ends Fri 08.05. exclusive of next Mon
expect(data.anchorDate, DateTime(2026, 5, 4));
expect(
data.lessons.map((l) => l.subjectShort).toList(),