204 lines
7.1 KiB
Dart
204 lines
7.1 KiB
Dart
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<String> moveFolder({required String folderId, required String newParentId}) async =>
|
|
folderId;
|
|
@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;
|
|
}
|