import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:marianum_mobile/api/api_error.dart'; import 'package:marianum_mobile/api/errors/auth_exception.dart'; import 'package:marianum_mobile/api/errors/error_mapper.dart'; import 'package:marianum_mobile/api/errors/network_exception.dart'; import 'package:marianum_mobile/api/errors/not_found_exception.dart'; import 'package:marianum_mobile/api/errors/parse_exception.dart'; import 'package:nextcloud/nextcloud.dart'; void main() { group('errorToUserMessage', () { test('null falls back to the default message', () { expect(errorToUserMessage(null), contains('Etwas ist schiefgelaufen')); }); test('AppException returns its own userMessage', () { final exception = AuthException.unauthorized(); expect(errorToUserMessage(exception), exception.userMessage); }); test('SocketException maps to NetworkException message', () { expect( errorToUserMessage(const SocketException('boom')), const NetworkException().userMessage, ); }); test( 'TimeoutException maps to the timeout-specific NetworkException message', () { expect( errorToUserMessage(TimeoutException('slow')), NetworkException.timeout().userMessage, ); }, ); test('http.ClientException maps to NetworkException message', () { expect( errorToUserMessage(http.ClientException('failed')), const NetworkException().userMessage, ); }); test('HandshakeException maps to a TLS-specific message', () { final message = errorToUserMessage(const HandshakeException('bad cert')); expect(message, contains('sichere Verbindung')); expect(message, contains('Geräte-Uhrzeit')); }); test('FormatException maps to ParseException message', () { expect( errorToUserMessage(const FormatException('bad json')), const ParseException().userMessage, ); }); test('ApiError surfaces only the first line of its message', () { final err = ApiError('Boom\nGET https://example.com/foo'); expect(errorToUserMessage(err), 'Boom'); }); test('ApiError with empty message falls back to default', () { final err = ApiError(''); expect(errorToUserMessage(err), contains('Etwas ist schiefgelaufen')); }); test('unknown error type falls back', () { expect( errorToUserMessage(StateError('weird')), contains('Etwas ist schiefgelaufen'), ); }); test('custom fallback overrides the default', () { expect(errorToUserMessage(null, fallback: 'meins'), 'meins'); }); test('DioException connectionTimeout maps to timeout NetworkException', () { final ex = DioException( requestOptions: RequestOptions(path: '/x'), type: DioExceptionType.connectionTimeout, ); expect(errorToUserMessage(ex), NetworkException.timeout().userMessage); }); test('DioException connectionError maps to NetworkException', () { final ex = DioException( requestOptions: RequestOptions(path: '/x'), type: DioExceptionType.connectionError, ); expect(errorToUserMessage(ex), const NetworkException().userMessage); }); test('DioException badResponse maps to the server error message', () { final ex = DioException( requestOptions: RequestOptions(path: '/x'), type: DioExceptionType.badResponse, response: Response( requestOptions: RequestOptions(path: '/x'), statusCode: 503, ), ); // The status code lives in the technical details, not the message. expect( errorToUserMessage(ex), contains('konnte die Anfrage gerade nicht verarbeiten'), ); expect(errorToTechnicalDetails(ex), contains('503')); }); }); group('DynamiteApiException mapping', () { test('500 maps to the server error message without the raw dump', () { const ex = DynamiteApiException(500, {'server': 'nginx'}, ''); expect( errorToUserMessage(ex), contains('konnte die Anfrage gerade nicht verarbeiten'), ); expect(errorToTechnicalDetails(ex), 'HTTP 500'); expect(errorToTechnicalDetails(ex), isNot(contains('nginx'))); expect(errorAllowsRetry(ex), isTrue); }); test('5xx details include a trimmed body preview', () { const ex = DynamiteApiException(503, {}, ' Service\n Unavailable '); expect(errorToTechnicalDetails(ex), 'HTTP 503 body=Service Unavailable'); }); test('long bodies are capped in the details', () { final ex = DynamiteApiException(502, const {}, 'x' * 600); final details = errorToTechnicalDetails(ex)!; expect(details, startsWith('HTTP 502 body=')); expect(details, endsWith('…')); expect(details.length, lessThan(600)); }); test('401 maps to the unauthorized AuthException', () { const ex = DynamiteApiException(401, {}, ''); expect( errorToUserMessage(ex), AuthException.unauthorized().userMessage, ); expect(errorAllowsRetry(ex), isFalse); }); test('403 maps to the forbidden AuthException', () { const ex = DynamiteApiException(403, {}, ''); expect(errorToUserMessage(ex), AuthException.forbidden().userMessage); expect(errorAllowsRetry(ex), isFalse); }); test('404 maps to NotFoundException', () { const ex = DynamiteApiException(404, {}, ''); expect(errorToUserMessage(ex), const NotFoundException().userMessage); expect(errorAllowsRetry(ex), isFalse); }); test('429 maps to a rate-limit message that allows retry', () { const ex = DynamiteApiException(429, {}, ''); expect(errorToUserMessage(ex), contains('Zu viele Anfragen')); expect(errorAllowsRetry(ex), isTrue); }); }); group('errorToTechnicalDetails', () { test('null returns null', () { expect(errorToTechnicalDetails(null), isNull); }); test('AppException uses its technicalDetails when set', () { final ex = AuthException.unauthorized(technicalDetails: 'http 401, foo'); expect(errorToTechnicalDetails(ex), 'http 401, foo'); }); test('AppException without details falls back to toString()', () { final ex = AuthException.unauthorized(); expect(errorToTechnicalDetails(ex), ex.toString()); }); test('arbitrary object stringifies', () { expect(errorToTechnicalDetails(StateError('x')), contains('x')); }); }); group('errorAllowsRetry', () { test('null allows retry by default', () { expect(errorAllowsRetry(null), isTrue); }); test('AuthException disallows retry (allowRetry=false)', () { expect(errorAllowsRetry(AuthException.unauthorized()), isFalse); }); test('NetworkException allows retry (allowRetry=true)', () { expect(errorAllowsRetry(const NetworkException()), isTrue); }); test('non-AppException allows retry by default', () { expect(errorAllowsRetry(StateError('x')), isTrue); }); }); }