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.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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'});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user