/// Tests unitaires pour ContributionsBloc library contributions_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/contributions/bloc/contributions_bloc.dart'; import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_event.dart'; import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_state.dart'; import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart'; import 'package:unionflow_mobile_apps/features/contributions/data/repositories/contribution_repository.dart'; import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart'; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contributions.dart'; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_by_id.dart'; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/create_contribution.dart' as uc; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/update_contribution.dart' as uc; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/delete_contribution.dart' as uc; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/pay_contribution.dart'; import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_stats.dart'; @GenerateMocks([ GetContributions, GetContributionById, uc.CreateContribution, uc.UpdateContribution, uc.DeleteContribution, PayContribution, GetContributionStats, IContributionRepository, ]) import 'contributions_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- ContributionModel _makeContribution({ String id = 'c1', ContributionStatus statut = ContributionStatus.payee, }) => ContributionModel( id: id, membreId: 'membre1', montant: 5000, dateEcheance: DateTime(2025, 12, 31), annee: 2025, statut: statut, ); ContributionPageResult _makePageResult(List items) => ContributionPageResult( contributions: items, total: items.length, page: 0, size: 20, totalPages: items.isEmpty ? 0 : 1, ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { late MockGetContributions mockGetContributions; late MockGetContributionById mockGetContributionById; late MockCreateContribution mockCreateContribution; late MockUpdateContribution mockUpdateContribution; late MockDeleteContribution mockDeleteContribution; late MockPayContribution mockPayContribution; late MockGetContributionStats mockGetContributionStats; late MockIContributionRepository mockRepository; ContributionsBloc buildBloc() => ContributionsBloc( mockGetContributions, mockGetContributionById, mockCreateContribution, mockUpdateContribution, mockDeleteContribution, mockPayContribution, mockGetContributionStats, mockRepository, ); setUp(() { mockGetContributions = MockGetContributions(); mockGetContributionById = MockGetContributionById(); mockCreateContribution = MockCreateContribution(); mockUpdateContribution = MockUpdateContribution(); mockDeleteContribution = MockDeleteContribution(); mockPayContribution = MockPayContribution(); mockGetContributionStats = MockGetContributionStats(); mockRepository = MockIContributionRepository(); }); // ---- initial state ------------------------------------------------------- test('initial state is ContributionsInitial', () { final bloc = buildBloc(); expect(bloc.state, isA()); bloc.close(); }); // ---- LoadContributions --------------------------------------------------- group('LoadContributions', () { final contribution = _makeContribution(); final pageResult = _makePageResult([contribution]); blocTest( 'emits [Loading, Loaded] on success', build: () { when(mockGetContributions( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => pageResult); return buildBloc(); }, act: (b) => b.add(const LoadContributions()), expect: () => [ isA(), isA() .having((s) => s.contributions.length, 'count', 1) .having((s) => s.total, 'total', 1), ], ); blocTest( 'emits [Loading, Loaded] with empty list', build: () { when(mockGetContributions( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => _makePageResult([])); return buildBloc(); }, act: (b) => b.add(const LoadContributions()), expect: () => [ isA(), isA() .having((s) => s.contributions, 'contributions', isEmpty), ], ); blocTest( 'emits [Loading, Error] on exception', build: () { when(mockGetContributions( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('network')); return buildBloc(); }, act: (b) => b.add(const LoadContributions()), expect: () => [ isA(), isA(), ], ); blocTest( 'respects custom page and size parameters', build: () { final bigResult = _makePageResult( List.generate(5, (i) => _makeContribution(id: 'c$i'))); when(mockGetContributions(page: 2, size: 5)) .thenAnswer((_) async => bigResult); return buildBloc(); }, act: (b) => b.add(const LoadContributions(page: 2, size: 5)), expect: () => [ isA(), isA() .having((s) => s.contributions.length, 'count', 5), ], ); }); // ---- LoadContributionById ------------------------------------------------ group('LoadContributionById', () { final contribution = _makeContribution(); blocTest( 'emits [Loading, DetailLoaded] on success', build: () { when(mockGetContributionById.call(any)) .thenAnswer((_) async => contribution); return buildBloc(); }, act: (b) => b.add(const LoadContributionById(id: 'c1')), expect: () => [ isA(), isA() .having((s) => s.contribution.id, 'id', 'c1'), ], ); blocTest( 'emits [Loading, Error] when not found', build: () { when(mockGetContributionById.call(any)) .thenThrow(Exception('not found')); return buildBloc(); }, act: (b) => b.add(const LoadContributionById(id: 'missing')), expect: () => [ isA(), isA() .having((s) => s.message, 'message', contains('Contribution non trouvée')), ], ); }); // ---- CreateContribution -------------------------------------------------- group('CreateContribution', () { final newContribution = _makeContribution(id: 'new1'); blocTest( 'emits [Loading, ContributionCreated] on success', build: () { when(mockCreateContribution.call(any)) .thenAnswer((_) async => newContribution); return buildBloc(); }, act: (b) => b.add(CreateContribution(contribution: newContribution)), expect: () => [ isA(), isA() .having((s) => s.contribution.id, 'id', 'new1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockCreateContribution.call(any)) .thenThrow(Exception('validation error')); return buildBloc(); }, act: (b) => b.add(CreateContribution(contribution: newContribution)), expect: () => [ isA(), isA(), ], ); }); // ---- UpdateContribution -------------------------------------------------- group('UpdateContribution', () { final updatedContribution = _makeContribution(id: 'c1'); blocTest( 'emits [Loading, ContributionUpdated] on success', build: () { when(mockUpdateContribution.call(any, any)) .thenAnswer((_) async => updatedContribution); return buildBloc(); }, act: (b) => b.add(UpdateContribution( id: 'c1', contribution: updatedContribution)), expect: () => [ isA(), isA() .having((s) => s.contribution.id, 'id', 'c1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockUpdateContribution.call(any, any)) .thenThrow(Exception('update failed')); return buildBloc(); }, act: (b) => b.add(UpdateContribution( id: 'c1', contribution: updatedContribution)), expect: () => [ isA(), isA(), ], ); }); // ---- DeleteContribution -------------------------------------------------- group('DeleteContribution', () { blocTest( 'emits [Loading, ContributionDeleted] on success', build: () { when(mockDeleteContribution.call(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const DeleteContribution(id: 'c1')), expect: () => [ isA(), isA() .having((s) => s.id, 'id', 'c1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockDeleteContribution.call(any)) .thenThrow(Exception('delete failed')); return buildBloc(); }, act: (b) => b.add(const DeleteContribution(id: 'c1')), expect: () => [ isA(), isA(), ], ); }); // ---- SearchContributions ------------------------------------------------- group('SearchContributions', () { final results = [_makeContribution(id: 'sr1')]; final pageResult = _makePageResult(results); blocTest( 'emits [Loading, Loaded] on success with filters', build: () { when(mockRepository.getCotisations( page: anyNamed('page'), size: anyNamed('size'), membreId: anyNamed('membreId'), statut: anyNamed('statut'), type: anyNamed('type'), annee: anyNamed('annee'), )).thenAnswer((_) async => pageResult); return buildBloc(); }, act: (b) => b.add(const SearchContributions( membreId: 'membre1', statut: ContributionStatus.payee, annee: 2025, )), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.getCotisations( page: anyNamed('page'), size: anyNamed('size'), membreId: anyNamed('membreId'), statut: anyNamed('statut'), type: anyNamed('type'), annee: anyNamed('annee'), )).thenThrow(Exception('search failed')); return buildBloc(); }, act: (b) => b.add(const SearchContributions()), expect: () => [ isA(), isA(), ], ); }); // ---- LoadContributionsByMembre ------------------------------------------- group('LoadContributionsByMembre', () { final pageResult = _makePageResult([_makeContribution()]); blocTest( 'emits [Loading, Loaded] on success', build: () { when(mockRepository.getCotisations( page: anyNamed('page'), size: anyNamed('size'), membreId: anyNamed('membreId'), statut: anyNamed('statut'), type: anyNamed('type'), annee: anyNamed('annee'), )).thenAnswer((_) async => pageResult); return buildBloc(); }, act: (b) => b.add(const LoadContributionsByMembre(membreId: 'membre1')), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.getCotisations( page: anyNamed('page'), size: anyNamed('size'), membreId: anyNamed('membreId'), statut: anyNamed('statut'), type: anyNamed('type'), annee: anyNamed('annee'), )).thenThrow(Exception('load failed')); return buildBloc(); }, act: (b) => b.add(const LoadContributionsByMembre(membreId: 'membre1')), expect: () => [ isA(), isA(), ], ); }); // ---- LoadContributionsPayees --------------------------------------------- group('LoadContributionsPayees', () { final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee); final nonPayee = _makeContribution(id: 'np1', statut: ContributionStatus.nonPayee); blocTest( 'emits [Loading, Loaded] with only paid contributions', build: () { when(mockRepository.getMesCotisations()) .thenAnswer((_) async => _makePageResult([payee, nonPayee])); return buildBloc(); }, act: (b) => b.add(const LoadContributionsPayees()), expect: () => [ isA(), isA() .having((s) => s.contributions.length, 'count', 1) .having((s) => s.contributions.first.id, 'id', 'p1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.getMesCotisations()) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadContributionsPayees()), expect: () => [ isA(), isA(), ], ); }); // ---- LoadContributionsNonPayees ------------------------------------------ group('LoadContributionsNonPayees', () { final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee); final nonPayee = _makeContribution(id: 'np1', statut: ContributionStatus.nonPayee); blocTest( 'emits [Loading, Loaded] with only unpaid contributions', build: () { when(mockRepository.getMesCotisations()) .thenAnswer((_) async => _makePageResult([payee, nonPayee])); return buildBloc(); }, act: (b) => b.add(const LoadContributionsNonPayees()), expect: () => [ isA(), isA() .having((s) => s.contributions.length, 'count', 1) .having((s) => s.contributions.first.id, 'id', 'np1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.getMesCotisations()) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadContributionsNonPayees()), expect: () => [ isA(), isA(), ], ); }); // ---- LoadContributionsEnRetard ------------------------------------------- group('LoadContributionsEnRetard', () { final enRetard = _makeContribution(id: 'r1', statut: ContributionStatus.enRetard); final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee); blocTest( 'emits [Loading, Loaded] with only late contributions', build: () { when(mockRepository.getMesCotisations()) .thenAnswer((_) async => _makePageResult([enRetard, payee])); return buildBloc(); }, act: (b) => b.add(const LoadContributionsEnRetard()), expect: () => [ isA(), isA() .having((s) => s.contributions.length, 'count', 1) .having((s) => s.contributions.first.id, 'id', 'r1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.getMesCotisations()) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadContributionsEnRetard()), expect: () => [ isA(), isA(), ], ); }); // ---- RecordPayment ------------------------------------------------------- group('RecordPayment', () { final paid = _makeContribution(statut: ContributionStatus.payee); blocTest( 'emits [Loading, PaymentRecorded] on success', build: () { when(mockPayContribution.call( cotisationId: anyNamed('cotisationId'), montant: anyNamed('montant'), datePaiement: anyNamed('datePaiement'), methodePaiement: anyNamed('methodePaiement'), numeroPaiement: anyNamed('numeroPaiement'), referencePaiement: anyNamed('referencePaiement'), )).thenAnswer((_) async => paid); return buildBloc(); }, act: (b) => b.add(RecordPayment( contributionId: 'c1', montant: 5000, methodePaiement: PaymentMethod.especes, datePaiement: DateTime(2025, 6, 1), )), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockPayContribution.call( cotisationId: anyNamed('cotisationId'), montant: anyNamed('montant'), datePaiement: anyNamed('datePaiement'), methodePaiement: anyNamed('methodePaiement'), numeroPaiement: anyNamed('numeroPaiement'), referencePaiement: anyNamed('referencePaiement'), )).thenThrow(Exception('payment failed')); return buildBloc(); }, act: (b) => b.add(RecordPayment( contributionId: 'c1', montant: 5000, methodePaiement: PaymentMethod.waveMoney, datePaiement: DateTime(2025, 6, 1), )), expect: () => [ isA(), isA(), ], ); }); // ---- LoadContributionsStats ---------------------------------------------- group('LoadContributionsStats', () { final synthese = { 'montantDu': 10000.0, 'totalPayeAnnee': 5000.0, 'cotisationsEnAttente': 2, 'prochaineEcheance': '2025-07-31', 'anneeEnCours': 2025, }; blocTest( 'emits ContributionsStatsLoaded using synthese when non-null', build: () { when(mockGetContributionStats.call()).thenAnswer((_) async => synthese); // getContributions called when no preserved list when(mockGetContributions( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => _makePageResult([])); return buildBloc(); }, act: (b) => b.add(const LoadContributionsStats()), expect: () => [ isA() .having((s) => s.stats['isMesSynthese'], 'isMesSynthese', true), ], ); blocTest( 'falls back to repository.getStatistiques when synthese is null', build: () { when(mockGetContributionStats.call()).thenAnswer((_) async => null); when(mockGetContributions( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => _makePageResult([])); when(mockRepository.getStatistiques()) .thenAnswer((_) async => {'totalCotisations': 10}); return buildBloc(); }, act: (b) => b.add(const LoadContributionsStats()), expect: () => [ isA(), ], ); blocTest( 'emits ContributionsError on failure', build: () { when(mockGetContributionStats.call()) .thenThrow(Exception('stats failed')); return buildBloc(); }, act: (b) => b.add(const LoadContributionsStats()), expect: () => [isA()], ); }); // ---- GenerateAnnualContributions ----------------------------------------- group('GenerateAnnualContributions', () { blocTest( 'emits [Loading, ContributionsGenerated] on success', build: () { when(mockRepository.genererCotisationsAnnuelles(any)) .thenAnswer((_) async => 42); return buildBloc(); }, act: (b) => b.add(GenerateAnnualContributions( annee: 2025, montant: 10000, dateEcheance: DateTime(2025, 12, 31), )), expect: () => [ isA(), isA() .having((s) => s.nombreGenere, 'nombreGenere', 42), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.genererCotisationsAnnuelles(any)) .thenThrow(Exception('generate failed')); return buildBloc(); }, act: (b) => b.add(GenerateAnnualContributions( annee: 2025, montant: 10000, dateEcheance: DateTime(2025, 12, 31), )), expect: () => [ isA(), isA(), ], ); }); // ---- SendPaymentReminder ------------------------------------------------- group('SendPaymentReminder', () { blocTest( 'emits [Loading, ReminderSent] on success', build: () { when(mockRepository.envoyerRappel(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const SendPaymentReminder(contributionId: 'c1')), expect: () => [ isA(), isA() .having((s) => s.contributionId, 'contributionId', 'c1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRepository.envoyerRappel(any)) .thenThrow(Exception('reminder failed')); return buildBloc(); }, act: (b) => b.add(const SendPaymentReminder(contributionId: 'c1')), expect: () => [ isA(), isA(), ], ); }); }