import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:pluriwave/servicios/servicio_compras.dart'; /// Port-boundary tests (Design ADR-2 "keeps Strict TDD viable with zero /// plugin channels in unit tests"): [eventoDesdeEstadoCompra] es la parte /// pura, y [ServicioComprasPlayBilling] se ejercita inyectando /// [_InAppPurchaseFalso] — sin ningún platform channel real. /// `EstadoEntitlement` se prueba aparte con un `PuertoCompras` falso /// (`estado_entitlement_test.dart`). /// Fake [InAppPurchase]: deja que cada test empuje lotes por /// [purchaseStream] a mano. `noSuchMethod` cubre el resto de la API del /// plugin, que estos tests no ejercitan. class _InAppPurchaseFalso implements InAppPurchase { final _compras = StreamController>.broadcast(); int restauracionesPedidas = 0; final completadas = []; @override Stream> get purchaseStream => _compras.stream; @override Future restorePurchases({String? applicationUserName}) async { restauracionesPedidas++; } @override Future completePurchase(PurchaseDetails purchase) async { completadas.add(purchase); } void emitir(List compras) => _compras.add(compras); Future dispose() => _compras.close(); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } PurchaseDetails _compraFalsa(PurchaseStatus status) => PurchaseDetails( purchaseID: 'compra-1', productID: ServicioComprasPlayBilling.idProducto, verificationData: PurchaseVerificationData( localVerificationData: 'local', serverVerificationData: 'server', source: 'google_play', ), transactionDate: null, status: status, ); void main() { group('eventoDesdeEstadoCompra', () { test('purchased -> comprada', () { expect( eventoDesdeEstadoCompra(PurchaseStatus.purchased).tipo, TipoEventoCompra.comprada, ); }); test('restored -> restaurada', () { expect( eventoDesdeEstadoCompra(PurchaseStatus.restored).tipo, TipoEventoCompra.restaurada, ); }); test('canceled -> cancelada', () { expect( eventoDesdeEstadoCompra(PurchaseStatus.canceled).tipo, TipoEventoCompra.cancelada, ); }); test('pending -> pendiente', () { expect( eventoDesdeEstadoCompra(PurchaseStatus.pending).tipo, TipoEventoCompra.pendiente, ); }); test('error conserva el mensaje diagnostico', () { final evento = eventoDesdeEstadoCompra( PurchaseStatus.error, mensaje: 'BILLING_UNAVAILABLE', ); expect(evento.tipo, TipoEventoCompra.error); expect(evento.mensaje, 'BILLING_UNAVAILABLE'); }); }); group('ServicioComprasPlayBilling.purchaseStream', () { test('un lote vacio emite noEncontrada (restaurar sin compras)', () async { final iap = _InAppPurchaseFalso(); addTearDown(iap.dispose); final servicio = ServicioComprasPlayBilling(inAppPurchase: iap); addTearDown(servicio.dispose); final tipos = []; final sub = servicio.eventos.listen((e) => tipos.add(e.tipo)); addTearDown(sub.cancel); await servicio.restaurar(); iap.emitir(const []); await Future.delayed(Duration.zero); // Sin este evento `EstadoEntitlement._compraEnCurso` se queda en `true` // para siempre y `hoja_premium.dart` deshabilita AMBOS botones // (comprar y restaurar): el usuario no puede pagar. expect(tipos, [TipoEventoCompra.noEncontrada]); expect(iap.restauracionesPedidas, 1); }); test('un lote con compras NO emite noEncontrada', () async { final iap = _InAppPurchaseFalso(); addTearDown(iap.dispose); final servicio = ServicioComprasPlayBilling(inAppPurchase: iap); addTearDown(servicio.dispose); final tipos = []; final sub = servicio.eventos.listen((e) => tipos.add(e.tipo)); addTearDown(sub.cancel); iap.emitir([_compraFalsa(PurchaseStatus.restored)]); await Future.delayed(Duration.zero); expect(tipos, [TipoEventoCompra.restaurada]); }); test('completa las compras pendientes de confirmar', () async { final iap = _InAppPurchaseFalso(); addTearDown(iap.dispose); final servicio = ServicioComprasPlayBilling(inAppPurchase: iap); addTearDown(servicio.dispose); final compra = _compraFalsa(PurchaseStatus.purchased)..pendingCompletePurchase = true; iap.emitir([compra]); await Future.delayed(Duration.zero); expect(iap.completadas, [compra]); }); }); test('idProducto es el identificador unico no-consumible', () { expect(ServicioComprasPlayBilling.idProducto, 'pluriwave_premium'); }); }