diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d47e69a..82cccf6 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,6 +13,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { @@ -39,6 +40,10 @@ android { } } +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} + flutter { source = "../.." } diff --git a/lib/state/rent_controller.dart b/lib/state/rent_controller.dart index ed827cd..18de39f 100644 --- a/lib/state/rent_controller.dart +++ b/lib/state/rent_controller.dart @@ -41,6 +41,7 @@ class RentController extends ChangeNotifier { RentController({ StorageService? storageService, ForexRateApiService? exchangeService, + NotificationService? notificationService, }) : _storageService = storageService ?? StorageService(), _exchangeService = exchangeService ?? @@ -48,7 +49,8 @@ class RentController extends ChangeNotifier { httpClient: http.Client(), apiKey: _exchangeRateApiKeyFromEnv(), ) { - _notificationService = NotificationService(_onNotificationAction); + _notificationService = + notificationService ?? NotificationService(_onNotificationAction); } static String _exchangeRateApiKeyFromEnv() { diff --git a/test/backup_restore_compatibility_test.dart b/test/backup_restore_compatibility_test.dart new file mode 100644 index 0000000..2c49dfb --- /dev/null +++ b/test/backup_restore_compatibility_test.dart @@ -0,0 +1,202 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:rental_income_tracker/models/app_settings.dart'; +import 'package:rental_income_tracker/models/monthly_rent_record.dart'; +import 'package:rental_income_tracker/services/notification_service.dart'; +import 'package:rental_income_tracker/services/open_exchange_service.dart'; +import 'package:rental_income_tracker/services/storage_service.dart'; +import 'package:rental_income_tracker/state/rent_controller.dart'; + +class _FakeStorageService extends StorageService { + AppSettings _settings = AppSettings.defaults(); + Map _records = {}; + + @override + Future loadSettings() async => _settings; + + @override + Future saveSettings(AppSettings settings) async { + _settings = settings; + } + + @override + Future> loadRecords() async => + Map.from(_records); + + @override + Future saveRecords(Map records) async { + _records = Map.from(records); + } +} + +class _FakeExchangeService extends ForexRateApiService { + _FakeExchangeService({required this.quote}) + : super(httpClient: http.Client(), apiKey: 'test-key'); + + final double quote; + + @override + Future convertUsdToSek({ + required DateTime estDate, + required double amount, + }) async { + return ForexConversionResult(quote: quote, result: quote * amount); + } +} + +class _FakeNotificationService extends NotificationService { + _FakeNotificationService() : super((_) async {}); + + @override + Future initialize() async {} + + @override + Future processPendingActions() async {} + + @override + Future scheduleForMonth({ + required int year, + required int month, + required int quietHourStart, + required int fallbackHour, + }) async {} + + @override + Future cancelForMonth(int year, int month) async {} + + @override + Future cancelAll() async {} +} + +void main() { + group('backup/import compatibility', () { + test( + 'round-trips current backup format through export and import', + () async { + final sourceStorage = _FakeStorageService(); + final sourceController = RentController( + storageService: sourceStorage, + exchangeService: _FakeExchangeService(quote: 10.5), + notificationService: _FakeNotificationService(), + ); + + await sourceController.updateRentUsd(2450); + await sourceController.setMonthStatusManually( + year: 2025, + month: 1, + status: PaymentStatus.onTime, + paidUsd: 2000, + ); + await sourceController.setMonthStatusManually( + year: 2025, + month: 2, + status: PaymentStatus.late, + paidUsd: 1800, + ); + + final tempDir = await Directory.systemTemp.createTemp('backup_test_'); + addTearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + final backupPath = await sourceController.exportBackupToDirectory( + tempDir.path, + ); + + final targetStorage = _FakeStorageService(); + final targetController = RentController( + storageService: targetStorage, + exchangeService: _FakeExchangeService(quote: 99), + notificationService: _FakeNotificationService(), + ); + + await targetController.importBackupFromFile(backupPath); + + expect(targetController.settings.rentUsd, 2450); + + targetController.selectYear(2025); + final rows = targetController.buildRowsForSelectedYear(); + final january = rows.firstWhere((row) => row.month == 1); + final february = rows.firstWhere((row) => row.month == 2); + + expect(january.status, PaymentStatus.onTime); + expect(january.usdAmount, 2000); + expect(january.sekAmount, 21000); + expect(january.effectiveUsdToSekRate, 10.5); + + expect(february.status, PaymentStatus.late); + expect(february.usdAmount, 1800); + expect(february.sekAmount, 18900); + expect(february.effectiveUsdToSekRate, 10.5); + + final persistedSettings = await targetStorage.loadSettings(); + final persistedRecords = await targetStorage.loadRecords(); + expect(persistedSettings.rentUsd, 2450); + expect( + persistedRecords.keys, + containsAll(['2025-01', '2025-02']), + ); + }, + ); + + test( + 'imports legacy export payloads with missing optional fields', + () async { + final tempDir = await Directory.systemTemp.createTemp('backup_legacy_'); + addTearDown(() async { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + + final legacyFile = File('${tempDir.path}/legacy_export.json'); + await legacyFile.writeAsString(''' +{ + "exportedAtUtc": "2026-03-20T10:00:00.000Z", + "settings": { + "rentUsd": 1995.0, + "occupied": true + }, + "records": [ + { + "year": 2024, + "month": 12, + "usdAmount": 1995.0, + "sekAmount": 21446.25, + "usdToSekRate": 10.75, + "paidAtUtc": "2024-12-02T12:00:00.000Z", + "onTime": false + } + ] +} +'''); + + final controller = RentController( + storageService: _FakeStorageService(), + exchangeService: _FakeExchangeService(quote: 1), + notificationService: _FakeNotificationService(), + ); + + await controller.importBackupFromFile(legacyFile.path); + + expect(controller.settings.rentUsd, 1995.0); + expect(controller.settings.localFallbackHour, 9); + expect(controller.settings.localQuietHourStart, 23); + + controller.selectYear(2024); + final december = controller.buildRowsForSelectedYear().firstWhere( + (row) => row.month == 12, + ); + + expect(december.status, PaymentStatus.late); + expect(december.usdAmount, 1995.0); + expect(december.sekAmount, 21446.25); + expect(december.effectiveUsdToSekRate, 10.75); + }, + ); + }); +}