import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/proof_store.dart'; import 'package:miler/data/service_profile.dart'; /// ───────────────────────────────────────────────────────────────────────── /// PROOF OF DELIVERY /// /// A photo taken at a door is evidence, and evidence that disappears is worse /// than none — it makes the rider believe he has cover he does not have. /// These pin the two properties that make it real: it outlives the OS cache /// `ImagePicker` hands back, and it belongs to the session that took it. /// ───────────────────────────────────────────────────────────────────────── void main() { late Directory docs; setUp(() async { ServiceProfile.setActive(ServiceProfile.milkMan); SharedPreferences.setMockInitialValues({'userid': 38}); // path_provider has no host under the test binding. docs = await Directory.systemTemp.createTemp('proof_docs'); TestWidgetsFlutterBinding.ensureInitialized(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( const MethodChannel('plugins.flutter.io/path_provider'), (call) async => docs.path, ); }); tearDown(() async { ServiceProfile.setActive(ServiceProfile.parcel); if (await docs.exists()) await docs.delete(recursive: true); }); /// A real 1×1 PNG. The store tests never decode it, but the widget test /// does: `Image.file` on 64 bytes of nonsense never completes its frame, so /// `pumpAndSettle` waits for a decode that will never land and the test /// hangs for the full ten minutes rather than failing. const List onePixelPng = [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ]; /// Stands in for what the camera hands back: a file in a volatile place. Future cameraShot(String name) async { final cache = await Directory.systemTemp.createTemp('picker_cache'); final f = File('${cache.path}/$name.jpg'); await f.writeAsBytes(onePixelPng); return f; } group('the store', () { test('the photo is copied out of the camera cache', () async { final shot = await cameraShot('a'); final saved = await ProofStore.save('ORDER-1', shot.path); expect(saved, isNotNull); expect( saved, isNot(shot.path), reason: 'ImagePicker returns an OS cache path the system reclaims — ' 'a record pointing at it loses its evidence', ); expect(File(saved!).existsSync(), isTrue); // The original can vanish and the proof survives, which is the point. await shot.parent.delete(recursive: true); expect(File(saved).existsSync(), isTrue); expect(await ProofStore.pathFor('ORDER-1'), saved); }); test('a retake replaces rather than accumulates', () async { final first = await ProofStore.save( 'ORDER-1', (await cameraShot('a')).path, ); final second = await ProofStore.save( 'ORDER-1', (await cameraShot('b')).path, ); expect(second, first, reason: 'one stop, one proof'); expect(await ProofStore.pathFor('ORDER-1'), second); }); test('a proof whose file has gone is not proof', () async { final saved = await ProofStore.save( 'ORDER-1', (await cameraShot('a')).path, ); await File(saved!).delete(); expect( await ProofStore.pathFor('ORDER-1'), isNull, reason: 'returning a dead path would draw a broken image as evidence', ); }); test('nothing is invented from nothing', () async { expect(await ProofStore.save('', 'whatever'), isNull); expect(await ProofStore.save('ORDER-1', ''), isNull); expect(await ProofStore.save('ORDER-1', '/no/such/file.jpg'), isNull); expect(await ProofStore.pathFor('ORDER-NONE'), isNull); }); test('a device path is never offered as a remote url', () async { await ProofStore.save('ORDER-1', (await cameraShot('a')).path); expect( await ProofStore.remoteUrlFor('ORDER-1'), '', reason: 'the Miler API has no upload route; a filesystem path written ' "into the hub's photourl would look like evidence and resolve to " 'nothing', ); }); }); group('ownership', () { test("another rider cannot open this rider's doorstep photos", () async { await ProofStore.save('ORDER-1', (await cameraShot('a')).path); expect(await ProofStore.pathFor('ORDER-1'), isNotNull); SharedPreferences.setMockInitialValues({'userid': 99}); expect( await ProofStore.pathFor('ORDER-1'), isNull, reason: 'proof is scoped like every other record — see WorkScope', ); }); test('the other line cannot either', () async { await ProofStore.save('ORDER-1', (await cameraShot('a')).path); ServiceProfile.setActive(ServiceProfile.parcel); expect(await ProofStore.pathFor('ORDER-1'), isNull); }); test('logout deletes the photos, not just the index', () async { final saved = await ProofStore.save( 'ORDER-1', (await cameraShot('a')).path, ); await ProofStore.clearScope(); expect(await ProofStore.pathFor('ORDER-1'), isNull); expect( File(saved!).existsSync(), isFalse, reason: "a doorstep photo must not outlive the session that took it", ); }); }); }