MO-Fuel-Tax-Back/test/fuel_report_images_test.dart

201 lines
7 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<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;
}