import 'package:flutter_test/flutter_test.dart'; import 'package:emergency/models/country.dart'; import 'package:emergency/models/emergency_number.dart'; import 'package:emergency/models/emergency_type.dart'; import 'package:emergency/providers/emergency_provider.dart'; import 'package:emergency/providers/emergency_status.dart'; import 'package:emergency/repositories/emergency_number_repository.dart'; import 'package:emergency/services/country_code_sources/country_code_source.dart'; import 'package:emergency/services/location_service.dart'; /// Einfache manuelle Fakes statt Mocking-Package (siehe AP4-Vorgabe: kein /// neues Mocking-Package einführen). Beide Basisklassen sind nicht `final`/ /// `sealed`, daher genügt es, `load`/`lookup` bzw. `resolveCountryCode` zu /// überschreiben, statt ein eigenes Interface einzuführen. class _FakeRepository extends EmergencyNumberRepository { _FakeRepository({this.loadError, this.countryToReturn}); Object? loadError; Country? countryToReturn; int loadCallCount = 0; @override Future load() async { loadCallCount++; final error = loadError; if (error != null) throw error; } @override Country? lookup(String isoCode) => countryToReturn; } /// Nur zur Erfüllung des `super()`-Konstruktors von [LocationService] nötig /// (wird nie aufgerufen, da [_FakeLocationService.resolveCountryCode] /// überschrieben ist). Ohne einen expliziten `gpsSource` würde /// `LocationService()` intern ein echtes `GpsCountryCodeSource` (und damit /// `Geocoding()`) konstruieren, was außerhalb eines registrierten /// Platform-Interface (siehe `gps_country_code_source_test.dart`) sofort wirft. class _NoopSource implements CountryCodeSource { const _NoopSource(); @override Future resolve() async => null; } class _FakeLocationService extends LocationService { _FakeLocationService(this.isoCode) : super(simSource: const _NoopSource(), gpsSource: const _NoopSource()); /// Simuliert das Ergebnis der automatischen Kette (SIM → GPS → Cache). String? isoCode; /// Simuliert die manuelle Auswahl (AP6): hat, analog zur echten /// Priorisierung in `LocationService`, Vorrang vor [isoCode]. String? manualIsoCode; int resolveCallCount = 0; int setManualCallCount = 0; String? lastManualCode; @override Future resolveCountryCode() async { resolveCallCount++; return manualIsoCode ?? isoCode; } @override Future setManualCountryCode(String? code) async { setManualCallCount++; lastManualCode = code; manualIsoCode = code; } } const _germany = Country( isoCode: 'DE', nameDe: 'Deutschland', nameEn: 'Germany', flag: '🇩🇪', numbers: [ EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'), ], ); const _france = Country( isoCode: 'FR', nameDe: 'Frankreich', nameEn: 'France', flag: '🇫🇷', numbers: [ EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'), ], ); void main() { group('EmergencyProvider.init()', () { test('erfolgreicher Durchlauf -> status ready mit Land', () async { final provider = EmergencyProvider( repository: _FakeRepository(countryToReturn: _germany), locationService: _FakeLocationService('DE'), ); await provider.init(); expect(provider.status, EmergencyStatus.ready); expect(provider.country, _germany); expect(provider.errorMessage, isNull); }); test('Ländercode nicht ermittelbar -> status countryUnknown', () async { final provider = EmergencyProvider( repository: _FakeRepository(countryToReturn: _germany), locationService: _FakeLocationService(null), ); await provider.init(); expect(provider.status, EmergencyStatus.countryUnknown); expect(provider.country, isNull); }); test('Land nicht in Datenbasis -> status ready mit country == null', () async { final provider = EmergencyProvider( repository: _FakeRepository(countryToReturn: null), locationService: _FakeLocationService('XX'), ); await provider.init(); expect(provider.status, EmergencyStatus.ready); expect(provider.country, isNull); }); test('repository.load() wirft -> status error mit errorMessage', () async { final repository = _FakeRepository(loadError: const FormatException('kaputtes Asset')); final locationService = _FakeLocationService('DE'); final provider = EmergencyProvider(repository: repository, locationService: locationService); await provider.init(); expect(provider.status, EmergencyStatus.error); expect(provider.errorMessage, isNotNull); expect(repository.loadCallCount, 1); // Ländererkennung darf nach einem Ladefehler nicht mehr angestoßen werden. expect(locationService.resolveCallCount, 0); }); test('durchläuft die Status initializing -> locating -> ready', () async { final provider = EmergencyProvider( repository: _FakeRepository(countryToReturn: _germany), locationService: _FakeLocationService('DE'), ); final observedStatuses = []; provider.addListener(() => observedStatuses.add(provider.status)); await provider.init(); expect(observedStatuses, [ EmergencyStatus.initializing, EmergencyStatus.locating, EmergencyStatus.ready, ]); }); }); group('EmergencyProvider.retry()', () { test('wiederholt init() und kann nach einem Fehler erfolgreich sein', () async { final repository = _FakeRepository( loadError: const FormatException('kaputtes Asset'), countryToReturn: _germany, ); final locationService = _FakeLocationService('DE'); final provider = EmergencyProvider(repository: repository, locationService: locationService); await provider.init(); expect(provider.status, EmergencyStatus.error); expect(repository.loadCallCount, 1); repository.loadError = null; await provider.retry(); expect(provider.status, EmergencyStatus.ready); expect(provider.country, _germany); expect(provider.errorMessage, isNull); expect(repository.loadCallCount, 2); }); }); group('EmergencyProvider.selectCountryManually()', () { test('persistiert die Auswahl und stößt init() erneut an', () async { final repository = _FakeRepository(countryToReturn: _germany); final locationService = _FakeLocationService('DE'); final provider = EmergencyProvider(repository: repository, locationService: locationService); await provider.init(); expect(provider.country, _germany); repository.countryToReturn = _france; await provider.selectCountryManually('FR'); expect(locationService.setManualCallCount, 1); expect(locationService.lastManualCode, 'FR'); // init() wurde ein zweites Mal komplett durchlaufen. expect(locationService.resolveCallCount, 2); expect(provider.status, EmergencyStatus.ready); expect(provider.country, _france); }); }); group('EmergencyProvider.clearManualSelection()', () { test('löscht die manuelle Auswahl und ermittelt danach erneut automatisch', () async { // isoCode simuliert das Ergebnis der automatischen Kette (z. B. SIM), // manualIsoCode eine bereits zuvor gesetzte manuelle Auswahl. final locationService = _FakeLocationService('DE')..manualIsoCode = 'FR'; final repository = _FakeRepository(countryToReturn: _france); final provider = EmergencyProvider(repository: repository, locationService: locationService); await provider.init(); expect(provider.country, _france); // manuelle Auswahl hat Vorrang repository.countryToReturn = _germany; await provider.clearManualSelection(); expect(locationService.setManualCallCount, 1); expect(locationService.lastManualCode, isNull); expect(locationService.manualIsoCode, isNull); expect(locationService.resolveCallCount, 2); expect(provider.status, EmergencyStatus.ready); // Nach dem Löschen greift wieder das automatische Ergebnis (DE). expect(provider.country, _germany); }); }); group('EmergencyProvider.repository', () { test('exponiert das intern verwendete Repository', () { final repository = _FakeRepository(countryToReturn: _germany); final provider = EmergencyProvider( repository: repository, locationService: _FakeLocationService('DE'), ); expect(provider.repository, same(repository)); }); }); }