Added Google Drive storage location
This commit is contained in:
parent
e630f2a3ec
commit
73a7c93e64
22 changed files with 1690 additions and 205 deletions
133
test/lock_coordinator_test.dart
Normal file
133
test/lock_coordinator_test.dart
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fuel_tax_tracker/services/drive_service.dart' show DriveLockFile;
|
||||
import 'package:fuel_tax_tracker/services/lock_coordinator.dart';
|
||||
|
||||
void main() {
|
||||
group('acquireLock', () {
|
||||
test('creates a lock file named {username}-{epochMillis}.lock', () async {
|
||||
final now = DateTime.utc(2024, 1, 1, 12, 0, 0);
|
||||
String? capturedName;
|
||||
|
||||
await acquireLock(
|
||||
username: 'me@example.com',
|
||||
nowUtc: () => now,
|
||||
createLock: (name) async {
|
||||
capturedName = name;
|
||||
return 'id';
|
||||
},
|
||||
listLocks: () async =>
|
||||
[DriveLockFile(id: 'id', username: 'me@example.com', createdAtUtc: now)],
|
||||
deleteLock: (_) async {},
|
||||
delay: (_) async {},
|
||||
);
|
||||
|
||||
expect(capturedName, 'me@example.com-${now.millisecondsSinceEpoch}.lock');
|
||||
});
|
||||
|
||||
test('proceeds immediately when no other lock is older', () async {
|
||||
var listCallCount = 0;
|
||||
final delayCalls = <Duration>[];
|
||||
final now = DateTime.utc(2024, 1, 1, 12, 0, 0);
|
||||
|
||||
final lockId = await acquireLock(
|
||||
username: 'me',
|
||||
nowUtc: () => now,
|
||||
createLock: (_) async => 'my-lock-id',
|
||||
listLocks: () async {
|
||||
listCallCount++;
|
||||
return [DriveLockFile(id: 'my-lock-id', username: 'me', createdAtUtc: now)];
|
||||
},
|
||||
deleteLock: (_) async {},
|
||||
delay: (d) async => delayCalls.add(d),
|
||||
);
|
||||
|
||||
expect(lockId, 'my-lock-id');
|
||||
expect(listCallCount, 1);
|
||||
expect(delayCalls, isEmpty);
|
||||
});
|
||||
|
||||
test('a newer lock does not block acquisition', () async {
|
||||
final now = DateTime.utc(2024, 1, 1, 12, 0, 0);
|
||||
final delayCalls = <Duration>[];
|
||||
|
||||
final lockId = await acquireLock(
|
||||
username: 'me',
|
||||
nowUtc: () => now,
|
||||
createLock: (_) async => 'mine',
|
||||
listLocks: () async => [
|
||||
DriveLockFile(id: 'mine', username: 'me', createdAtUtc: now),
|
||||
DriveLockFile(
|
||||
id: 'newer', username: 'other', createdAtUtc: now.add(const Duration(seconds: 5))),
|
||||
],
|
||||
deleteLock: (_) async {},
|
||||
delay: (d) async => delayCalls.add(d),
|
||||
);
|
||||
|
||||
expect(lockId, 'mine');
|
||||
expect(delayCalls, isEmpty);
|
||||
});
|
||||
|
||||
test('waits for an older, non-stale lock, then proceeds once it is gone', () async {
|
||||
final now = DateTime.utc(2024, 1, 1, 12, 0, 0);
|
||||
var listCallCount = 0;
|
||||
final delayCalls = <Duration>[];
|
||||
final deleteCalls = <String>[];
|
||||
|
||||
final lockId = await acquireLock(
|
||||
username: 'me',
|
||||
nowUtc: () => now,
|
||||
createLock: (_) async => 'mine',
|
||||
listLocks: () async {
|
||||
listCallCount++;
|
||||
if (listCallCount == 1) {
|
||||
return [
|
||||
DriveLockFile(id: 'mine', username: 'me', createdAtUtc: now),
|
||||
DriveLockFile(
|
||||
id: 'other',
|
||||
username: 'other',
|
||||
createdAtUtc: now.subtract(const Duration(seconds: 5))),
|
||||
];
|
||||
}
|
||||
return [DriveLockFile(id: 'mine', username: 'me', createdAtUtc: now)];
|
||||
},
|
||||
deleteLock: (id) async => deleteCalls.add(id),
|
||||
delay: (d) async => delayCalls.add(d),
|
||||
);
|
||||
|
||||
expect(lockId, 'mine');
|
||||
expect(listCallCount, 2);
|
||||
expect(delayCalls.length, 1);
|
||||
expect(deleteCalls, isEmpty, reason: 'a non-stale older lock should not be deleted');
|
||||
});
|
||||
|
||||
test('deletes an older lock once it exceeds the staleness threshold', () async {
|
||||
final now = DateTime.utc(2024, 1, 1, 12, 0, 0);
|
||||
var listCallCount = 0;
|
||||
final deleteCalls = <String>[];
|
||||
|
||||
await acquireLock(
|
||||
username: 'me',
|
||||
nowUtc: () => now,
|
||||
staleAge: const Duration(minutes: 10),
|
||||
createLock: (_) async => 'mine',
|
||||
listLocks: () async {
|
||||
listCallCount++;
|
||||
if (listCallCount == 1) {
|
||||
return [
|
||||
DriveLockFile(id: 'mine', username: 'me', createdAtUtc: now),
|
||||
DriveLockFile(
|
||||
id: 'stale',
|
||||
username: 'ghost',
|
||||
createdAtUtc: now.subtract(const Duration(minutes: 15))),
|
||||
];
|
||||
}
|
||||
return [DriveLockFile(id: 'mine', username: 'me', createdAtUtc: now)];
|
||||
},
|
||||
deleteLock: (id) async => deleteCalls.add(id),
|
||||
delay: (_) async {},
|
||||
);
|
||||
|
||||
expect(deleteCalls, ['stale']);
|
||||
});
|
||||
});
|
||||
}
|
||||
54
test/merge_utils_test.dart
Normal file
54
test/merge_utils_test.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fuel_tax_tracker/services/merge_utils.dart';
|
||||
|
||||
class _Record {
|
||||
final String id;
|
||||
final String value;
|
||||
final DateTime updatedAt;
|
||||
|
||||
_Record(this.id, this.value, this.updatedAt);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('mergeById', () {
|
||||
test('unions records that only exist on one side', () {
|
||||
final remote = [_Record('a', 'remote-a', DateTime(2024, 1, 1))];
|
||||
final local = [_Record('b', 'local-b', DateTime(2024, 1, 1))];
|
||||
|
||||
final result = mergeById(remote, local, (r) => r.id, (r) => r.updatedAt);
|
||||
|
||||
expect(result.map((r) => r.id), containsAll(['a', 'b']));
|
||||
expect(result.length, 2);
|
||||
});
|
||||
|
||||
test('keeps the newer updatedAt when a record exists on both sides', () {
|
||||
final remote = [_Record('a', 'remote-old', DateTime(2024, 1, 1))];
|
||||
final local = [_Record('a', 'local-new', DateTime(2024, 6, 1))];
|
||||
|
||||
final result = mergeById(remote, local, (r) => r.id, (r) => r.updatedAt);
|
||||
|
||||
expect(result.length, 1);
|
||||
expect(result.first.value, 'local-new');
|
||||
});
|
||||
|
||||
test('keeps the remote copy when it is newer than local', () {
|
||||
final remote = [_Record('a', 'remote-new', DateTime(2024, 6, 1))];
|
||||
final local = [_Record('a', 'local-old', DateTime(2024, 1, 1))];
|
||||
|
||||
final result = mergeById(remote, local, (r) => r.id, (r) => r.updatedAt);
|
||||
|
||||
expect(result.length, 1);
|
||||
expect(result.first.value, 'remote-new');
|
||||
});
|
||||
|
||||
test('local wins ties', () {
|
||||
final sameTime = DateTime(2024, 1, 1);
|
||||
final remote = [_Record('a', 'remote', sameTime)];
|
||||
final local = [_Record('a', 'local', sameTime)];
|
||||
|
||||
final result = mergeById(remote, local, (r) => r.id, (r) => r.updatedAt);
|
||||
|
||||
expect(result.first.value, 'local');
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue