/// Tests unitaires pour AdhesionsBloc library adhesions_bloc_test; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:unionflow_mobile_apps/features/adhesions/bloc/adhesions_bloc.dart'; import 'package:unionflow_mobile_apps/features/adhesions/data/models/adhesion_model.dart'; import 'package:unionflow_mobile_apps/features/adhesions/data/repositories/adhesion_repository.dart'; @GenerateMocks([AdhesionRepository]) import 'adhesions_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- AdhesionModel _makeAdhesion({ String id = 'adh1', String statut = 'EN_ATTENTE', }) => AdhesionModel( id: id, membreId: 'membre1', nomMembre: 'Dupont Jean', organisationId: 'org1', nomOrganisation: 'TestOrg', statut: statut, fraisAdhesion: 10000, ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { late MockAdhesionRepository mockRepository; AdhesionsBloc buildBloc() => AdhesionsBloc(mockRepository); setUp(() { mockRepository = MockAdhesionRepository(); }); // ---- initial state ------------------------------------------------------- test('initial state has status initial and empty adhesions list', () { final bloc = buildBloc(); expect(bloc.state.status, AdhesionsStatus.initial); expect(bloc.state.adhesions, isEmpty); bloc.close(); }); // ---- LoadAdhesions ------------------------------------------------------- group('LoadAdhesions', () { final adhesionList = [_makeAdhesion(), _makeAdhesion(id: 'adh2')]; blocTest( 'emits loading then loaded with adhesions list', build: () { when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => adhesionList); return buildBloc(); }, act: (b) => b.add(const LoadAdhesions()), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesions.length, 'count', 2), ], ); blocTest( 'emits loading then loaded with empty list', build: () { when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(const LoadAdhesions()), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesions, 'adhesions', isEmpty), ], ); blocTest( 'emits loading then error on exception', build: () { when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('network failure')); return buildBloc(); }, act: (b) => b.add(const LoadAdhesions()), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); blocTest( 'uses custom page and size', build: () { when(mockRepository.getAll(page: 1, size: 5)) .thenAnswer((_) async => [_makeAdhesion()]); return buildBloc(); }, act: (b) => b.add(const LoadAdhesions(page: 1, size: 5)), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded), ], verify: (_) => verify(mockRepository.getAll(page: 1, size: 5)), ); }); // ---- LoadAdhesionsByMembre ----------------------------------------------- group('LoadAdhesionsByMembre', () { final list = [_makeAdhesion()]; blocTest( 'emits loading then loaded with membre adhesions', build: () { when(mockRepository.getByMembre(any, page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => list); return buildBloc(); }, act: (b) => b.add(LoadAdhesionsByMembre('membre1')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesions.length, 'count', 1), ], ); blocTest( 'emits error when repository throws', build: () { when(mockRepository.getByMembre(any, page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(LoadAdhesionsByMembre('membre1')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- LoadAdhesionsEnAttente ---------------------------------------------- group('LoadAdhesionsEnAttente', () { final enAttente = [_makeAdhesion(statut: 'EN_ATTENTE')]; blocTest( 'emits loading then loaded with pending adhesions', build: () { when(mockRepository.getEnAttente( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => enAttente); return buildBloc(); }, act: (b) => b.add(const LoadAdhesionsEnAttente()), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesions.length, 'count', 1), ], ); blocTest( 'emits error on repository failure', build: () { when(mockRepository.getEnAttente( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadAdhesionsEnAttente()), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- LoadAdhesionsByStatut ----------------------------------------------- group('LoadAdhesionsByStatut', () { final approved = [_makeAdhesion(statut: 'APPROUVEE')]; blocTest( 'emits loading then loaded filtered by statut', build: () { when(mockRepository.getByStatut(any, page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => approved); return buildBloc(); }, act: (b) => b.add(LoadAdhesionsByStatut('APPROUVEE')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.getByStatut(any, page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('filter failed')); return buildBloc(); }, act: (b) => b.add(LoadAdhesionsByStatut('APPROUVEE')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- LoadAdhesionById ---------------------------------------------------- group('LoadAdhesionById', () { final adhesion = _makeAdhesion(); blocTest( 'emits loading then loaded with adhesionDetail', build: () { when(mockRepository.getById(any)) .thenAnswer((_) async => adhesion); return buildBloc(); }, act: (b) => b.add(LoadAdhesionById('adh1')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesionDetail?.id, 'detail id', 'adh1'), ], ); blocTest( 'emits error when not found', build: () { when(mockRepository.getById(any)) .thenThrow(Exception('not found')); return buildBloc(); }, act: (b) => b.add(LoadAdhesionById('missing')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- CreateAdhesion ------------------------------------------------------ group('CreateAdhesion', () { final adhesion = _makeAdhesion(); blocTest( 'creates adhesion then re-triggers LoadAdhesions', build: () { when(mockRepository.create(any)).thenAnswer((_) async => adhesion); // LoadAdhesions triggered internally when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => [adhesion]); return buildBloc(); }, act: (b) => b.add(CreateAdhesion(adhesion)), expect: () => [ // Loading from CreateAdhesion isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), // Loading from auto-triggered LoadAdhesions isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), // Loaded from LoadAdhesions result isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.create(any)) .thenThrow(Exception('creation failed')); return buildBloc(); }, act: (b) => b.add(CreateAdhesion(adhesion)), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- ApprouverAdhesion --------------------------------------------------- group('ApprouverAdhesion', () { final approved = _makeAdhesion(statut: 'APPROUVEE'); blocTest( 'approves adhesion and re-triggers LoadAdhesions', build: () { when(mockRepository.approuver(any, approuvePar: anyNamed('approuvePar'))) .thenAnswer((_) async => approved); when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => [approved]); return buildBloc(); }, act: (b) => b.add(ApprouverAdhesion('adh1', approuvePar: 'admin1')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesionDetail?.statut, 'statut', 'APPROUVEE'), isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.approuver(any, approuvePar: anyNamed('approuvePar'))) .thenThrow(Exception('approval failed')); return buildBloc(); }, act: (b) => b.add(ApprouverAdhesion('adh1')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- RejeterAdhesion ----------------------------------------------------- group('RejeterAdhesion', () { final rejected = _makeAdhesion(statut: 'REJETEE'); blocTest( 'rejects adhesion with motif and re-triggers LoadAdhesions', build: () { when(mockRepository.rejeter(any, any)) .thenAnswer((_) async => rejected); when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(RejeterAdhesion('adh1', 'Dossier incomplet')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesionDetail?.statut, 'statut', 'REJETEE'), isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.rejeter(any, any)) .thenThrow(Exception('rejection failed')); return buildBloc(); }, act: (b) => b.add(RejeterAdhesion('adh1', 'motif')), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- EnregistrerPaiementAdhesion ----------------------------------------- group('EnregistrerPaiementAdhesion', () { final paid = _makeAdhesion(statut: 'PAYEE'); blocTest( 'records payment then re-triggers LoadAdhesions', build: () { when(mockRepository.enregistrerPaiement( any, montantPaye: anyNamed('montantPaye'), methodePaiement: anyNamed('methodePaiement'), referencePaiement: anyNamed('referencePaiement'), )).thenAnswer((_) async => paid); when(mockRepository.getAll( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => [paid]); return buildBloc(); }, act: (b) => b.add(EnregistrerPaiementAdhesion( 'adh1', montantPaye: 10000, methodePaiement: 'WAVE_MONEY', referencePaiement: 'REF-001', )), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded) .having((s) => s.adhesionDetail?.statut, 'statut', 'PAYEE'), isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.enregistrerPaiement( any, montantPaye: anyNamed('montantPaye'), methodePaiement: anyNamed('methodePaiement'), referencePaiement: anyNamed('referencePaiement'), )).thenThrow(Exception('payment failed')); return buildBloc(); }, act: (b) => b.add(EnregistrerPaiementAdhesion('adh1', montantPaye: 5000)), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.loading), isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); // ---- LoadAdhesionsStats -------------------------------------------------- group('LoadAdhesionsStats', () { final stats = { 'total': 10, 'enAttente': 3, 'approuvees': 5, 'rejetees': 2, }; blocTest( 'emits state with stats populated on success', build: () { when(mockRepository.getStats()).thenAnswer((_) async => stats); return buildBloc(); }, act: (b) => b.add(const LoadAdhesionsStats()), expect: () => [ isA() .having((s) => s.stats, 'stats', stats), ], ); blocTest( 'emits state with null stats when repository returns null', build: () { when(mockRepository.getStats()).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const LoadAdhesionsStats()), expect: () => [ isA() .having((s) => s.stats, 'stats', isNull), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.getStats()) .thenThrow(Exception('stats error')); return buildBloc(); }, act: (b) => b.add(const LoadAdhesionsStats()), expect: () => [ isA() .having((s) => s.status, 'status', AdhesionsStatus.error), ], ); }); }