From 688577108157a0d12145425272d46409196be201 Mon Sep 17 00:00:00 2001 From: Maxiwere45 Date: Tue, 9 Jun 2026 11:18:56 +0200 Subject: [PATCH] fix(data): race-safe DB open, graceful unknown types, explicit id error + fallback tests Co-Authored-By: Claude Opus 4.8 --- .../datasources/pokemon_local_datasource.dart | 7 +++---- lib/data/dto/pokemon_dto.dart | 14 ++++++++++--- test/data/pokemon_repository_test.dart | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/data/datasources/pokemon_local_datasource.dart b/lib/data/datasources/pokemon_local_datasource.dart index 96acf48..c08d686 100644 --- a/lib/data/datasources/pokemon_local_datasource.dart +++ b/lib/data/datasources/pokemon_local_datasource.dart @@ -4,10 +4,10 @@ import '../dto/pokemon_dto.dart'; /// Accès SQLite local au Pokédex. Schéma et migrations identiques à l'ancien PokedexDatabase. class PokemonLocalDataSource { - Database? _database; + Future? _dbFuture; - Future _getDb() async { - _database ??= await openDatabase( + Future _getDb() { + return _dbFuture ??= openDatabase( 'pokedex.db', version: 2, onUpgrade: (db, oldVersion, newVersion) async { @@ -22,7 +22,6 @@ class PokemonLocalDataSource { 'CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT, hp INTEGER NOT NULL, atk INTEGER NOT NULL, def INTEGER NOT NULL, spd INTEGER NOT NULL, description TEXT, isCaught INTEGER NOT NULL DEFAULT 0, isSeen INTEGER NOT NULL DEFAULT 0)'); }, ); - return _database!; } Future> getAll() async { diff --git a/lib/data/dto/pokemon_dto.dart b/lib/data/dto/pokemon_dto.dart index 8229d80..5387ff4 100644 --- a/lib/data/dto/pokemon_dto.dart +++ b/lib/data/dto/pokemon_dto.dart @@ -32,7 +32,9 @@ class PokemonDto { /// Construit une entité depuis la réponse Tyradex (objet unique ou élément de liste). /// [fallbackId] sert quand le JSON ne contient pas `pokedex_id`. static Pokemon fromTyradexJson(Map json, {int? fallbackId}) { - final id = (json['pokedex_id'] as int?) ?? fallbackId!; + final id = (json['pokedex_id'] as int?) ?? + fallbackId ?? + (throw ArgumentError('pokedex_id absent et fallbackId non fourni')); final nameMap = json['name'] as Map?; final name = nameMap?['fr'] ?? nameMap?['en'] ?? 'unknown'; @@ -77,9 +79,15 @@ class PokemonDto { return Pokemon( name: row['name'], id: row['id'], - type1: PokemonType.values.firstWhere((e) => e.name == row['type1']), + type1: PokemonType.values.firstWhere( + (e) => e.name == row['type1'], + orElse: () => PokemonType.unknown, + ), type2: row['type2'] != null - ? PokemonType.values.firstWhere((e) => e.name == row['type2']) + ? PokemonType.values.firstWhere( + (e) => e.name == row['type2'], + orElse: () => PokemonType.unknown, + ) : null, hp: row['hp'] ?? 0, atk: row['atk'] ?? 0, diff --git a/test/data/pokemon_repository_test.dart b/test/data/pokemon_repository_test.dart index 71c77c8..a1577cc 100644 --- a/test/data/pokemon_repository_test.dart +++ b/test/data/pokemon_repository_test.dart @@ -49,6 +49,13 @@ class FakeRemote implements PokemonRemoteDataSource { } } +class ThrowingRemote implements PokemonRemoteDataSource { + @override + Future> getAll() async => throw Exception('network'); + @override + Future getById(int id) async => throw Exception('network'); +} + void main() { group('getById', () { test('retourne le cache local sans appeler l\'API', () async { @@ -103,4 +110,18 @@ void main() { expect(list.length, 3); }); }); + + group('repli sur erreur', () { + test('getById retourne null si l\'API échoue et le cache est vide', () async { + final repo = PokemonRepositoryImpl(remote: ThrowingRemote(), local: FakeLocal()); + expect(await repo.getById(42), isNull); + }); + + test('getAll retourne le cache existant si la synchro API échoue', () async { + final local = FakeLocal({1: _mk(1)}); // cache incomplet -> tente l'API, qui échoue + final repo = PokemonRepositoryImpl(remote: ThrowingRemote(), local: local); + final list = await repo.getAll(); + expect(list.length, 1); // repli sur le cache + }); + }); }