import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/verificacion_licencia.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Silent license re-verification policy (refund revocation): pure, /// headless-safe, driven by an injectable clock and a fake ownership query — /// zero plugin channels. /// Scripted ownership query: returns [resultados] in order and counts calls. class _ConsultaFalsa { _ConsultaFalsa(this.resultados); final List resultados; int llamadas = 0; Future call() async { final resultado = resultados[llamadas.clamp(0, resultados.length - 1)]; llamadas++; return resultado; } } /// Mutable clock so each test can move time forward between checks. class _Reloj { _Reloj(this.ahora); DateTime ahora; DateTime call() => ahora; void avanzar(Duration d) => ahora = ahora.add(d); } void main() { late _Reloj reloj; setUp(() { SharedPreferences.setMockInitialValues({}); reloj = _Reloj(DateTime(2026, 9, 18, 10)); }); Future prefsCon({required bool premium}) async { SharedPreferences.setMockInitialValues({claveCompraPremium: premium}); return SharedPreferences.getInstance(); } Future verificar( SharedPreferences prefs, Future Function() consultar, ) => verificarLicencia(consultar: consultar, prefs: prefs, reloj: reloj.call); group('verificarLicencia', () { test('desconocido (offline / sin billing) conserva premium y no toca el ' 'contador de ausencias', () async { final prefs = await prefsCon(premium: true); final consulta = _ConsultaFalsa([ ResultadoVerificacionLicencia.noPoseida, ]); await verificar(prefs, consulta.call); expect(prefs.getInt(claveAusenciasLicencia), 1); reloj.avanzar(const Duration(days: 2)); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.desconocido, ); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isTrue); expect(prefs.getInt(claveAusenciasLicencia), 1); }); test('una excepcion en la consulta no cambia nada (fail-open)', () async { final prefs = await prefsCon(premium: true); final cambio = await verificar( prefs, () async => throw Exception('BillingClient desconectado'), ); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isTrue); expect(prefs.getInt(claveAusenciasLicencia), isNull); }); test('poseida conserva premium y reinicia el contador', () async { final prefs = await prefsCon(premium: true); await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); expect(prefs.getInt(claveAusenciasLicencia), 1); reloj.avanzar(const Duration(days: 1)); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.poseida, ); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isTrue); expect(prefs.getInt(claveAusenciasLicencia), isNull); }); test('poseida con la flag en false desbloquea en silencio ' '(reinstalacion)', () async { final prefs = await prefsCon(premium: false); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.poseida, ); expect(cambio, CambioLicencia.desbloqueada); expect(prefs.getBool(claveCompraPremium), isTrue); }); test('una sola ausencia NO revoca', () async { final prefs = await prefsCon(premium: true); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isTrue); }); test('dos ausencias separadas por el umbral revocan', () async { final prefs = await prefsCon(premium: true); await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); reloj.avanzar(intervaloVerificacionLicencia); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); expect(cambio, CambioLicencia.revocada); expect(prefs.getBool(claveCompraPremium), isFalse); expect(prefs.getInt(claveAusenciasLicencia), isNull); }); test('dos ausencias demasiado juntas NO revocan', () async { final prefs = await prefsCon(premium: true); await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); // Simulates the throttle having been bypassed (e.g. prefs cleared by // a second engine): only the spacing guard stands between a transient // empty Play cache and a false revocation. await prefs.remove(claveUltimaVerificacionLicencia); await prefs.remove(claveUltimoIntentoLicencia); reloj.avanzar(separacionMinimaAusencias - const Duration(minutes: 1)); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isTrue); }); test('ausencia seguida de poseida reinicia: la siguiente ausencia ' 'vuelve a contar desde cero', () async { final prefs = await prefsCon(premium: true); final consulta = _ConsultaFalsa([ ResultadoVerificacionLicencia.noPoseida, ResultadoVerificacionLicencia.poseida, ResultadoVerificacionLicencia.noPoseida, ]); await verificar(prefs, consulta.call); reloj.avanzar(const Duration(days: 1)); await verificar(prefs, consulta.call); reloj.avanzar(const Duration(days: 1)); final cambio = await verificar(prefs, consulta.call); expect(consulta.llamadas, 3); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isTrue); expect(prefs.getInt(claveAusenciasLicencia), 1); }); test('noPoseida con la flag ya en false no hace nada', () async { final prefs = await prefsCon(premium: false); final cambio = await verificar( prefs, () async => ResultadoVerificacionLicencia.noPoseida, ); expect(cambio, CambioLicencia.sinCambios); expect(prefs.getBool(claveCompraPremium), isFalse); expect(prefs.getInt(claveAusenciasLicencia), isNull); }); test('throttle: no vuelve a consultar dentro de las 24h tras una ' 'verificacion con respuesta', () async { final prefs = await prefsCon(premium: true); final consulta = _ConsultaFalsa([ResultadoVerificacionLicencia.poseida]); await verificar(prefs, consulta.call); reloj.avanzar(intervaloVerificacionLicencia - const Duration(minutes: 1)); await verificar(prefs, consulta.call); expect(consulta.llamadas, 1); reloj.avanzar(const Duration(minutes: 1)); await verificar(prefs, consulta.call); expect(consulta.llamadas, 2); }); test('throttle: tras un desconocido reintenta pasada la ventana corta, ' 'no antes', () async { final prefs = await prefsCon(premium: true); final consulta = _ConsultaFalsa([ ResultadoVerificacionLicencia.desconocido, ]); await verificar(prefs, consulta.call); reloj.avanzar(intervaloReintentoLicencia - const Duration(minutes: 1)); await verificar(prefs, consulta.call); expect(consulta.llamadas, 1); reloj.avanzar(const Duration(minutes: 1)); await verificar(prefs, consulta.call); expect(consulta.llamadas, 2); }); test( 'un reloj que retrocede no bloquea la verificacion para siempre', () async { final prefs = await prefsCon(premium: true); final consulta = _ConsultaFalsa([ ResultadoVerificacionLicencia.poseida, ]); await verificar(prefs, consulta.call); reloj.avanzar(const Duration(days: -3)); await verificar(prefs, consulta.call); expect(consulta.llamadas, 2); }, ); test('solo UNA verificacion en vuelo: llamadas concurrentes comparten la ' 'misma consulta', () async { final prefs = await prefsCon(premium: true); final respuesta = Completer(); var llamadas = 0; Future consultar() { llamadas++; return respuesta.future; } final a = verificar(prefs, consultar); final b = verificar(prefs, consultar); respuesta.complete(ResultadoVerificacionLicencia.poseida); await Future.wait([a, b]); expect(llamadas, 1); }); }); }