A whole bunch of changes
This commit is contained in:
parent
901ec678a3
commit
956231048e
122 changed files with 5442 additions and 914 deletions
174
test/purge_data_test.dart
Normal file
174
test/purge_data_test.dart
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fuel_tax_tracker/models/fuel_entry.dart';
|
||||
import 'package:fuel_tax_tracker/models/vehicle.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';
|
||||
|
||||
/// Both purge paths reuse the same soft-delete tombstones as a normal
|
||||
/// per-row delete (see [DatabaseService.softDeleteVehicle]) rather than a
|
||||
/// real SQL `DELETE`, specifically so the deletion is something a cloud
|
||||
/// sync can push to other devices instead of the rows just getting
|
||||
/// silently re-imported from the remote copy on the next sync.
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
late DatabaseService database;
|
||||
|
||||
setUpAll(() {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('purge_data_test_');
|
||||
PathProviderPlatform.instance = _FakePathProviderPlatform(tempDir.path);
|
||||
database = DatabaseService();
|
||||
await database.init();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await database.rawDb.close();
|
||||
await tempDir.delete(recursive: true);
|
||||
});
|
||||
|
||||
Vehicle vehicle({required String id, required String vin}) => Vehicle(
|
||||
id: id,
|
||||
vin: vin,
|
||||
updatedAt: DateTime.utc(2026, 1, 1),
|
||||
);
|
||||
|
||||
FuelEntry entry({
|
||||
required String id,
|
||||
required String vehicleId,
|
||||
required DateTime date,
|
||||
String? receiptImagePath,
|
||||
}) =>
|
||||
FuelEntry(
|
||||
id: id,
|
||||
vehicleId: vehicleId,
|
||||
date: date,
|
||||
gallons: 10,
|
||||
pricePerGallon: 3.5,
|
||||
totalCost: 35,
|
||||
updatedAt: DateTime.utc(2026, 1, 1),
|
||||
receiptImagePath: receiptImagePath,
|
||||
);
|
||||
|
||||
group('purgeAllData', () {
|
||||
test('soft-deletes every active vehicle and fuel entry, marking them dirty', () async {
|
||||
await database.saveVehicle(vehicle(id: 'v1', vin: 'VIN1'));
|
||||
await database.saveVehicle(vehicle(id: 'v2', vin: 'VIN2'));
|
||||
await database.saveFuelEntry(entry(id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 1)));
|
||||
await database.saveFuelEntry(entry(id: 'e2', vehicleId: 'v2', date: DateTime(2026, 4, 1)));
|
||||
|
||||
await database.purgeAllData(DateTime.utc(2026, 8, 13));
|
||||
|
||||
expect(await database.getVehicles(), isEmpty);
|
||||
expect(await database.getFuelEntries(), isEmpty);
|
||||
|
||||
final vehicleRows = await database.rawDb.query('vehicles');
|
||||
final entryRows = await database.rawDb.query('fuel_entries');
|
||||
expect(vehicleRows, hasLength(2), reason: 'tombstoned, not hard-deleted');
|
||||
expect(entryRows, hasLength(2));
|
||||
for (final row in [...vehicleRows, ...entryRows]) {
|
||||
expect(row['deleted_at'], isNotNull);
|
||||
expect(row['dirty'], 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('empties the local receipts folder, including untracked files', () async {
|
||||
final source = File(p.join(tempDir.path, 'source.jpg'))..writeAsBytesSync([1, 2, 3]);
|
||||
final storedPath =
|
||||
await database.storeReceiptImage(source, 'e1', 'VIN1', DateTime(2026, 3, 1));
|
||||
// Simulate an orphaned file the DB never referenced.
|
||||
final strayFile = File(p.join(database.receiptsDirectory.path, 'stray.jpg'))
|
||||
..writeAsBytesSync([9]);
|
||||
|
||||
await database.purgeAllData(DateTime.utc(2026, 8, 13));
|
||||
|
||||
expect(await File(storedPath).exists(), isFalse);
|
||||
expect(await strayFile.exists(), isFalse);
|
||||
expect(await database.receiptsDirectory.exists(), isTrue,
|
||||
reason: 'recreated empty, ready for future use');
|
||||
});
|
||||
|
||||
test('leaves already-deleted rows alone', () async {
|
||||
await database.saveVehicle(vehicle(id: 'v1', vin: 'VIN1'));
|
||||
await database.softDeleteVehicle('v1', DateTime.utc(2026, 1, 2));
|
||||
|
||||
await database.purgeAllData(DateTime.utc(2026, 8, 13));
|
||||
|
||||
final rows = await database.rawDb.query('vehicles', where: 'id = ?', whereArgs: ['v1']);
|
||||
expect(rows.single['deleted_at'], DateTime.utc(2026, 1, 2).millisecondsSinceEpoch);
|
||||
});
|
||||
});
|
||||
|
||||
group('purgeFuelEntriesInRange', () {
|
||||
test('soft-deletes only entries dated within the inclusive range', () async {
|
||||
await database.saveVehicle(vehicle(id: 'v1', vin: 'VIN1'));
|
||||
await database.saveFuelEntry(
|
||||
entry(id: 'before', vehicleId: 'v1', date: DateTime(2026, 2, 28)),
|
||||
);
|
||||
await database.saveFuelEntry(
|
||||
entry(id: 'start-boundary', vehicleId: 'v1', date: DateTime(2026, 3, 1)),
|
||||
);
|
||||
await database.saveFuelEntry(
|
||||
entry(id: 'inside', vehicleId: 'v1', date: DateTime(2026, 3, 15)),
|
||||
);
|
||||
await database.saveFuelEntry(
|
||||
entry(id: 'end-boundary', vehicleId: 'v1', date: DateTime(2026, 3, 31)),
|
||||
);
|
||||
await database.saveFuelEntry(
|
||||
entry(id: 'after', vehicleId: 'v1', date: DateTime(2026, 4, 1)),
|
||||
);
|
||||
|
||||
await database.purgeFuelEntriesInRange(
|
||||
DateTime(2026, 3, 1),
|
||||
DateTime(2026, 3, 31),
|
||||
DateTime.utc(2026, 8, 13),
|
||||
);
|
||||
|
||||
final remaining = await database.getFuelEntries();
|
||||
expect(remaining.map((e) => e.id).toSet(), {'before', 'after'});
|
||||
expect(await database.getVehicles(), hasLength(1), reason: 'vehicles are untouched');
|
||||
});
|
||||
|
||||
test('returns local receipt paths for the caller to delete, without touching untracked files',
|
||||
() async {
|
||||
await database.saveVehicle(vehicle(id: 'v1', vin: 'VIN1'));
|
||||
final source = File(p.join(tempDir.path, 'source.jpg'))..writeAsBytesSync([1, 2, 3]);
|
||||
final storedPath =
|
||||
await database.storeReceiptImage(source, 'e1', 'VIN1', DateTime(2026, 3, 15));
|
||||
await database.saveFuelEntry(
|
||||
entry(id: 'e1', vehicleId: 'v1', date: DateTime(2026, 3, 15), receiptImagePath: storedPath),
|
||||
);
|
||||
|
||||
final paths = await database.purgeFuelEntriesInRange(
|
||||
DateTime(2026, 3, 1),
|
||||
DateTime(2026, 3, 31),
|
||||
DateTime.utc(2026, 8, 13),
|
||||
);
|
||||
|
||||
expect(paths, [storedPath]);
|
||||
// purgeFuelEntriesInRange only reports paths — deleting the files
|
||||
// themselves is the caller's job (AppState), mirroring
|
||||
// softDeleteFuelEntriesForVehicle.
|
||||
expect(await File(storedPath).exists(), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakePathProviderPlatform extends PathProviderPlatform with MockPlatformInterfaceMixin {
|
||||
final String tempPath;
|
||||
_FakePathProviderPlatform(this.tempPath);
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationDocumentsPath() async => tempPath;
|
||||
|
||||
@override
|
||||
Future<String?> getTemporaryPath() async => tempPath;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue