Remote sync of webdav

This commit is contained in:
Courtney Arnold 2026-08-13 17:05:10 -05:00
parent f01186df79
commit 483fbeafb6
44 changed files with 4842 additions and 975 deletions

View file

@ -0,0 +1,137 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:fuel_tax_tracker/services/database_service.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
/// Confirms the version 3 → 4 migration (VIN becomes editable; a hidden
/// `id` takes over as vehicles' primary key/merge key) preserves existing
/// local data rather than dropping it, unlike the earlier VIN-as-primary-key
/// migration which was a from-scratch rebuild. Builds a real on-disk
/// version-3 database by hand (the old schema, pre-dating this change), then
/// runs [DatabaseService.init] against it and checks what comes out.
void main() {
late Directory tempDir;
setUpAll(() {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
});
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('vehicle_id_migration_test_');
PathProviderPlatform.instance = _FakePathProviderPlatform(tempDir.path);
});
tearDown(() async {
await tempDir.delete(recursive: true);
});
test('migrates a version-3 database, preserving vehicles and fuel entries', () async {
final dbDir = Directory(p.join(tempDir.path, 'FuelTaxTracker'));
await dbDir.create(recursive: true);
final dbPath = p.join(dbDir.path, dbFileName);
final oldDb = await databaseFactory.openDatabase(
dbPath,
options: OpenDatabaseOptions(
version: 3,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE vehicles (
vin TEXT PRIMARY KEY,
nickname TEXT,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
dirty INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE fuel_entries (
id TEXT PRIMARY KEY,
vehicle_vin TEXT NOT NULL,
date INTEGER NOT NULL,
gallons REAL NOT NULL,
price_per_gallon REAL NOT NULL,
total_cost REAL NOT NULL,
receipt_image_path TEXT,
receipt_drive_file_id TEXT,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
dirty INTEGER NOT NULL DEFAULT 1
)
''');
},
),
);
await oldDb.insert('vehicles', {
'vin': 'VIN1',
'nickname': "Mom's Car",
'updated_at': 1000,
'deleted_at': null,
'dirty': 0,
});
await oldDb.insert('vehicles', {
'vin': 'VIN2',
'nickname': null,
'updated_at': 1500,
'deleted_at': 2000, // a soft-deleted vehicle, should still migrate
'dirty': 0,
});
await oldDb.insert('fuel_entries', {
'id': 'f1',
'vehicle_vin': 'VIN1',
'date': 1200,
'gallons': 12.5,
'price_per_gallon': 3.2,
'total_cost': 40.0,
'receipt_image_path': '/local/receipt.jpg',
'receipt_drive_file_id': null,
'updated_at': 1200,
'deleted_at': null,
'dirty': 0,
});
await oldDb.close();
final databaseService = DatabaseService();
await databaseService.init();
addTearDown(() => databaseService.rawDb.close());
final vehicles = await databaseService.getVehicles();
expect(vehicles, hasLength(1), reason: 'the soft-deleted vehicle should not show as active');
expect(vehicles.first.vin, 'VIN1');
expect(vehicles.first.nickname, "Mom's Car");
expect(vehicles.first.id, isNotEmpty);
final allVehicleRows = await databaseService.rawDb.query('vehicles');
expect(allVehicleRows, hasLength(2), reason: 'the soft-deleted vehicle should still exist');
final ids = {for (final row in allVehicleRows) row['vin']: row['id'] as String};
expect(ids['VIN1'], isNotEmpty);
expect(ids['VIN2'], isNotEmpty);
expect(ids['VIN1'], isNot(ids['VIN2']), reason: 'each vehicle gets its own generated id');
for (final row in allVehicleRows) {
expect(row['dirty'], 1, reason: 'migrated rows must be re-pushed under the new schema');
}
final fuelEntries = await databaseService.getFuelEntries();
expect(fuelEntries, hasLength(1));
expect(fuelEntries.first.vehicleId, ids['VIN1'],
reason: "the fuel entry's foreign key should now point at VIN1's new id");
expect(fuelEntries.first.gallons, 12.5);
expect(fuelEntries.first.receiptImagePath, '/local/receipt.jpg');
});
}
class _FakePathProviderPlatform extends PathProviderPlatform with MockPlatformInterfaceMixin {
final String tempPath;
_FakePathProviderPlatform(this.tempPath);
@override
Future<String?> getApplicationDocumentsPath() async => tempPath;
@override
Future<String?> getTemporaryPath() async => tempPath;
}