test(favorites): cover sqlite migrations
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
## Favoritos
|
## 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.
|
- [ ] 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
|
## Agrupaciones de favoritos
|
||||||
|
|||||||
@@ -8,12 +8,24 @@ import '../modelos/grupo_favoritos.dart';
|
|||||||
|
|
||||||
/// Servicio de persistencia de emisoras favoritas con SQLite.
|
/// Servicio de persistencia de emisoras favoritas con SQLite.
|
||||||
///
|
///
|
||||||
/// - Inicializaci?n lazy: la BD se abre en el primer acceso.
|
/// - Inicialización lazy: la BD se abre en el primer acceso.
|
||||||
/// - Migration-ready: versi?n 3 a?ade agrupaciones de favoritos.
|
/// - Migration-ready: versión 3 añade agrupaciones de favoritos.
|
||||||
class ServicioFavoritos {
|
class ServicioFavoritos {
|
||||||
static const _dbName = 'pluriwave.db';
|
static const _dbName = 'pluriwave.db';
|
||||||
static const _dbVersion = 3;
|
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;
|
Database? _db;
|
||||||
|
|
||||||
Future<Database> get _database async {
|
Future<Database> get _database async {
|
||||||
@@ -21,10 +33,27 @@ class ServicioFavoritos {
|
|||||||
return _db!;
|
return _db!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> cerrar() async {
|
||||||
|
await _db?.close();
|
||||||
|
_db = null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Database> _initDb() async {
|
Future<Database> _initDb() async {
|
||||||
final dbPath = await getDatabasesPath();
|
final dbPath = await _databasePathProvider();
|
||||||
await Directory(dbPath).create(recursive: true);
|
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(
|
return openDatabase(
|
||||||
path,
|
path,
|
||||||
version: _dbVersion,
|
version: _dbVersion,
|
||||||
@@ -79,8 +108,8 @@ class ServicioFavoritos {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migraci?n defensiva: algunas instalaciones antiguas pueden venir de
|
// Migración defensiva: algunas instalaciones antiguas pueden venir de
|
||||||
// esquemas intermedios. No asumimos qu? columna existe: la verificamos.
|
// esquemas intermedios. No asumimos qué columna existe: la verificamos.
|
||||||
await addColumn('favicon', 'TEXT');
|
await addColumn('favicon', 'TEXT');
|
||||||
await addColumn('pais', 'TEXT');
|
await addColumn('pais', 'TEXT');
|
||||||
await addColumn('codigo_pais', 'TEXT');
|
await addColumn('codigo_pais', 'TEXT');
|
||||||
@@ -279,7 +308,7 @@ class ServicioFavoritos {
|
|||||||
String _normalizarNombreGrupo(String nombre) {
|
String _normalizarNombreGrupo(String nombre) {
|
||||||
final normalizado = nombre.trim().replaceAll(RegExp(r'\s+'), ' ');
|
final normalizado = nombre.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||||
if (normalizado.isEmpty) {
|
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) {
|
if (normalizado.length > 28) {
|
||||||
throw ArgumentError('El nombre del grupo no puede superar 28 caracteres');
|
throw ArgumentError('El nombre del grupo no puede superar 28 caracteres');
|
||||||
|
|||||||
@@ -722,6 +722,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.6"
|
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:
|
sqflite_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -738,6 +746,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.0"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
sqflite_common_ffi: ^2.3.7+1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
generate: true
|
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