fix(data): race-safe DB open, graceful unknown types, explicit id error + fallback tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
51c6ef904d
commit
6885771081
@ -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<Database>? _dbFuture;
|
||||
|
||||
Future<Database> _getDb() async {
|
||||
_database ??= await openDatabase(
|
||||
Future<Database> _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<List<Pokemon>> getAll() async {
|
||||
|
||||
@ -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<String, dynamic> 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<String, dynamic>?;
|
||||
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,
|
||||
|
||||
@ -49,6 +49,13 @@ class FakeRemote implements PokemonRemoteDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
class ThrowingRemote implements PokemonRemoteDataSource {
|
||||
@override
|
||||
Future<List<Pokemon>> getAll() async => throw Exception('network');
|
||||
@override
|
||||
Future<Pokemon> 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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user