242 lines
8.2 KiB
Dart
242 lines
8.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:marianum_mobile/api/errors/auth_exception.dart';
|
|
import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/get_parent_letter.dart';
|
|
import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_exception.dart';
|
|
import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_models.dart';
|
|
|
|
const _detailJson = '''
|
|
{
|
|
"id": "l1", "subject": "Wandertag 10b", "preview": "Liebe Eltern",
|
|
"sender": {"displayName": "Hr. Müller"},
|
|
"sentAt": "2026-09-20T08:15:00", "editedAt": null, "read": false,
|
|
"childIds": ["c1"], "attachmentCount": 1, "status": "open",
|
|
"deadline": "2026-10-01T23:59:00",
|
|
"body": "Liebe Eltern,\\n…",
|
|
"attachments": [{"id": "a1", "fileName": "Brief.pdf", "mimeType": "application/pdf", "size": 48211}],
|
|
"request": {
|
|
"fields": [
|
|
{"id": "f1", "type": "single_choice", "label": "Teilnahme?", "required": true,
|
|
"options": [{"id": "o1", "label": "Ja"}, {"id": "o2", "label": "Nein"}]},
|
|
{"id": "f2", "type": "date_range", "label": "Zeitraum", "required": false, "options": null}
|
|
],
|
|
"signatureRequired": true, "deadline": "2026-10-01T23:59:00"
|
|
},
|
|
"children": [
|
|
{"childId": "c1", "status": "done", "editable": false,
|
|
"response": {"respondedAt": "2026-09-21T19:02:11",
|
|
"respondedBy": {"displayName": "A. Hoffmann", "self": true},
|
|
"answers": [{"fieldId": "f1", "optionIds": ["o1"], "text": null}], "signed": true}}
|
|
],
|
|
"thread": {"enabled": true, "messages": [
|
|
{"id": "m1", "author": {"displayName": "Hr. Müller", "self": false}, "body": "Danke", "sentAt": "2026-09-21T20:10:00"}
|
|
]}
|
|
}
|
|
''';
|
|
|
|
void main() {
|
|
group('ParentLetterSummary', () {
|
|
test('parses the documented inbox entry', () {
|
|
final response = ParentLetterListResponse.fromJson({
|
|
'items': [
|
|
{
|
|
'id': 'l1',
|
|
'subject': 'Wandertag',
|
|
'preview': 'Liebe Eltern',
|
|
'sender': {'displayName': 'Hr. Müller'},
|
|
'sentAt': '2026-09-20T08:15:00',
|
|
'editedAt': null,
|
|
'read': false,
|
|
'childIds': ['c1', 'c2'],
|
|
'attachmentCount': 2,
|
|
'status': 'expired',
|
|
'deadline': '2026-10-01T23:59:00',
|
|
},
|
|
],
|
|
'hasMore': true,
|
|
'unreadCount': 3,
|
|
'openCount': 1,
|
|
});
|
|
final letter = response.items.single;
|
|
expect(letter.sender.displayName, 'Hr. Müller');
|
|
expect(letter.sentAt, DateTime(2026, 9, 20, 8, 15));
|
|
expect(letter.read, isFalse);
|
|
expect(letter.childIds, ['c1', 'c2']);
|
|
expect(letter.status, ParentLetterStatus.expired);
|
|
expect(letter.deadline, DateTime(2026, 10, 1, 23, 59));
|
|
expect(response.hasMore, isTrue);
|
|
expect(response.unreadCount, 3);
|
|
expect(response.openCount, 1);
|
|
});
|
|
|
|
test('explicit nulls and unknown values fall back to safe defaults', () {
|
|
final letter = ParentLetterSummary.fromJson({
|
|
'id': 'l1',
|
|
'subject': null,
|
|
'sender': null,
|
|
'sentAt': '2026-09-20T08:15:00',
|
|
'read': null,
|
|
'childIds': null,
|
|
'status': 'archived',
|
|
'deadline': null,
|
|
});
|
|
expect(letter.subject, '');
|
|
expect(letter.sender.displayName, '');
|
|
expect(letter.read, isTrue);
|
|
expect(letter.childIds, isEmpty);
|
|
expect(letter.status, ParentLetterStatus.info);
|
|
expect(letter.deadline, isNull);
|
|
});
|
|
|
|
test('an empty list response is an empty inbox', () {
|
|
final response = ParentLetterListResponse.fromJson(const {});
|
|
expect(response.items, isEmpty);
|
|
expect(response.hasMore, isFalse);
|
|
expect(response.unreadCount, 0);
|
|
});
|
|
});
|
|
|
|
group('ParentLetterDetail', () {
|
|
final detail = ParentLetterDetail.fromJson(
|
|
jsonDecode(_detailJson) as Map<String, dynamic>,
|
|
);
|
|
|
|
test('splits the flat object into summary and content', () {
|
|
expect(detail.summary.id, 'l1');
|
|
expect(detail.summary.status, ParentLetterStatus.open);
|
|
expect(detail.content.body, startsWith('Liebe Eltern'));
|
|
expect(detail.content.attachments.single.size, 48211);
|
|
expect(detail.content.thread.messages.single.author.self, isFalse);
|
|
});
|
|
|
|
test('keeps unknown field types and tolerates null options', () {
|
|
final fields = detail.content.request!.fields;
|
|
expect(fields[0].type, ParentLetterFieldType.singleChoice);
|
|
expect(fields[0].isRequired, isTrue);
|
|
expect(fields[0].options.map((o) => o.id), ['o1', 'o2']);
|
|
expect(fields[1].type, ParentLetterFieldType.unknown);
|
|
expect(fields[1].options, isEmpty);
|
|
});
|
|
|
|
test('parses the per-child response', () {
|
|
final child = detail.content.children.single;
|
|
expect(child.editable, isFalse);
|
|
expect(child.response!.respondedBy.self, isTrue);
|
|
expect(child.response!.signed, isTrue);
|
|
expect(child.response!.answers.single.optionIds, ['o1']);
|
|
});
|
|
|
|
test('a letter without request, children and thread is pure information', () {
|
|
final info = ParentLetterDetail.fromJson({
|
|
'id': 'l2',
|
|
'sentAt': '2026-09-20T08:15:00',
|
|
'request': null,
|
|
'thread': null,
|
|
});
|
|
expect(info.content.request, isNull);
|
|
expect(info.content.children, isEmpty);
|
|
expect(info.content.thread.enabled, isFalse);
|
|
});
|
|
|
|
test('survives the storage round trip', () {
|
|
final restored = ParentLetterDetail.fromJson(
|
|
jsonDecode(jsonEncode(detail.toJson())) as Map<String, dynamic>,
|
|
);
|
|
expect(restored, detail);
|
|
});
|
|
});
|
|
|
|
group('ParentLetterException mapping', () {
|
|
Future<Object> errorOf(int status, Object? body) async {
|
|
final options = RequestOptions(path: 'parent-letters/l1');
|
|
final dio = _ThrowingDio(
|
|
DioException(
|
|
requestOptions: options,
|
|
type: DioExceptionType.badResponse,
|
|
response: Response<dynamic>(
|
|
requestOptions: options,
|
|
statusCode: status,
|
|
data: body,
|
|
),
|
|
),
|
|
);
|
|
try {
|
|
await GetParentLetter(dio: dio).run('l1');
|
|
} catch (e) {
|
|
return e;
|
|
}
|
|
fail('expected an error');
|
|
}
|
|
|
|
test('reads the JSON error code', () async {
|
|
final error = await errorOf(409, {'error': 'response_final'});
|
|
expect(
|
|
error,
|
|
isA<ParentLetterException>()
|
|
.having((e) => e.error, 'error', ParentLetterError.responseFinal)
|
|
.having((e) => e.allowRetry, 'allowRetry', isFalse),
|
|
);
|
|
});
|
|
|
|
test('reads the plain-text "Fehler: <code>" body', () async {
|
|
final error = await errorOf(410, 'Fehler: deadline_passed');
|
|
expect(
|
|
(error as ParentLetterException).error,
|
|
ParentLetterError.deadlinePassed,
|
|
);
|
|
});
|
|
|
|
test('the code wins over the status', () async {
|
|
final error = await errorOf(404, {'error': 'child_not_found'});
|
|
expect(
|
|
(error as ParentLetterException).error,
|
|
ParentLetterError.childNotFound,
|
|
);
|
|
});
|
|
|
|
test('falls back to the status without a known code', () async {
|
|
expect(
|
|
((await errorOf(404, null)) as ParentLetterException).error,
|
|
ParentLetterError.letterNotFound,
|
|
);
|
|
expect(
|
|
((await errorOf(409, 'Fehler: whatever')) as ParentLetterException)
|
|
.error,
|
|
ParentLetterError.responseFinal,
|
|
);
|
|
});
|
|
|
|
test('401 and 5xx keep the generic mapping', () async {
|
|
expect(await errorOf(401, 'Fehler: x'), isA<AuthException>());
|
|
expect(
|
|
await errorOf(500, {'error': 'letter_not_found'}),
|
|
isNot(isA<ParentLetterException>()),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Minimal fake Dio whose `get` always fails with the given exception; every
|
|
/// other member is unused.
|
|
class _ThrowingDio implements Dio {
|
|
final DioException error;
|
|
|
|
_ThrowingDio(this.error);
|
|
|
|
@override
|
|
Future<Response<T>> get<T>(
|
|
String path, {
|
|
Object? data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Options? options,
|
|
CancelToken? cancelToken,
|
|
ProgressCallback? onReceiveProgress,
|
|
}) => Future<Response<T>>.error(error);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) =>
|
|
super.noSuchMethod(invocation);
|
|
}
|