Enable core library desugaring and update RentController to accept a notification service; add backup compatibility tests
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -39,6 +40,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class RentController extends ChangeNotifier {
|
|||||||
RentController({
|
RentController({
|
||||||
StorageService? storageService,
|
StorageService? storageService,
|
||||||
ForexRateApiService? exchangeService,
|
ForexRateApiService? exchangeService,
|
||||||
|
NotificationService? notificationService,
|
||||||
}) : _storageService = storageService ?? StorageService(),
|
}) : _storageService = storageService ?? StorageService(),
|
||||||
_exchangeService =
|
_exchangeService =
|
||||||
exchangeService ??
|
exchangeService ??
|
||||||
@@ -48,7 +49,8 @@ class RentController extends ChangeNotifier {
|
|||||||
httpClient: http.Client(),
|
httpClient: http.Client(),
|
||||||
apiKey: _exchangeRateApiKeyFromEnv(),
|
apiKey: _exchangeRateApiKeyFromEnv(),
|
||||||
) {
|
) {
|
||||||
_notificationService = NotificationService(_onNotificationAction);
|
_notificationService =
|
||||||
|
notificationService ?? NotificationService(_onNotificationAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _exchangeRateApiKeyFromEnv() {
|
static String _exchangeRateApiKeyFromEnv() {
|
||||||
|
|||||||
@@ -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<String, MonthlyRentRecord> _records = <String, MonthlyRentRecord>{};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AppSettings> loadSettings() async => _settings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSettings(AppSettings settings) async {
|
||||||
|
_settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, MonthlyRentRecord>> loadRecords() async =>
|
||||||
|
Map<String, MonthlyRentRecord>.from(_records);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveRecords(Map<String, MonthlyRentRecord> records) async {
|
||||||
|
_records = Map<String, MonthlyRentRecord>.from(records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeExchangeService extends ForexRateApiService {
|
||||||
|
_FakeExchangeService({required this.quote})
|
||||||
|
: super(httpClient: http.Client(), apiKey: 'test-key');
|
||||||
|
|
||||||
|
final double quote;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ForexConversionResult> convertUsdToSek({
|
||||||
|
required DateTime estDate,
|
||||||
|
required double amount,
|
||||||
|
}) async {
|
||||||
|
return ForexConversionResult(quote: quote, result: quote * amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeNotificationService extends NotificationService {
|
||||||
|
_FakeNotificationService() : super((_) async {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initialize() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> processPendingActions() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> scheduleForMonth({
|
||||||
|
required int year,
|
||||||
|
required int month,
|
||||||
|
required int quietHourStart,
|
||||||
|
required int fallbackHour,
|
||||||
|
}) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> cancelForMonth(int year, int month) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> 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(<String>['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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user