Files
pluriwave/test/servicios/servicio_alarmas_corrupcion_test.dart
FreeTLab 65c1ac2085
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m38s
fix(alarm): stop corrupt entries and unreadable payloads from wiping saved alarms
A single malformed alarm entry (bad/missing id, wrong type) used to
discard the ENTIRE persisted list on next load, and a fully unparseable
payload let the periodic recalculation silently overwrite it with an
empty one -- both destroyed valid alarms with no user action.

Adds a shared per-entry tolerant-parse helper (persistencia_tolerante.dart)
that skips and logs only the bad entry; ServicioAlarmas now normalizes its
cached raw after a partial load (no dirty-guard thrash) and sets a
degraded-read flag after a total decode failure that suppresses automatic
writes until a good read or an explicit user mutation restores authority.
2026-07-11 12:27:39 +02:00

184 lines
5.9 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _claveAlarmas = 'alarmas_musicales_v1';
/// SharedPreferences spy: only the members ServicioAlarmas touches are
/// implemented; everything else throws via noSuchMethod. Mirrors
/// servicio_alarmas_cache_test.dart's _PrefsEspia, plus initial-value
/// seeding so corruption scenarios can be set up directly.
class _PrefsEspia implements SharedPreferences {
_PrefsEspia({Map<String, Object>? inicial}) : _datos = {...?inicial};
final Map<String, Object> _datos;
int escriturasString = 0;
int lecturasString = 0;
@override
String? getString(String key) {
lecturasString++;
return _datos[key] as String?;
}
@override
Future<bool> setString(String key, String value) async {
escriturasString++;
_datos[key] = value;
return true;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
Map<String, dynamic> _alarmaValida(String id, int hora) => {
'id': id,
'nombre': 'Alarma $id',
'hora': hora,
'minuto': 0,
'tipoProgramacion': 'diaria',
'diasSemana': <int>[],
};
void main() {
group(
'ServicioAlarmas — lectura tolerante y guardia de degradacion '
'(persistence-resilience)',
() {
test(
'carga parcial (2 validas + 1 con id de tipo incorrecto): '
'recalcularTodas no reescribe en cada tick (D3, no-thrash)',
() async {
final raw = jsonEncode({
'alarmas': [
_alarmaValida('a1', 7),
_alarmaValida('a2', 8),
// id con tipo incorrecto -> AlarmaMusical.fromJson lanza al
// hacer `json['id'] as String`; debe saltarse, no tumbar todo.
{..._alarmaValida('a3', 9), 'id': 42},
],
'vacaciones': [],
'excepciones': [],
});
final prefs = _PrefsEspia(inicial: {_claveAlarmas: raw});
final ahora = DateTime(2026, 6, 11, 6, 0);
final servicio = ServicioAlarmas(prefs: prefs, reloj: () => ahora);
final config = await servicio.cargar();
expect(config.alarmas, hasLength(2));
expect(config.alarmas.map((a) => a.id).toSet(), {'a1', 'a2'});
final escriturasBase = prefs.escriturasString;
await servicio.recalcularTodas();
await servicio.recalcularTodas();
expect(
prefs.escriturasString - escriturasBase,
lessThanOrEqualTo(1),
reason:
'la cache normalizada evita que el dirty-guard se dispare '
'en cada tick de 60s (D3)',
);
},
);
test(
'carga totalmente corrupta: cargar() queda vacio, recalcularTodas '
'no escribe, el raw original persiste intacto (D4)',
() async {
const raw = '{bad';
final prefs = _PrefsEspia(inicial: {_claveAlarmas: raw});
final servicio = ServicioAlarmas(
prefs: prefs,
reloj: () => DateTime(2026, 6, 11, 6, 0),
);
final config = await servicio.cargar();
expect(config.alarmas, isEmpty);
final escriturasBase = prefs.escriturasString;
await servicio.recalcularTodas();
expect(prefs.escriturasString, escriturasBase);
expect(
prefs.getString(_claveAlarmas),
raw,
reason:
'el raw corrupto original debe preservarse intacto en '
'disco mientras la lectura este degradada',
);
},
);
test(
'guardarAlarma tras una lectura degradada escribe una vez y '
'restaura la autoridad de escritura (D4)',
() async {
const raw = '{bad';
final prefs = _PrefsEspia(inicial: {_claveAlarmas: raw});
var ahora = DateTime(2026, 6, 11, 6, 0);
final servicio = ServicioAlarmas(prefs: prefs, reloj: () => ahora);
await servicio.cargar();
final escriturasBase = prefs.escriturasString;
final nueva = servicio.crearAlarma(
nombre: 'Nueva',
hora: 7,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
await servicio.guardarAlarma(nueva);
expect(prefs.escriturasString, escriturasBase + 1);
expect(prefs.getString(_claveAlarmas), isNot(raw));
// Un cambio real de agenda despues del guardado explicito debe
// volver a escribir con normalidad -- la bandera de degradacion
// no puede quedar pegada para siempre.
final escriturasTrasGuardar = prefs.escriturasString;
ahora = DateTime(2026, 6, 12, 8, 0);
await servicio.recalcularTodas();
expect(prefs.escriturasString, escriturasTrasGuardar + 1);
},
);
test(
'entrada sin id se omite y jamas se le fabrica uno nuevo (D2)',
() async {
final raw = jsonEncode({
'alarmas': [
{
'nombre': 'Sin id',
'hora': 6,
'minuto': 0,
'tipoProgramacion': 'diaria',
'diasSemana': <int>[],
},
_alarmaValida('a1', 7),
_alarmaValida('a2', 8),
],
'vacaciones': [],
'excepciones': [],
});
final prefs = _PrefsEspia(inicial: {_claveAlarmas: raw});
final servicio = ServicioAlarmas(
prefs: prefs,
reloj: () => DateTime(2026, 6, 11, 6, 0),
);
final config = await servicio.cargar();
expect(config.alarmas, hasLength(2));
expect(config.alarmas.map((a) => a.id).toSet(), {'a1', 'a2'});
},
);
},
);
}