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

@ -6,7 +6,7 @@ import 'package:path/path.dart' as p;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
/// Exercises the exact ATTACH + INSERT OR REPLACE merge SQL the app runs
/// (see [db_schema.dart] / DriveSyncService._pullAndMerge), against real
/// (see [db_schema.dart] / CloudSyncService._pullAndMerge), against real
/// temporary SQLite files via sqflite_common_ffi — the pure-Dart backend
/// that lets sqflite run on the Dart VM for tests, without a real device.
void main() {
@ -43,18 +43,19 @@ void main() {
group('vehicle merge', () {
test('pulls in a vehicle that only exists on remote', () async {
await remote.insert('vehicles', _vehicleRow(id: 'v1', updatedAt: 1000));
await remote.insert('vehicles', _vehicleRow(id: 'v1', vin: 'VIN1', updatedAt: 1000));
await merge();
final rows = await local.query('vehicles');
expect(rows, hasLength(1));
expect(rows.first['id'], 'v1');
expect(rows.first['vin'], 'VIN1');
expect(rows.first['dirty'], 0);
});
test('leaves a local-only dirty vehicle untouched', () async {
await local.insert('vehicles', _vehicleRow(id: 'v1', updatedAt: 1000, dirty: 1));
await local.insert('vehicles', _vehicleRow(id: 'v1', vin: 'VIN1', updatedAt: 1000, dirty: 1));
await merge();
@ -64,30 +65,60 @@ void main() {
});
test('remote wins when strictly newer than local', () async {
await local.insert(
'vehicles', _vehicleRow(id: 'v1', make: 'OldMake', updatedAt: 1000, dirty: 1));
await remote.insert('vehicles', _vehicleRow(id: 'v1', make: 'NewMake', updatedAt: 2000));
await local.insert('vehicles',
_vehicleRow(id: 'v1', vin: 'VIN1', nickname: 'OldNickname', updatedAt: 1000, dirty: 1));
await remote.insert('vehicles',
_vehicleRow(id: 'v1', vin: 'VIN1', nickname: 'NewNickname', updatedAt: 2000));
await merge();
final rows = await local.query('vehicles');
expect(rows, hasLength(1));
expect(rows.first['make'], 'NewMake');
expect(rows.first['nickname'], 'NewNickname');
expect(rows.first['dirty'], 0);
});
test('local wins on a tie or when strictly newer', () async {
await local.insert(
'vehicles', _vehicleRow(id: 'v1', make: 'MineNewer', updatedAt: 2000, dirty: 1));
await remote.insert('vehicles', _vehicleRow(id: 'v1', make: 'TheirsOlder', updatedAt: 1000));
await local.insert('vehicles',
_vehicleRow(id: 'v1', vin: 'VIN1', nickname: 'MineNewer', updatedAt: 2000, dirty: 1));
await remote.insert('vehicles',
_vehicleRow(id: 'v1', vin: 'VIN1', nickname: 'TheirsOlder', updatedAt: 1000));
await merge();
final rows = await local.query('vehicles');
expect(rows, hasLength(1));
expect(rows.first['make'], 'MineNewer');
expect(rows.first['nickname'], 'MineNewer');
expect(rows.first['dirty'], 1, reason: 'still pending push since local was not overwritten');
});
test('a null nickname merges in fine (nickname is optional)', () async {
await remote.insert(
'vehicles', _vehicleRow(id: 'v1', vin: 'VIN1', nickname: null, updatedAt: 1000));
await merge();
final rows = await local.query('vehicles');
expect(rows, hasLength(1));
expect(rows.first['nickname'], isNull);
});
test('the same vehicle edited on two devices merges by id, not vin', () async {
// VIN is user-editable, so it can't be the merge key — this is the
// scenario that motivated switching the merge key to a hidden id:
// the local device renamed the VIN (a legitimate edit) while the
// remote copy still has the old VIN and an older updated_at.
await local.insert('vehicles',
_vehicleRow(id: 'v1', vin: 'VIN1-CORRECTED', nickname: 'Mine', updatedAt: 2000, dirty: 1));
await remote.insert(
'vehicles', _vehicleRow(id: 'v1', vin: 'VIN1-TYPO', nickname: 'Mine', updatedAt: 1000));
await merge();
final rows = await local.query('vehicles');
expect(rows, hasLength(1));
expect(rows.first['vin'], 'VIN1-CORRECTED', reason: 'local was newer, so its VIN edit wins');
});
});
group('fuel entry merge', () {
@ -134,17 +165,16 @@ Future<Database> _openFreshDb(String path) async {
Map<String, Object?> _vehicleRow({
required String id,
String make = 'Ford',
required String vin,
String? nickname = 'Test Vehicle',
int updatedAt = 0,
int? deletedAt,
int dirty = 0,
}) =>
{
'id': id,
'make': make,
'model': 'F-150',
'color': 'Red',
'license_plate': 'ABC123',
'vin': vin,
'nickname': nickname,
'updated_at': updatedAt,
'deleted_at': deletedAt,
'dirty': dirty,