feat(data): add PokemonRepository implementation + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
42bd9f7c20
commit
51c6ef904d
63
lib/data/repositories/pokemon_repository_impl.dart
Normal file
63
lib/data/repositories/pokemon_repository_impl.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import '../../core/config/app_constants.dart';
|
||||||
|
import '../../core/logger.dart';
|
||||||
|
import '../../domain/entities/pokemon.dart';
|
||||||
|
import '../../domain/repositories/pokemon_repository.dart';
|
||||||
|
import '../datasources/pokemon_local_datasource.dart';
|
||||||
|
import '../datasources/pokemon_remote_datasource.dart';
|
||||||
|
|
||||||
|
/// Implémentation : DB locale d'abord, complétée/repliée sur l'API.
|
||||||
|
/// [local] vaut `null` sur le web (pas de SQLite).
|
||||||
|
class PokemonRepositoryImpl implements PokemonRepository {
|
||||||
|
final PokemonRemoteDataSource remote;
|
||||||
|
final PokemonLocalDataSource? local;
|
||||||
|
|
||||||
|
PokemonRepositoryImpl({required this.remote, this.local});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async {
|
||||||
|
final localDs = local;
|
||||||
|
if (localDs == null) return remote.getAll();
|
||||||
|
|
||||||
|
final cached = await localDs.getAll();
|
||||||
|
if (cached.length >= AppConstants.totalPokemon) return cached;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final remoteList = await remote.getAll();
|
||||||
|
await localDs.saveAll(remoteList);
|
||||||
|
return localDs.getAll();
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error('Sync getAll échouée', e, st);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Pokemon?> getById(int id) async {
|
||||||
|
final localDs = local;
|
||||||
|
if (localDs != null) {
|
||||||
|
final cached = await localDs.getById(id);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final fetched = await remote.getById(id);
|
||||||
|
if (localDs != null) await localDs.saveAll([fetched]);
|
||||||
|
return fetched;
|
||||||
|
} catch (e, st) {
|
||||||
|
AppLogger.error('getById($id) échoué', e, st);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveAll(List<Pokemon> pokemons) async =>
|
||||||
|
local?.saveAll(pokemons);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(Pokemon pokemon) async => local?.update(pokemon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> caughtCount() async => (await local?.caughtCount()) ?? 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> seenCount() async => (await local?.seenCount()) ?? 0;
|
||||||
|
}
|
||||||
106
test/data/pokemon_repository_test.dart
Normal file
106
test/data/pokemon_repository_test.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pokeguess/domain/entities/pokemon.dart';
|
||||||
|
import 'package:pokeguess/data/datasources/pokemon_local_datasource.dart';
|
||||||
|
import 'package:pokeguess/data/datasources/pokemon_remote_datasource.dart';
|
||||||
|
import 'package:pokeguess/data/repositories/pokemon_repository_impl.dart';
|
||||||
|
|
||||||
|
Pokemon _mk(int id, {bool caught = false}) => Pokemon(
|
||||||
|
name: 'p$id',
|
||||||
|
id: id,
|
||||||
|
type1: PokemonType.normal,
|
||||||
|
hp: 1, atk: 1, def: 1, spd: 1,
|
||||||
|
isCaught: caught,
|
||||||
|
);
|
||||||
|
|
||||||
|
class FakeLocal implements PokemonLocalDataSource {
|
||||||
|
final Map<int, Pokemon> store;
|
||||||
|
FakeLocal([Map<int, Pokemon>? init]) : store = init ?? {};
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async => store.values.toList();
|
||||||
|
@override
|
||||||
|
Future<Pokemon?> getById(int id) async => store[id];
|
||||||
|
@override
|
||||||
|
Future<void> saveAll(List<Pokemon> pokemons) async {
|
||||||
|
for (final p in pokemons) store[p.id] = p;
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Future<void> update(Pokemon pokemon) async => store[pokemon.id] = pokemon;
|
||||||
|
@override
|
||||||
|
Future<int> caughtCount() async =>
|
||||||
|
store.values.where((p) => p.isCaught).length;
|
||||||
|
@override
|
||||||
|
Future<int> seenCount() async => store.values.where((p) => p.isSeen).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeRemote implements PokemonRemoteDataSource {
|
||||||
|
final List<Pokemon> all;
|
||||||
|
int getAllCalls = 0;
|
||||||
|
int getByIdCalls = 0;
|
||||||
|
FakeRemote(this.all);
|
||||||
|
@override
|
||||||
|
Future<List<Pokemon>> getAll() async {
|
||||||
|
getAllCalls++;
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Future<Pokemon> getById(int id) async {
|
||||||
|
getByIdCalls++;
|
||||||
|
return all.firstWhere((p) => p.id == id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('getById', () {
|
||||||
|
test('retourne le cache local sans appeler l\'API', () async {
|
||||||
|
final local = FakeLocal({5: _mk(5)});
|
||||||
|
final remote = FakeRemote([_mk(5)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final p = await repo.getById(5);
|
||||||
|
expect(p?.id, 5);
|
||||||
|
expect(remote.getByIdCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('va chercher sur l\'API et met en cache si absent en local', () async {
|
||||||
|
final local = FakeLocal();
|
||||||
|
final remote = FakeRemote([_mk(7)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final p = await repo.getById(7);
|
||||||
|
expect(p?.id, 7);
|
||||||
|
expect(remote.getByIdCalls, 1);
|
||||||
|
expect(await local.getById(7), isNotNull); // mis en cache
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sans local (web), passe directement par l\'API', () async {
|
||||||
|
final remote = FakeRemote([_mk(9)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: null);
|
||||||
|
final p = await repo.getById(9);
|
||||||
|
expect(p?.id, 9);
|
||||||
|
expect(remote.getByIdCalls, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getAll', () {
|
||||||
|
test('si le cache est complet, ne rappelle pas l\'API', () async {
|
||||||
|
final store = {for (var i = 1; i <= 1025; i++) i: _mk(i)};
|
||||||
|
final local = FakeLocal(store);
|
||||||
|
final remote = FakeRemote([]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final list = await repo.getAll();
|
||||||
|
expect(list.length, 1025);
|
||||||
|
expect(remote.getAllCalls, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('si le cache est incomplet, synchronise depuis l\'API', () async {
|
||||||
|
final local = FakeLocal({1: _mk(1)});
|
||||||
|
final remote = FakeRemote([_mk(1), _mk(2), _mk(3)]);
|
||||||
|
final repo = PokemonRepositoryImpl(remote: remote, local: local);
|
||||||
|
|
||||||
|
final list = await repo.getAll();
|
||||||
|
expect(remote.getAllCalls, 1);
|
||||||
|
expect(list.length, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user