137 lines
4.8 KiB
Dart
137 lines
4.8 KiB
Dart
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;
|
|
}
|