added guardian letters with chat and multiple answer functionalities
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user