import 'dart:async'; import 'package:commons_core/commons_core.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/db/enums.dart'; import 'package:tane/services/offer_mapper.dart'; import 'package:tane/state/offers_cubit.dart'; /// In-memory [OfferTransport] for tests: addressable by id, discover matches by /// geohash prefix. No relay, no network. class FakeOfferTransport implements OfferTransport { final List _offers = []; @override Future publish(Offer offer) async { _offers.removeWhere((o) => o.id == offer.id); _offers.add(offer); return PublishResult(accepted: true, transportRef: offer.id); } @override Stream discover(DiscoveryQuery query) { final controller = StreamController(); for (final o in _offers) { if (o.approxGeohash.startsWith(query.geohashPrefix)) controller.add(o); } return controller.stream; // left open (live), like a real subscription } @override Future retract(String offerId) async => _offers.removeWhere((o) => o.id == offerId); @override Future close() async {} } void main() { group('OfferMapper', () { test('maps local sharing intent to the network offer type', () { expect(OfferMapper.offerTypeFor(OfferStatus.shared), OfferType.gift); expect(OfferMapper.offerTypeFor(OfferStatus.exchange), OfferType.exchange); expect(OfferMapper.offerTypeFor(OfferStatus.sell), OfferType.sale); }); test('never publishes a private lot', () { expect(() => OfferMapper.offerTypeFor(OfferStatus.private), throwsArgumentError); }); test('builds an offer, keeping price only for a sale', () { final gift = OfferMapper.fromSharedLot( lotId: 'lot-1', authorPubkeyHex: 'ab' * 32, summary: 'Tomate rosa', sharing: OfferStatus.shared, areaGeohash: 'sp3e9', priceAmount: 5, // should be dropped for a gift ); expect(gift.type, OfferType.gift); expect(gift.id, 'lot-1'); expect(gift.priceAmount, isNull); final sale = OfferMapper.fromSharedLot( lotId: 'lot-2', authorPubkeyHex: 'ab' * 32, summary: 'Judía', sharing: OfferStatus.sell, areaGeohash: 'sp3e9', priceAmount: 3, priceCurrency: 'G1', ); expect(sale.type, OfferType.sale); expect(sale.priceAmount, 3); expect(sale.priceCurrency, 'G1'); }); }); group('OffersCubit', () { test('discovers published offers by geohash prefix', () async { final transport = FakeOfferTransport(); await transport.publish(OfferMapper.fromSharedLot( lotId: 'lot-1', authorPubkeyHex: 'ab' * 32, summary: 'Tomate rosa de Barbastro', sharing: OfferStatus.shared, areaGeohash: 'sp3e9', )); final cubit = OffersCubit(transport); await cubit.discover('sp3'); await pumpEventQueue(); expect(cubit.state.hasSearched, isTrue); expect(cubit.state.searching, isFalse); expect(cubit.state.offers, hasLength(1)); expect(cubit.state.offers.single.summary, 'Tomate rosa de Barbastro'); await cubit.close(); }); test('a different area finds nothing', () async { final transport = FakeOfferTransport(); await transport.publish(OfferMapper.fromSharedLot( lotId: 'lot-1', authorPubkeyHex: 'ab' * 32, summary: 'Pimiento', sharing: OfferStatus.shared, areaGeohash: 'sp3e9', )); final cubit = OffersCubit(transport); await cubit.discover('u09'); await pumpEventQueue(); expect(cubit.state.offers, isEmpty); await cubit.close(); }); test('publish reports the transport verdict', () async { final cubit = OffersCubit(FakeOfferTransport()); final result = await cubit.publish(OfferMapper.fromSharedLot( lotId: 'lot-9', authorPubkeyHex: 'ab' * 32, summary: 'Calabaza', sharing: OfferStatus.exchange, areaGeohash: 'sp3e9', exchangeTerms: 'a cambio de legumbre', )); expect(result.accepted, isTrue); expect(cubit.state.publishing, isFalse); await cubit.close(); }); test('offline (no transport): degrades gracefully, never throws', () async { final cubit = OffersCubit(null); expect(cubit.isOnline, isFalse); await cubit.discover('sp3'); expect(cubit.state.error, 'offline'); final result = await cubit.publish(OfferMapper.fromSharedLot( lotId: 'x', authorPubkeyHex: 'ab' * 32, summary: 'x', sharing: OfferStatus.shared, areaGeohash: 'sp3e9', )); expect(result.accepted, isFalse); await cubit.close(); }); }); }