From b6e66e75ce27747aa0e3674623bd6f3b80f77e8a Mon Sep 17 00:00:00 2001 From: freetlab Date: Fri, 22 May 2026 17:20:40 +0200 Subject: [PATCH] test(favorites): cover sqlite migrations --- TODO.md | 2 +- lib/servicios/servicio_favoritos.dart | 43 +++++-- pubspec.lock | 16 +++ pubspec.yaml | 1 + .../servicio_favoritos_sqlite_test.dart | 118 ++++++++++++++++++ 5 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 test/servicios/servicio_favoritos_sqlite_test.dart diff --git a/TODO.md b/TODO.md index db62f12..6c379a5 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/lib/servicios/servicio_favoritos.dart b/lib/servicios/servicio_favoritos.dart index bfcaf7b..538444b 100644 --- a/lib/servicios/servicio_favoritos.dart +++ b/lib/servicios/servicio_favoritos.dart @@ -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 Function()? databasePathProvider, + String? databaseName, + }) : _databaseFactory = databaseFactory, + _databasePathProvider = databasePathProvider ?? getDatabasesPath, + _databaseName = databaseName ?? _dbName; + + final DatabaseFactory? _databaseFactory; + final Future Function() _databasePathProvider; + final String _databaseName; + Database? _db; Future get _database async { @@ -21,10 +33,27 @@ class ServicioFavoritos { return _db!; } + Future cerrar() async { + await _db?.close(); + _db = null; + } + Future _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'); diff --git a/pubspec.lock b/pubspec.lock index 60e3709..69b3da8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index edd76e1..619fbed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/servicios/servicio_favoritos_sqlite_test.dart b/test/servicios/servicio_favoritos_sqlite_test.dart new file mode 100644 index 0000000..c1b3015 --- /dev/null +++ b/test/servicios/servicio_favoritos_sqlite_test.dart @@ -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, + ); +}