test(favorites): cover sqlite migrations
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
|
||||
## Favoritos
|
||||
|
||||
- [ ] Revisar el sistema de guardado de favoritos en instalaciones nuevas y migradas: inicialización de SQLite, creación de ruta/base de datos, migraciones de columnas y refresco de estado tras guardar. Reporte: en un móvil no se están guardando favoritos.
|
||||
- [x] Revisar el sistema de guardado de favoritos en instalaciones nuevas y migradas: inicialización de SQLite, creación de ruta/base de datos, migraciones de columnas y refresco de estado tras guardar. Reporte: en un móvil no se están guardando favoritos.
|
||||
- [ ] Añadir tests de regresión para favoritos en base de datos real/migrada, incluyendo esquemas antiguos y primera instalación limpia.
|
||||
|
||||
## Agrupaciones de favoritos
|
||||
|
||||
@@ -8,12 +8,24 @@ import '../modelos/grupo_favoritos.dart';
|
||||
|
||||
/// Servicio de persistencia de emisoras favoritas con SQLite.
|
||||
///
|
||||
/// - Inicializaci?n lazy: la BD se abre en el primer acceso.
|
||||
/// - Migration-ready: versi?n 3 a?ade agrupaciones de favoritos.
|
||||
/// - Inicialización lazy: la BD se abre en el primer acceso.
|
||||
/// - Migration-ready: versión 3 añade agrupaciones de favoritos.
|
||||
class ServicioFavoritos {
|
||||
static const _dbName = 'pluriwave.db';
|
||||
static const _dbVersion = 3;
|
||||
|
||||
ServicioFavoritos({
|
||||
DatabaseFactory? databaseFactory,
|
||||
Future<String> Function()? databasePathProvider,
|
||||
String? databaseName,
|
||||
}) : _databaseFactory = databaseFactory,
|
||||
_databasePathProvider = databasePathProvider ?? getDatabasesPath,
|
||||
_databaseName = databaseName ?? _dbName;
|
||||
|
||||
final DatabaseFactory? _databaseFactory;
|
||||
final Future<String> Function() _databasePathProvider;
|
||||
final String _databaseName;
|
||||
|
||||
Database? _db;
|
||||
|
||||
Future<Database> get _database async {
|
||||
@@ -21,10 +33,27 @@ class ServicioFavoritos {
|
||||
return _db!;
|
||||
}
|
||||
|
||||
Future<void> cerrar() async {
|
||||
await _db?.close();
|
||||
_db = null;
|
||||
}
|
||||
|
||||
Future<Database> _initDb() async {
|
||||
final dbPath = await getDatabasesPath();
|
||||
final dbPath = await _databasePathProvider();
|
||||
await Directory(dbPath).create(recursive: true);
|
||||
final path = join(dbPath, _dbName);
|
||||
final path = join(dbPath, _databaseName);
|
||||
final factory = _databaseFactory;
|
||||
if (factory != null) {
|
||||
return factory.openDatabase(
|
||||
path,
|
||||
options: OpenDatabaseOptions(
|
||||
version: _dbVersion,
|
||||
onCreate: _onCreate,
|
||||
onUpgrade: _onUpgrade,
|
||||
onOpen: _asegurarEsquema,
|
||||
),
|
||||
);
|
||||
}
|
||||
return openDatabase(
|
||||
path,
|
||||
version: _dbVersion,
|
||||
@@ -79,8 +108,8 @@ class ServicioFavoritos {
|
||||
}
|
||||
}
|
||||
|
||||
// Migraci?n defensiva: algunas instalaciones antiguas pueden venir de
|
||||
// esquemas intermedios. No asumimos qu? columna existe: la verificamos.
|
||||
// Migración defensiva: algunas instalaciones antiguas pueden venir de
|
||||
// esquemas intermedios. No asumimos qué columna existe: la verificamos.
|
||||
await addColumn('favicon', 'TEXT');
|
||||
await addColumn('pais', 'TEXT');
|
||||
await addColumn('codigo_pais', 'TEXT');
|
||||
@@ -279,7 +308,7 @@ class ServicioFavoritos {
|
||||
String _normalizarNombreGrupo(String nombre) {
|
||||
final normalizado = nombre.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||
if (normalizado.isEmpty) {
|
||||
throw ArgumentError('El nombre del grupo no puede estar vac?o');
|
||||
throw ArgumentError('El nombre del grupo no puede estar vacío');
|
||||
}
|
||||
if (normalizado.length > 28) {
|
||||
throw ArgumentError('El nombre del grupo no puede superar 28 caracteres');
|
||||
|
||||
@@ -722,6 +722,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: cd0c7f7de39a08f2d54ef144d9058c46eca8461879aaa648025643455c1e5a20
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0+3"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -738,6 +746,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -59,6 +59,7 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
sqflite_common_ffi: ^2.3.7+1
|
||||
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
||||
import 'package:pluriwave/servicios/servicio_favoritos.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
|
||||
setUpAll(() {
|
||||
sqfliteFfiInit();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('pluriwave_favoritos_');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
ServicioFavoritos crearServicio() {
|
||||
return ServicioFavoritos(
|
||||
databaseFactory: databaseFactoryFfi,
|
||||
databasePathProvider: () async => tempDir.path,
|
||||
);
|
||||
}
|
||||
|
||||
test('primera instalación crea esquema completo y guarda favoritos', () async {
|
||||
final servicio = crearServicio();
|
||||
addTearDown(servicio.cerrar);
|
||||
|
||||
await servicio.agregar(_emisora('radio-1', 'Radio Uno'));
|
||||
|
||||
final favoritos = await servicio.obtenerTodos();
|
||||
final grupos = await servicio.obtenerGrupos();
|
||||
|
||||
expect(favoritos, hasLength(1));
|
||||
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
||||
expect(grupos, hasLength(1));
|
||||
expect(grupos.single.esSinAsignar, isTrue);
|
||||
});
|
||||
|
||||
test('migra esquema antiguo sin grupo ni columnas nuevas', () async {
|
||||
final dbPath = p.join(tempDir.path, 'pluriwave.db');
|
||||
final db = await databaseFactoryFfi.openDatabase(
|
||||
dbPath,
|
||||
options: OpenDatabaseOptions(
|
||||
version: 1,
|
||||
onCreate: (db, version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE favoritos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT NOT NULL UNIQUE,
|
||||
nombre TEXT NOT NULL,
|
||||
url TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await db.insert('favoritos', {
|
||||
'uuid': 'legacy-1',
|
||||
'nombre': 'Legacy Radio',
|
||||
'url': 'https://example.com/legacy.mp3',
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
await db.close();
|
||||
|
||||
final servicio = crearServicio();
|
||||
addTearDown(servicio.cerrar);
|
||||
|
||||
final favoritos = await servicio.obtenerTodos();
|
||||
final grupos = await servicio.obtenerGrupos();
|
||||
|
||||
expect(favoritos, hasLength(1));
|
||||
expect(favoritos.single.uuid, 'legacy-1');
|
||||
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
||||
expect(grupos.singleWhere((g) => g.esSinAsignar).nombre, 'Sin asignar');
|
||||
|
||||
final grupo = await servicio.crearGrupo('Viajes');
|
||||
await servicio.asignarGrupo('legacy-1', grupo.id);
|
||||
expect(
|
||||
(await servicio.obtenerTodos()).single.grupoFavoritosId,
|
||||
grupo.id,
|
||||
);
|
||||
});
|
||||
|
||||
test('eliminar grupo reasigna sus favoritos a Sin asignar', () async {
|
||||
final servicio = crearServicio();
|
||||
addTearDown(servicio.cerrar);
|
||||
|
||||
await servicio.agregar(_emisora('radio-1', 'Radio Uno'));
|
||||
final grupo = await servicio.crearGrupo('Trabajo');
|
||||
await servicio.asignarGrupo('radio-1', grupo.id);
|
||||
|
||||
await servicio.eliminarGrupo(grupo.id);
|
||||
|
||||
final favoritos = await servicio.obtenerTodos();
|
||||
final grupos = await servicio.obtenerGrupos();
|
||||
expect(favoritos.single.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
||||
expect(grupos.any((g) => g.id == grupo.id), isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
Emisora _emisora(String uuid, String nombre) {
|
||||
return Emisora(
|
||||
uuid: uuid,
|
||||
nombre: nombre,
|
||||
url: 'https://example.com/$uuid.mp3',
|
||||
codec: 'MP3',
|
||||
bitrate: 192,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user