A whole bunch of changes
This commit is contained in:
parent
901ec678a3
commit
956231048e
122 changed files with 5442 additions and 914 deletions
201
test/fuel_report_images_test.dart
Normal file
201
test/fuel_report_images_test.dart
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
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/cloud/cloud_storage_provider.dart';
|
||||
import 'package:fuel_tax_tracker/services/fuel_report.dart';
|
||||
import 'package:fuel_tax_tracker/services/fuel_report_images.dart';
|
||||
|
||||
void main() {
|
||||
final vehicle = Vehicle(
|
||||
id: 'v1',
|
||||
vin: '1FMPU18L1TLB51349',
|
||||
updatedAt: DateTime.utc(2026, 1, 1),
|
||||
);
|
||||
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('fuel_report_images_test_');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await tempDir.delete(recursive: true);
|
||||
});
|
||||
|
||||
FuelReport reportWith(List<FuelEntry> entries) => FuelReport(
|
||||
startDate: DateTime(2026, 1, 1),
|
||||
endDate: DateTime(2026, 12, 31),
|
||||
rows: [
|
||||
VehicleReportRow(
|
||||
vehicle: vehicle,
|
||||
entries: entries,
|
||||
totalGallons: entries.fold(0.0, (sum, e) => sum + e.gallons),
|
||||
totalCost: entries.fold(0.0, (sum, e) => sum + e.totalCost),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
FuelEntry entry({
|
||||
required String id,
|
||||
String? receiptImagePath,
|
||||
String? receiptDriveFileId,
|
||||
}) =>
|
||||
FuelEntry(
|
||||
id: id,
|
||||
vehicleId: 'v1',
|
||||
date: DateTime(2026, 3, 1),
|
||||
gallons: 10,
|
||||
pricePerGallon: 3.5,
|
||||
totalCost: 35,
|
||||
updatedAt: DateTime.utc(2026, 3, 1),
|
||||
receiptImagePath: receiptImagePath,
|
||||
receiptDriveFileId: receiptDriveFileId,
|
||||
);
|
||||
|
||||
test('reads bytes from a local file when one exists', () async {
|
||||
final file = File('${tempDir.path}/a.jpg')..writeAsBytesSync([1, 2, 3]);
|
||||
final report = reportWith([entry(id: 'e1', receiptImagePath: file.path)]);
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: null);
|
||||
|
||||
expect(result.bytesByEntryId['e1'], [1, 2, 3]);
|
||||
expect(result.failedEntries, isEmpty);
|
||||
});
|
||||
|
||||
test('falls back to downloading via the cloud provider when there is no local file', () async {
|
||||
final report = reportWith([entry(id: 'e1', receiptDriveFileId: 'remote-1')]);
|
||||
final provider = _FakeProvider(_FakeSession(bytesByFileId: {'remote-1': [4, 5, 6]}));
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: provider);
|
||||
|
||||
expect(result.bytesByEntryId['e1'], [4, 5, 6]);
|
||||
expect(result.failedEntries, isEmpty);
|
||||
});
|
||||
|
||||
test('reports (rather than silently drops) an entry with neither a local file nor a cloud reference',
|
||||
() async {
|
||||
final report = reportWith([entry(id: 'e1')]);
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: null);
|
||||
|
||||
expect(result.bytesByEntryId.containsKey('e1'), isFalse);
|
||||
expect(result.failedEntries.map((e) => e.id), ['e1']);
|
||||
});
|
||||
|
||||
test('reports (rather than throws) when the download fails', () async {
|
||||
final report = reportWith([entry(id: 'e1', receiptDriveFileId: 'missing')]);
|
||||
final provider = _FakeProvider(_FakeSession(bytesByFileId: {}));
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: provider);
|
||||
|
||||
expect(result.bytesByEntryId.containsKey('e1'), isFalse);
|
||||
expect(result.failedEntries.map((e) => e.id), ['e1']);
|
||||
});
|
||||
|
||||
test('reports a cloud-only entry when there is no signed-in provider', () async {
|
||||
final report = reportWith([entry(id: 'e1', receiptDriveFileId: 'remote-1')]);
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: null);
|
||||
|
||||
expect(result.bytesByEntryId.containsKey('e1'), isFalse);
|
||||
expect(result.failedEntries.map((e) => e.id), ['e1']);
|
||||
});
|
||||
|
||||
test('a receipt uploaded via a different provider than the one currently connected fails cleanly',
|
||||
() async {
|
||||
// Mirrors the real scenario this was built for: a WebDAV-style path
|
||||
// stored as receiptDriveFileId, but the currently active provider is
|
||||
// something else entirely (here, the fake) — the ID is meaningless to
|
||||
// it and the download call throws, same as any other failed download.
|
||||
final report = reportWith([
|
||||
entry(
|
||||
id: 'e1',
|
||||
receiptDriveFileId:
|
||||
'/remote.php/dav/files/user/Show Me The Fuel Refund/receipts/VIN/e1.jpg',
|
||||
),
|
||||
]);
|
||||
final provider = _FakeProvider(_FakeSession(bytesByFileId: {}));
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: provider);
|
||||
|
||||
expect(result.bytesByEntryId.containsKey('e1'), isFalse);
|
||||
expect(result.failedEntries.map((e) => e.id), ['e1']);
|
||||
});
|
||||
|
||||
test('mixed success and failure across multiple entries', () async {
|
||||
final file = File('${tempDir.path}/a.jpg')..writeAsBytesSync([9]);
|
||||
final report = reportWith([
|
||||
entry(id: 'local', receiptImagePath: file.path),
|
||||
entry(id: 'cloud-ok', receiptDriveFileId: 'remote-1'),
|
||||
entry(id: 'cloud-missing', receiptDriveFileId: 'nope'),
|
||||
]);
|
||||
final provider = _FakeProvider(_FakeSession(bytesByFileId: {'remote-1': [1]}));
|
||||
|
||||
final result = await loadReceiptImageBytes(report: report, activeProvider: provider);
|
||||
|
||||
expect(result.bytesByEntryId.keys, {'local', 'cloud-ok'});
|
||||
expect(result.failedEntries.map((e) => e.id), ['cloud-missing']);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeSession implements CloudStorageSession {
|
||||
final Map<String, List<int>> bytesByFileId;
|
||||
_FakeSession({required this.bytesByFileId});
|
||||
|
||||
@override
|
||||
Future<List<int>> downloadFileBytes(String fileId) async {
|
||||
final bytes = bytesByFileId[fileId];
|
||||
if (bytes == null) throw StateError('not found: $fileId');
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get supportsSharedWithMe => false;
|
||||
@override
|
||||
Future<List<CloudFolder>> listFolders({String? parentId, bool sharedWithMe = false}) async => [];
|
||||
@override
|
||||
Future<String> findOrCreateFolder({required String parentId, required String name}) async => '';
|
||||
@override
|
||||
Future<CloudFileInfo?> findFile({required String folderId, required String name}) async => null;
|
||||
@override
|
||||
Future<CloudFileInfo> uploadFile({
|
||||
required String folderId,
|
||||
required String name,
|
||||
String? existingFileId,
|
||||
required File localFile,
|
||||
required String contentType,
|
||||
}) async =>
|
||||
CloudFileInfo(id: 'x', versionTag: null);
|
||||
@override
|
||||
Future<void> deleteFile(String fileId) async {}
|
||||
@override
|
||||
Future<String> createLockFile({required String folderId, required String name}) async => 'lock';
|
||||
@override
|
||||
Future<List<CloudLockFile>> listLockFiles(String folderId) async => [];
|
||||
@override
|
||||
void close() {}
|
||||
}
|
||||
|
||||
class _FakeProvider implements CloudStorageProvider {
|
||||
final _FakeSession session;
|
||||
_FakeProvider(this.session);
|
||||
|
||||
@override
|
||||
CloudProviderId get id => CloudProviderId.googleDrive;
|
||||
@override
|
||||
String get displayName => 'Fake Provider';
|
||||
@override
|
||||
bool get isSignedIn => true;
|
||||
@override
|
||||
String? get accountLabel => 'tester@example.com';
|
||||
@override
|
||||
Future<bool> attemptSilentSignIn() async => true;
|
||||
@override
|
||||
Future<String> signIn() async => accountLabel!;
|
||||
@override
|
||||
Future<void> signOut() async {}
|
||||
@override
|
||||
CloudStorageSession beginSession() => session;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue