Files
pluriwave/test/estado/estado_alarmas_gating_test.dart
T
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00

180 lines
5.9 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/estado/estado_alarmas.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/fakes_alarmas.dart';
/// Freemium gating (freemium-gating spec, design ADR-3/ADR-5): the 5-alarm
/// cap for free-tier users, grandfathering of pre-existing alarms, and the
/// full premium gate on vacation-range creation. `esPremium` is a REQUIRED
/// constructor parameter with no default — every other suite passes
/// `() => true` explicitly to keep its pre-gate behavior, and the tests here
/// inject `() => false` to exercise the free tier.
AlarmaMusical _alarma(String id, {bool activa = true}) => AlarmaMusical(
id: id,
nombre: 'Alarma $id',
hora: 7,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
activa: activa,
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
EstadoAlarmas construir({required bool premium}) {
final android = FakePuertoAlarmasAndroid();
final estado = EstadoAlarmas(
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
android: android,
iniciarAutomaticamente: false,
esPremium: () => premium,
);
addTearDown(estado.dispose);
addTearDown(android.dispose);
return estado;
}
group('puedeCrearAlarma / cap de 5 (free tier)', () {
test('con 4 alarmas puede crear una mas', () async {
final estado = construir(premium: false);
for (var i = 1; i <= 4; i++) {
await estado.guardarAlarma(_alarma('a$i'));
}
expect(estado.puedeCrearAlarma(), isTrue);
});
test(
'con 5 alarmas (cualquier estado activa) no puede crear una 6a',
() async {
final estado = construir(premium: false);
for (var i = 1; i <= 4; i++) {
await estado.guardarAlarma(_alarma('a$i'));
}
await estado.guardarAlarma(_alarma('a5', activa: false));
expect(estado.puedeCrearAlarma(), isFalse);
},
);
test('la 6a alarma es bloqueada ANTES de programar en Android', () async {
final estado = construir(premium: false);
for (var i = 1; i <= 5; i++) {
await estado.guardarAlarma(_alarma('a$i'));
}
final android = estado.android as FakePuertoAlarmasAndroid;
final programadasPrevias = android.programadas.length;
final resultado = await estado.guardarAlarma(_alarma('a6'));
expect(resultado, ResultadoGuardarAlarma.limiteAlcanzado);
expect(estado.alarmas.length, 5);
expect(android.programadas.length, programadasPrevias);
});
test('editar una de las 5 alarmas existentes sigue funcionando', () async {
final estado = construir(premium: false);
for (var i = 1; i <= 5; i++) {
await estado.guardarAlarma(_alarma('a$i'));
}
final resultado = await estado.guardarAlarma(
_alarma('a3').copyWith(hora: 8),
);
expect(resultado, ResultadoGuardarAlarma.guardada);
expect(estado.alarmas.firstWhere((a) => a.id == 'a3').hora, 8);
});
test('usuario premium no tiene tope', () async {
final estado = construir(premium: true);
for (var i = 1; i <= 5; i++) {
await estado.guardarAlarma(_alarma('a$i'));
}
final resultado = await estado.guardarAlarma(_alarma('a6'));
expect(resultado, ResultadoGuardarAlarma.guardada);
expect(estado.alarmas.length, 6);
expect(estado.puedeCrearAlarma(), isTrue);
});
test(
'grandfathering: 8 alarmas preexistentes siguen funcionando, solo se bloquea la 9a',
() async {
// Simula alarmas ya persistidas antes de que el gate existiera:
// se crean en modo premium (sin tope) y luego se re-evalua en free.
final estadoPremium = construir(premium: true);
for (var i = 1; i <= 8; i++) {
await estadoPremium.guardarAlarma(_alarma('g$i'));
}
expect(estadoPremium.alarmas.length, 8);
// Editar una de las 8 preexistentes en free tier sigue funcionando.
final estadoFree = EstadoAlarmas(
servicio: estadoPremium.servicio,
android: estadoPremium.android,
iniciarAutomaticamente: false,
esPremium: () => false,
);
addTearDown(estadoFree.dispose);
await estadoFree.cargarPersistidasSinRecalcular();
expect(estadoFree.alarmas.length, 8);
final edicion = await estadoFree.guardarAlarma(
estadoFree.alarmas.first.copyWith(hora: 9),
);
expect(edicion, ResultadoGuardarAlarma.guardada);
expect(estadoFree.alarmas.length, 8);
// Una 9a alarma NUEVA sigue bloqueada.
final resultado = await estadoFree.guardarAlarma(_alarma('g9'));
expect(resultado, ResultadoGuardarAlarma.limiteAlcanzado);
expect(estadoFree.alarmas.length, 8);
},
);
});
group('crearRangoVacaciones — gate completo (freemium-gating)', () {
test('free tier: cualquier creacion de vacaciones es bloqueada', () async {
final estado = construir(premium: false);
final creada = await estado.crearRangoVacaciones(
RangoVacaciones(
id: 'v1',
nombre: 'Verano',
inicio: DateTime(2026, 7, 1),
fin: DateTime(2026, 7, 15),
),
);
expect(creada, isFalse);
expect(estado.vacaciones, isEmpty);
});
test('premium: crea vacaciones sin restriccion', () async {
final estado = construir(premium: true);
final creada = await estado.crearRangoVacaciones(
RangoVacaciones(
id: 'v1',
nombre: 'Verano',
inicio: DateTime(2026, 7, 1),
fin: DateTime(2026, 7, 15),
),
);
expect(creada, isTrue);
expect(estado.vacaciones, hasLength(1));
});
});
}