import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/consignment_state.dart'; import 'package:miler/data/miler_api.dart'; import 'package:miler/data/mock_backend.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/pickups/pickups.dart'; /// ───────────────────────────────────────────────────────────────────────── /// TWO BACKENDS, ONE BUILD /// /// `Collected_By_Miler` and `start-delivery` ship behind a server-side flag /// that is **off in production today**, so both lifecycles are live at once: /// /// old `pickup-complete` releases hyperlocal work itself. The consignment /// is already `Out_for_Delivery` when it reaches the Deliveries tab and /// there is nothing for **Start delivery** to call. /// new the pivot stops at `Collected_By_Miler` and the press is what puts /// the load on the road. /// /// The app must be right in both **without mirroring the flag**, because a /// mirrored flag is a second source of truth that can be wrong — and would be /// wrong for every rider for however long it took to ship an app release after /// the server toggled. So the behaviour is inferred from what the data says: /// `consignmentstatus` on the row, then the consignment itself, then the /// `can_start_delivery` / `can_deliver` permissions. /// /// These pin both modes, and pin that the old one costs no request. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); late List sent; http.Response jsonRes(Object body, [int code = 200]) => http.Response( json.encode(body), code, headers: const {'content-type': 'application/json'}, ); void stubRoutes(Map routes) { sent = []; MilerApi.client = MockClient((req) async { sent.add(req); for (final e in routes.entries) { if (req.url.path.endsWith(e.key)) return e.value; } return jsonRes({'success': true}); }); } setUp(() async { MockBackend.enabled = false; SharedPreferences.setMockInitialValues({ 'userid': 38, 'authtoken': 'test-token', }); await ApiConfig.setToken('test-token'); ServiceProfile.setActive(ServiceProfile.milkMan); stubRoutes(const {}); }); tearDown(() { MilerApi.client = http.Client(); MockBackend.enabled = kMockBackend; ServiceProfile.setActive(ServiceProfile.parcel); }); Map stop({String? consignmentStatus}) => { 'orderid': 'ORDER-1', 'pickupid': 75, 'consignmentid': 46, if (consignmentStatus != null) 'consignmentstatus': consignmentStatus, 'orderstatus': 'picked', }; group('old backend — the pivot already released the load', () { test('the press costs no request at all', () async { stubRoutes(const {}); final released = await releaseForDelivery( stop(consignmentStatus: 'Out_for_Delivery'), ); expect(released, isTrue); expect( sent, isEmpty, reason: 'a start-delivery the server can only refuse is not a harmless ' 'no-op — it is a 4xx per rider press in the backend\'s log', ); }); test('a stop already delivered elsewhere is not re-released', () async { stubRoutes(const {}); expect( await releaseForDelivery(stop(consignmentStatus: 'Delivered')), isTrue, ); expect(sent, isEmpty); }); }); group('new backend — the rider makes the release', () { test('a collected consignment is started on the server', () async { stubRoutes({ '/start-delivery': jsonRes({ 'success': true, 'data': {'status': 'Out_for_Delivery'}, }), }); expect( await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')), isTrue, ); expect(sent.single.method, 'POST'); expect( sent.single.url.path.endsWith('/miler/consignments/46/start-delivery'), isTrue, ); expect(sent.single.headers['Idempotency-Key'], isNotNull); }); test('a refusal is answered by the consignment, not assumed', () async { // `INVALID_STATE` means "not Collected_By_Miler" — which covers both a // real refusal and a consignment another device already released. stubRoutes({ '/start-delivery': jsonRes({ 'success': false, 'code': 'INVALID_STATE', 'message': 'consignment is not collected', }, 400), '/miler/consignments/46': jsonRes({ 'success': true, 'data': {'status': 'Out_for_Delivery'}, }), }); expect( await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')), isTrue, reason: 'already on the road is not a reason to block the rider', ); }); test('a genuine refusal does not fake a release', () async { stubRoutes({ '/start-delivery': jsonRes({ 'success': false, 'code': 'INVALID_STATE', 'message': 'consignment is at the hub', }, 400), '/miler/consignments/46': jsonRes({ 'success': true, 'data': {'status': 'Inwarded_at_Hub'}, }), }); expect( await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')), isFalse, reason: 'showing ACTIVE for a consignment the hub still holds is the ' 'fiction that made deliver fail at the door with nothing on ' 'screen to explain it', ); }); }); group('a row that says nothing falls back to asking', () { test('the consignment is read when the row carries no status', () async { stubRoutes({ '/miler/consignments/46': jsonRes({ 'success': true, 'data': {'status': 'Out_for_Delivery'}, }), }); expect(await releaseForDelivery(stop()), isTrue); expect(sent.single.method, 'GET'); expect(sent.single.url.path.endsWith('/miler/consignments/46'), isTrue); }); test('the permission flags answer when the word does not', () { // A status this build has never heard of, plus the server's own verdict // on what may be done with it. expect( ConsignmentGate.stateFromDetailForTest({ 'status': 'Handed_To_Locker_Bank', 'can_deliver': true, }), ConsignmentState.outForDelivery, ); expect( ConsignmentGate.stateFromDetailForTest({ 'status': 'Awaiting_Rider_Departure', 'can_start_delivery': true, }), ConsignmentState.collectedByMiler, ); }); test('no consignment id is not a release', () async { expect(await releaseForDelivery({'orderid': 'ORDER-9'}), isFalse); }); }); group('a skipped stop and the server agree about whether it is open', () { test('a consignment the hub still calls out-for-delivery stays open', () { expect(ConsignmentState.outForDelivery.isOpenAfterSkip, isTrue); expect(ConsignmentState.collectedByMiler.isOpenAfterSkip, isTrue); }); test('a consignment the hub closed is closed', () { expect(ConsignmentState.delivered.isOpenAfterSkip, isFalse); expect(ConsignmentState.rtoInitiated.isOpenAfterSkip, isFalse); expect(ConsignmentState.returnedToSender.isOpenAfterSkip, isFalse); expect(ConsignmentState.cancelled.isOpenAfterSkip, isFalse); }); test('a read that failed is not permission to close a stop', () { expect( ConsignmentState.unknown.isOpenAfterSkip, isTrue, reason: 'a terminal local record over a consignment the hub still ' 'calls open is two systems disagreeing about whose problem a ' 'parcel is, with only the rider\'s screen saying "nobody\'s"', ); }); }); }