Files
pluriwave/test/servicios/verificacion_licencia_test.dart
T
Javier Bautista Fernández 0b7919e72e
Build & Deploy PluriWave / Análisis de código (push) Successful in 41s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 4m8s
fix(compras): revocar el PRO tras un reembolso sin penalizar a quien pago
Verificacion silenciosa en segundo plano con queryPastPurchases, al abrir o
volver a la app y al cargar Android Auto. Solo revoca tras dos respuestas
validas de Play sin la compra separadas 12h; sin red o con error no toca nada.
Reactiva el PRO automaticamente si Play confirma la compra.
2026-09-18 11:45:43 +02:00

273 lines
8.8 KiB
Dart

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<ResultadoVerificacionLicencia> resultados;
int llamadas = 0;
Future<ResultadoVerificacionLicencia> 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<SharedPreferences> prefsCon({required bool premium}) async {
SharedPreferences.setMockInitialValues({claveCompraPremium: premium});
return SharedPreferences.getInstance();
}
Future<CambioLicencia> verificar(
SharedPreferences prefs,
Future<ResultadoVerificacionLicencia> 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<ResultadoVerificacionLicencia>();
var llamadas = 0;
Future<ResultadoVerificacionLicencia> 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);
});
});
}