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:
@@ -1,117 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/webuntis/queries/get_rooms/get_rooms_response.dart';
|
||||
import 'package:marianum_mobile/api/webuntis/queries/get_subjects/get_subjects_response.dart';
|
||||
import 'package:marianum_mobile/api/webuntis/services/lesson_resolver.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
|
||||
TimetableState _state({
|
||||
Set<GetSubjectsResponseObject> subjects = const {},
|
||||
Set<GetRoomsResponseObject> rooms = const {},
|
||||
}) => TimetableState(
|
||||
subjects: subjects.isEmpty ? null : GetSubjectsResponse(subjects),
|
||||
rooms: rooms.isEmpty ? null : GetRoomsResponse(rooms),
|
||||
startDate: DateTime(2026, 1, 1),
|
||||
endDate: DateTime(2026, 12, 31),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('LessonResolver.resolveSubject', () {
|
||||
test('returns the matching subject when the id is found', () {
|
||||
final math = GetSubjectsResponseObject(7, 'M', 'Mathe', 'Ma', true);
|
||||
final state = _state(subjects: {math});
|
||||
|
||||
final result = LessonResolver.resolveSubject(state, 7);
|
||||
expect(result.id, 7);
|
||||
expect(result.name, 'M');
|
||||
expect(result.longName, 'Mathe');
|
||||
});
|
||||
|
||||
test('returns the placeholder fallback when id is null', () {
|
||||
final state = _state(subjects: const {});
|
||||
final result = LessonResolver.resolveSubject(state, null);
|
||||
expect(result.id, 0);
|
||||
expect(result.name, '?');
|
||||
expect(result.longName, 'Unbekannt');
|
||||
});
|
||||
|
||||
test('returns the placeholder fallback when id is unknown', () {
|
||||
final math = GetSubjectsResponseObject(7, 'M', 'Mathe', 'Ma', true);
|
||||
final state = _state(subjects: {math});
|
||||
|
||||
final result = LessonResolver.resolveSubject(state, 999);
|
||||
expect(result.id, 0);
|
||||
expect(result.longName, 'Unbekannt');
|
||||
});
|
||||
});
|
||||
|
||||
group('LessonResolver.resolveRoom', () {
|
||||
test('returns the matching room when the id is found', () {
|
||||
final room = GetRoomsResponseObject(
|
||||
3,
|
||||
'A1',
|
||||
'Aula 1',
|
||||
true,
|
||||
'Hauptgebäude',
|
||||
);
|
||||
final state = _state(rooms: {room});
|
||||
|
||||
final result = LessonResolver.resolveRoom(state, 3);
|
||||
expect(result.id, 3);
|
||||
expect(result.name, 'A1');
|
||||
expect(result.building, 'Hauptgebäude');
|
||||
});
|
||||
|
||||
test('returns the placeholder fallback when id is unknown', () {
|
||||
final state = _state(rooms: const {});
|
||||
final result = LessonResolver.resolveRoom(state, 42);
|
||||
expect(result.id, 0);
|
||||
expect(result.name, '?');
|
||||
});
|
||||
});
|
||||
|
||||
group('LessonFormatter', () {
|
||||
test('iconForCode picks the right icon per status', () {
|
||||
expect(
|
||||
LessonFormatter.iconForCode('cancelled').codePoint,
|
||||
isNot(LessonFormatter.iconForCode('irregular').codePoint),
|
||||
);
|
||||
expect(
|
||||
LessonFormatter.iconForCode(null).codePoint,
|
||||
isNot(LessonFormatter.iconForCode('cancelled').codePoint),
|
||||
);
|
||||
});
|
||||
|
||||
test('statusLabel maps known codes to German labels', () {
|
||||
expect(LessonFormatter.statusLabel(null), 'Regulär');
|
||||
expect(LessonFormatter.statusLabel(''), 'Regulär');
|
||||
expect(LessonFormatter.statusLabel('cancelled'), 'Entfällt');
|
||||
expect(LessonFormatter.statusLabel('irregular'), 'Geändert');
|
||||
expect(LessonFormatter.statusLabel('something-else'), 'something-else');
|
||||
});
|
||||
|
||||
test('codePrefix prepends a label for known codes', () {
|
||||
expect(LessonFormatter.codePrefix('cancelled'), 'Entfällt: ');
|
||||
expect(LessonFormatter.codePrefix('irregular'), 'Änderung: ');
|
||||
expect(LessonFormatter.codePrefix(null), '');
|
||||
});
|
||||
|
||||
test('formatLine renders name + (longname) + · extra in that order', () {
|
||||
expect(
|
||||
LessonFormatter.formatLine(
|
||||
'Mathe',
|
||||
longname: 'Mathematik',
|
||||
extra: 'Hauptgebäude',
|
||||
),
|
||||
'Mathe (Mathematik) · Hauptgebäude',
|
||||
);
|
||||
});
|
||||
|
||||
test('formatLine omits longname when it equals name', () {
|
||||
expect(LessonFormatter.formatLine('Mathe', longname: 'Mathe'), 'Mathe');
|
||||
});
|
||||
|
||||
test('formatLine substitutes ? when name is empty', () {
|
||||
expect(LessonFormatter.formatLine(''), '?');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
import 'package:marianum_mobile/view/pages/timetable/data/arbitrary_appointment.dart';
|
||||
import 'package:marianum_mobile/view/pages/timetable/data/calendar_logic.dart';
|
||||
import 'package:marianum_mobile/view/pages/timetable/data/lesson_period_schedule.dart';
|
||||
@@ -27,18 +27,21 @@ Appointment _appt({
|
||||
recurrenceRule: rrule,
|
||||
);
|
||||
|
||||
GetTimetableResponseObject _lesson({String? code}) =>
|
||||
GetTimetableResponseObject(
|
||||
id: 0,
|
||||
date: 0,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
kl: const [],
|
||||
te: const [],
|
||||
su: const [],
|
||||
ro: const [],
|
||||
code: code,
|
||||
);
|
||||
McTimetableEntry _lesson({String status = 'REGULAR'}) => McTimetableEntry(
|
||||
id: 0,
|
||||
date: DateTime(2026, 5, 8),
|
||||
startTime: DateTime(1970, 1, 1, 8),
|
||||
endTime: DateTime(1970, 1, 1, 9),
|
||||
subjects: const [],
|
||||
teachers: const [],
|
||||
rooms: const [],
|
||||
classNames: const [],
|
||||
lessonType: 'LESSON',
|
||||
status: status,
|
||||
substitutionText: null,
|
||||
lessonText: null,
|
||||
infoText: null,
|
||||
);
|
||||
|
||||
CustomTimetableEvent _customEvent() => CustomTimetableEvent(
|
||||
id: 'x',
|
||||
@@ -331,7 +334,7 @@ void main() {
|
||||
end: _at(2026, 5, 8, 10),
|
||||
);
|
||||
final regular = _appt(
|
||||
id: WebuntisAppointment(_lesson()),
|
||||
id: LessonAppointment(_lesson()),
|
||||
start: _at(2026, 5, 8, 8),
|
||||
end: _at(2026, 5, 8, 10),
|
||||
);
|
||||
@@ -345,19 +348,19 @@ void main() {
|
||||
(c) => c.appointment.id is CustomAppointment,
|
||||
);
|
||||
final regularCell = result.firstWhere(
|
||||
(c) => c.appointment.id is WebuntisAppointment,
|
||||
(c) => c.appointment.id is LessonAppointment,
|
||||
);
|
||||
expect(customCell.lane, lessThan(regularCell.lane));
|
||||
});
|
||||
|
||||
test('cancelled lesson lands left of a non-cancelled one on tie', () {
|
||||
final cancelled = _appt(
|
||||
id: WebuntisAppointment(_lesson(code: 'cancelled')),
|
||||
id: LessonAppointment(_lesson(status: 'CANCELLED')),
|
||||
start: _at(2026, 5, 8, 8),
|
||||
end: _at(2026, 5, 8, 10),
|
||||
);
|
||||
final regular = _appt(
|
||||
id: WebuntisAppointment(_lesson()),
|
||||
id: LessonAppointment(_lesson()),
|
||||
start: _at(2026, 5, 8, 8),
|
||||
end: _at(2026, 5, 8, 10),
|
||||
);
|
||||
@@ -366,13 +369,13 @@ void main() {
|
||||
regular,
|
||||
cancelled,
|
||||
], maxLanes: 2).whereType<LaidOutAppointment>().toList();
|
||||
String? codeOf(LaidOutAppointment c) {
|
||||
String? statusOf(LaidOutAppointment c) {
|
||||
final id = c.appointment.id;
|
||||
return id is WebuntisAppointment ? id.lesson.code : null;
|
||||
return id is LessonAppointment ? id.entry.status : null;
|
||||
}
|
||||
|
||||
final cancelledCell = result.firstWhere((c) => codeOf(c) == 'cancelled');
|
||||
final regularCell = result.firstWhere((c) => codeOf(c) == null);
|
||||
final cancelledCell = result.firstWhere((c) => statusOf(c) == 'CANCELLED');
|
||||
final regularCell = result.firstWhere((c) => statusOf(c) == 'REGULAR');
|
||||
expect(cancelledCell.lane, lessThan(regularCell.lane));
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user