/// Tests unitaires pour EvenementsBloc library evenements_bloc_test; import 'package:bloc_test/bloc_test.dart'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:unionflow_mobile_apps/features/events/bloc/evenements_bloc.dart'; import 'package:unionflow_mobile_apps/features/events/bloc/evenements_event.dart'; import 'package:unionflow_mobile_apps/features/events/bloc/evenements_state.dart'; import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart'; import 'package:unionflow_mobile_apps/features/events/data/repositories/evenement_repository_impl.dart'; import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart'; import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_events.dart'; import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_event_by_id.dart'; import 'package:unionflow_mobile_apps/features/events/domain/usecases/create_event.dart' as uc; import 'package:unionflow_mobile_apps/features/events/domain/usecases/update_event.dart' as uc; import 'package:unionflow_mobile_apps/features/events/domain/usecases/delete_event.dart' as uc; import 'package:unionflow_mobile_apps/features/events/domain/usecases/register_for_event.dart'; import 'package:unionflow_mobile_apps/features/events/domain/usecases/cancel_registration.dart'; import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_event_participants.dart'; @GenerateMocks([ GetEvents, GetEventById, uc.CreateEvent, uc.UpdateEvent, uc.DeleteEvent, RegisterForEvent, CancelRegistration, GetEventParticipants, IEvenementRepository, ]) import 'evenements_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- final _now = DateTime(2025, 6, 15); final _later = DateTime(2025, 6, 16); EvenementModel _makeEvenement({int id = 1, String titre = 'Réunion mensuelle'}) => EvenementModel( id: id, titre: titre, dateDebut: _now, dateFin: _later, ); EvenementSearchResult _makeSearchResult(List items) => EvenementSearchResult( evenements: items, total: items.length, page: 0, size: 20, totalPages: items.isEmpty ? 0 : 1, ); DioException _makeDioException(int statusCode) => DioException( requestOptions: RequestOptions(path: '/test'), response: Response( requestOptions: RequestOptions(path: '/test'), statusCode: statusCode, ), type: DioExceptionType.badResponse, ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { late MockGetEvents mockGetEvents; late MockGetEventById mockGetEventById; late MockCreateEvent mockCreateEvent; late MockUpdateEvent mockUpdateEvent; late MockDeleteEvent mockDeleteEvent; late MockRegisterForEvent mockRegisterForEvent; late MockCancelRegistration mockCancelRegistration; late MockGetEventParticipants mockGetEventParticipants; late MockIEvenementRepository mockRepository; EvenementsBloc buildBloc() => EvenementsBloc( mockGetEvents, mockGetEventById, mockCreateEvent, mockUpdateEvent, mockDeleteEvent, mockRegisterForEvent, mockCancelRegistration, mockGetEventParticipants, mockRepository, ); setUp(() { mockGetEvents = MockGetEvents(); mockGetEventById = MockGetEventById(); mockCreateEvent = MockCreateEvent(); mockUpdateEvent = MockUpdateEvent(); mockDeleteEvent = MockDeleteEvent(); mockRegisterForEvent = MockRegisterForEvent(); mockCancelRegistration = MockCancelRegistration(); mockGetEventParticipants = MockGetEventParticipants(); mockRepository = MockIEvenementRepository(); }); // ---- initial state ------------------------------------------------------- test('initial state is EvenementsInitial', () { final bloc = buildBloc(); expect(bloc.state, isA()); bloc.close(); }); // ---- LoadEvenements ------------------------------------------------------ group('LoadEvenements', () { final evenement = _makeEvenement(); final searchResult = _makeSearchResult([evenement]); blocTest( 'emits [Loading, Loaded] on success', build: () { when(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'))) .thenAnswer((_) async => searchResult); return buildBloc(); }, act: (b) => b.add(const LoadEvenements()), expect: () => [ isA(), isA() .having((s) => s.evenements.length, 'count', 1) .having((s) => s.total, 'total', 1), ], ); blocTest( 'emits [Refreshing, Loaded] when refresh=true and state is EvenementsLoaded', build: () { when(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'))) .thenAnswer((_) async => _makeSearchResult([])); return buildBloc(); }, seed: () => EvenementsLoaded( evenements: [evenement], total: 1, totalPages: 1, ), act: (b) => b.add(const LoadEvenements(refresh: true)), expect: () => [ isA() .having((s) => s.currentEvenements.length, 'current', 1), isA(), ], ); blocTest( 'emits [Loading, Error] on generic exception', build: () { when(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'))) .thenThrow(Exception('network')); return buildBloc(); }, act: (b) => b.add(const LoadEvenements()), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, NetworkError] on DioException (non-auth)', build: () { when(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'))) .thenThrow(_makeDioException(500)); return buildBloc(); }, act: (b) => b.add(const LoadEvenements()), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, EvenementsError] on 401 DioException', build: () { when(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'))) .thenThrow(_makeDioException(401)); return buildBloc(); }, act: (b) => b.add(const LoadEvenements()), expect: () => [ isA(), isA(), ], ); blocTest( 'uses recherche parameter', build: () { when(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: 'gala')) .thenAnswer((_) async => _makeSearchResult([evenement])); return buildBloc(); }, act: (b) => b.add(const LoadEvenements(recherche: 'gala')), expect: () => [ isA(), isA(), ], verify: (_) => verify(mockGetEvents( page: anyNamed('page'), size: anyNamed('size'), recherche: 'gala')), ); }); // ---- LoadEvenementById --------------------------------------------------- group('LoadEvenementById', () { final evenement = _makeEvenement(); blocTest( 'emits [Loading, EvenementDetailLoaded] when found', build: () { when(mockGetEventById.call(any)).thenAnswer((_) async => evenement); return buildBloc(); }, act: (b) => b.add(const LoadEvenementById('1')), expect: () => [ isA(), isA() .having((s) => s.evenement.id, 'id', 1), ], ); blocTest( 'emits [Loading, EvenementsError(404)] when not found (null)', build: () { when(mockGetEventById.call(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const LoadEvenementById('missing')), expect: () => [ isA(), isA() .having((s) => s.code, 'code', '404'), ], ); blocTest( 'emits [Loading, Error] on exception', build: () { when(mockGetEventById.call(any)).thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadEvenementById('1')), expect: () => [ isA(), isA(), ], ); }); // ---- CreateEvenement ----------------------------------------------------- group('CreateEvenement', () { final evenement = _makeEvenement(id: 99); blocTest( 'emits [Loading, EvenementCreated] on success', build: () { when(mockCreateEvent.call(any)).thenAnswer((_) async => evenement); return buildBloc(); }, act: (b) => b.add(CreateEvenement(evenement)), expect: () => [ isA(), isA() .having((s) => s.evenement.id, 'id', 99), ], ); blocTest( 'emits [Loading, EvenementsValidationError] on 400 DioException', build: () { final dioEx = DioException( requestOptions: RequestOptions(path: '/test'), response: Response( requestOptions: RequestOptions(path: '/test'), statusCode: 400, data: { 'errors': {'titre': 'obligatoire'} }, ), type: DioExceptionType.badResponse, ); when(mockCreateEvent.call(any)).thenThrow(dioEx); return buildBloc(); }, act: (b) => b.add(CreateEvenement(evenement)), expect: () => [ isA(), isA() .having((s) => s.code, 'code', '400'), ], ); blocTest( 'emits [Loading, Error] on generic exception', build: () { when(mockCreateEvent.call(any)).thenThrow(Exception('creation failed')); return buildBloc(); }, act: (b) => b.add(CreateEvenement(evenement)), expect: () => [ isA(), isA(), ], ); }); // ---- UpdateEvenement ----------------------------------------------------- group('UpdateEvenement', () { final evenement = _makeEvenement(); blocTest( 'emits [Loading, EvenementUpdated] on success', build: () { when(mockUpdateEvent.call(any, any)).thenAnswer((_) async => evenement); return buildBloc(); }, act: (b) => b.add(UpdateEvenement('1', evenement)), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, ValidationError] on 400', build: () { final dioEx = DioException( requestOptions: RequestOptions(path: '/test'), response: Response( requestOptions: RequestOptions(path: '/test'), statusCode: 400, data: {'errors': {}}, ), type: DioExceptionType.badResponse, ); when(mockUpdateEvent.call(any, any)).thenThrow(dioEx); return buildBloc(); }, act: (b) => b.add(UpdateEvenement('1', evenement)), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, Error] on generic exception', build: () { when(mockUpdateEvent.call(any, any)) .thenThrow(Exception('update failed')); return buildBloc(); }, act: (b) => b.add(UpdateEvenement('1', evenement)), expect: () => [ isA(), isA(), ], ); }); // ---- DeleteEvenement ----------------------------------------------------- group('DeleteEvenement', () { blocTest( 'emits [Loading, EvenementDeleted] on success', build: () { when(mockDeleteEvent.call(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const DeleteEvenement('1')), expect: () => [ isA(), isA() .having((s) => s.id, 'id', '1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockDeleteEvent.call(any)).thenThrow(Exception('delete failed')); return buildBloc(); }, act: (b) => b.add(const DeleteEvenement('1')), expect: () => [ isA(), isA(), ], ); }); // ---- LoadEvenementsAVenir ------------------------------------------------ group('LoadEvenementsAVenir', () { final result = _makeSearchResult([_makeEvenement()]); blocTest( 'emits [Loading, Loaded] on success', build: () { when(mockRepository.getEvenementsAVenir( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => result); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsAVenir()), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, NetworkError] on DioException', build: () { when(mockRepository.getEvenementsAVenir( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(_makeDioException(503)); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsAVenir()), expect: () => [ isA(), isA(), ], ); }); // ---- LoadEvenementsEnCours ----------------------------------------------- group('LoadEvenementsEnCours', () { blocTest( 'emits [Loading, Loaded] on success', build: () { when(mockRepository.getEvenementsEnCours( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => _makeSearchResult([_makeEvenement()])); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsEnCours()), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, Error] on generic exception', build: () { when(mockRepository.getEvenementsEnCours( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsEnCours()), expect: () => [ isA(), isA(), ], ); }); // ---- LoadEvenementsPasses ------------------------------------------------ group('LoadEvenementsPasses', () { blocTest( 'emits [Loading, Loaded] on success', build: () { when(mockRepository.getEvenementsPasses( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => _makeSearchResult([_makeEvenement()])); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsPasses()), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, Error] on generic exception', build: () { when(mockRepository.getEvenementsPasses( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('server error')); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsPasses()), expect: () => [ isA(), isA(), ], ); }); // ---- InscrireEvenement --------------------------------------------------- group('InscrireEvenement', () { blocTest( 'emits [Loading, EvenementInscrit] on success', build: () { when(mockRegisterForEvent.call(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const InscrireEvenement('evt-1')), expect: () => [ isA(), isA() .having((s) => s.evenementId, 'evenementId', 'evt-1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockRegisterForEvent.call(any)) .thenThrow(Exception('registration failed')); return buildBloc(); }, act: (b) => b.add(const InscrireEvenement('evt-1')), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [Loading, NetworkError] on non-auth DioException', build: () { when(mockRegisterForEvent.call(any)) .thenThrow(_makeDioException(409)); return buildBloc(); }, act: (b) => b.add(const InscrireEvenement('evt-1')), expect: () => [ isA(), isA(), ], ); }); // ---- DesinscrireEvenement ------------------------------------------------ group('DesinscrireEvenement', () { blocTest( 'emits [Loading, EvenementDesinscrit] on success', build: () { when(mockCancelRegistration.call(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(const DesinscrireEvenement('evt-1')), expect: () => [ isA(), isA() .having((s) => s.evenementId, 'evenementId', 'evt-1'), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockCancelRegistration.call(any)) .thenThrow(Exception('cancel failed')); return buildBloc(); }, act: (b) => b.add(const DesinscrireEvenement('evt-1')), expect: () => [ isA(), isA(), ], ); }); // ---- LoadParticipants ---------------------------------------------------- group('LoadParticipants', () { final participants = [ {'id': 'm1', 'nom': 'Dupont', 'statut': 'CONFIRME'}, {'id': 'm2', 'nom': 'Martin', 'statut': 'EN_ATTENTE'}, ]; blocTest( 'emits [Loading, ParticipantsLoaded] on success', build: () { when(mockGetEventParticipants.call(any)) .thenAnswer((_) async => participants); return buildBloc(); }, act: (b) => b.add(const LoadParticipants('evt-1')), expect: () => [ isA(), isA() .having((s) => s.evenementId, 'evenementId', 'evt-1') .having((s) => s.participants.length, 'count', 2), ], ); blocTest( 'emits [Loading, Error] on failure', build: () { when(mockGetEventParticipants.call(any)) .thenThrow(Exception('participants error')); return buildBloc(); }, act: (b) => b.add(const LoadParticipants('evt-1')), expect: () => [ isA(), isA(), ], ); }); // ---- LoadEvenementsStats ------------------------------------------------- group('LoadEvenementsStats', () { final stats = {'total': 20, 'aVenir': 5, 'enCours': 3}; blocTest( 'emits [Loading, EvenementsStatsLoaded] on success', build: () { when(mockRepository.getEvenementsStats()) .thenAnswer((_) async => stats); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsStats()), expect: () => [ isA(), isA() .having((s) => s.stats['total'], 'total', 20), ], ); blocTest( 'emits [Loading, Error] on exception', build: () { when(mockRepository.getEvenementsStats()) .thenThrow(Exception('stats error')); return buildBloc(); }, act: (b) => b.add(const LoadEvenementsStats()), expect: () => [ isA(), isA(), ], ); }); }