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/members/bloc/membres_bloc.dart'; import 'package:unionflow_mobile_apps/features/members/bloc/membres_event.dart'; import 'package:unionflow_mobile_apps/features/members/bloc/membres_state.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_members.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_by_id.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/create_member.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/update_member.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/delete_member.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/search_members.dart'; import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_stats.dart'; import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart'; import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart'; import 'package:unionflow_mobile_apps/shared/models/membre_search_result.dart'; import 'package:unionflow_mobile_apps/shared/models/membre_search_criteria.dart'; @GenerateMocks([ GetMembers, GetMemberById, CreateMember, UpdateMember, DeleteMember, SearchMembers, GetMemberStats, IMembreRepository, ]) import 'membres_bloc_test.mocks.dart'; void main() { late MembresBloc bloc; late MockGetMembers mockGetMembers; late MockGetMemberById mockGetMemberById; late MockCreateMember mockCreateMember; late MockUpdateMember mockUpdateMember; late MockDeleteMember mockDeleteMember; late MockSearchMembers mockSearchMembers; late MockGetMemberStats mockGetMemberStats; late MockIMembreRepository mockRepository; const orgId = 'org-123'; MembreSearchResult emptyResult() => MembreSearchResult( membres: [], totalElements: 0, totalPages: 0, currentPage: 0, pageSize: 20, numberOfElements: 0, hasNext: false, hasPrevious: false, isFirst: true, isLast: true, criteria: MembreSearchCriteria(), executionTimeMs: 0, ); setUp(() { mockGetMembers = MockGetMembers(); mockGetMemberById = MockGetMemberById(); mockCreateMember = MockCreateMember(); mockUpdateMember = MockUpdateMember(); mockDeleteMember = MockDeleteMember(); mockSearchMembers = MockSearchMembers(); mockGetMemberStats = MockGetMemberStats(); mockRepository = MockIMembreRepository(); bloc = MembresBloc( mockGetMembers, mockGetMemberById, mockCreateMember, mockUpdateMember, mockDeleteMember, mockSearchMembers, mockGetMemberStats, mockRepository, ); }); tearDown(() => bloc.close()); // ───────────────────────────────────────────────────────────────────────── // SuperAdmin — accès global sans filtre organisation // ───────────────────────────────────────────────────────────────────────── group('LoadMembres — SuperAdmin (sans organisationId)', () { blocTest( 'appelle GetMembers sans filtre org et émet MembresLoaded sans organisationId', build: () { when(mockGetMembers( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => emptyResult()); return bloc; }, act: (b) => b.add(const LoadMembres()), expect: () => [ const MembresLoading(), isA().having( (s) => s.organisationId, 'organisationId', isNull, ), ], verify: (_) { // GetMembers doit être appelé avec les bons paramètres final captured = verify(mockGetMembers( page: captureAnyNamed('page'), size: captureAnyNamed('size'), recherche: captureAnyNamed('recherche'), )).captured; expect(captured[0], equals(0)); // page expect(captured[1], equals(20)); // size expect(captured[2], isNull); // recherche // SearchMembers ne doit jamais être appelé verifyNever(mockSearchMembers( criteria: anyNamed('criteria'), page: anyNamed('page'), size: anyNamed('size'), )); }, ); blocTest( 'transmet le terme de recherche à GetMembers', build: () { when(mockGetMembers( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => emptyResult()); return bloc; }, act: (b) => b.add(const LoadMembres(recherche: 'Jean')), expect: () => [ const MembresLoading(), isA(), ], verify: (_) { final captured = verify(mockGetMembers( page: captureAnyNamed('page'), size: captureAnyNamed('size'), recherche: captureAnyNamed('recherche'), )).captured; expect(captured[2], equals('Jean')); // recherche verifyNever(mockSearchMembers( criteria: anyNamed('criteria'), page: anyNamed('page'), size: anyNamed('size'), )); }, ); }); // ───────────────────────────────────────────────────────────────────────── // OrgAdmin — accès limité à son organisation // ───────────────────────────────────────────────────────────────────────── group('LoadMembres — OrgAdmin (avec organisationId)', () { blocTest( 'appelle SearchMembers avec organisationIds et émet MembresLoaded avec organisationId', build: () { when(mockSearchMembers( criteria: anyNamed('criteria'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => emptyResult()); return bloc; }, act: (b) => b.add(const LoadMembres(organisationId: orgId)), expect: () => [ const MembresLoading(), isA().having( (s) => s.organisationId, 'organisationId', equals(orgId), ), ], verify: (_) { // GetMembers ne doit JAMAIS être appelé pour un OrgAdmin verifyNever(mockGetMembers( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )); // SearchMembers doit être appelé avec l'organisationId dans les critères final captured = verify(mockSearchMembers( criteria: captureAnyNamed('criteria'), page: captureAnyNamed('page'), size: captureAnyNamed('size'), )).captured; final criteria = captured[0] as MembreSearchCriteria; expect(criteria.organisationIds, equals([orgId])); expect(criteria.query, isNull); expect(captured[1], equals(0)); // page expect(captured[2], equals(20)); // size }, ); blocTest( 'OrgAdmin avec recherche : ajoute query aux critères', build: () { when(mockSearchMembers( criteria: anyNamed('criteria'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => emptyResult()); return bloc; }, act: (b) => b.add(const LoadMembres(organisationId: orgId, recherche: 'Dupont')), expect: () => [ const MembresLoading(), isA(), ], verify: (_) { final captured = verify(mockSearchMembers( criteria: captureAnyNamed('criteria'), page: captureAnyNamed('page'), size: captureAnyNamed('size'), )).captured; final criteria = captured[0] as MembreSearchCriteria; expect(criteria.organisationIds, equals([orgId])); expect(criteria.query, equals('Dupont')); }, ); blocTest( 'OrgAdmin avec recherche vide : query non transmis', build: () { when(mockSearchMembers( criteria: anyNamed('criteria'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => emptyResult()); return bloc; }, act: (b) => b.add(const LoadMembres(organisationId: orgId, recherche: '')), expect: () => [const MembresLoading(), isA()], verify: (_) { final captured = verify(mockSearchMembers( criteria: captureAnyNamed('criteria'), page: captureAnyNamed('page'), size: captureAnyNamed('size'), )).captured; final criteria = captured[0] as MembreSearchCriteria; expect(criteria.organisationIds, equals([orgId])); // recherche vide → query null (pas transmis aux critères) expect(criteria.query, isNull); }, ); blocTest( 'OrgAdmin : la pagination conserve l organisationId', build: () { when(mockSearchMembers( criteria: anyNamed('criteria'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => emptyResult()); return bloc; }, act: (b) => b.add(const LoadMembres(organisationId: orgId, page: 1)), expect: () => [ const MembresLoading(), isA().having( (s) => s.organisationId, 'organisationId', equals(orgId), ), ], verify: (_) { final captured = verify(mockSearchMembers( criteria: captureAnyNamed('criteria'), page: captureAnyNamed('page'), size: captureAnyNamed('size'), )).captured; expect(captured[1], equals(1)); // page = 1 }, ); }); // ───────────────────────────────────────────────────────────────────────── // MembresLoaded.copyWith // ───────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────── // ResetMotDePasse // ───────────────────────────────────────────────────────────────────────── group('ResetMotDePasse', () { MembreCompletModel membreAvecPassword() => MembreCompletModel( id: 'membre-id-1', prenom: 'Jean', nom: 'Dupont', email: 'jean.dupont@test.com', motDePasseTemporaire: 'NewP@ss1234', ); blocTest( 'émet [MembresLoading, MotDePasseReinitialise] en cas de succès', build: () { when(mockRepository.resetMotDePasse('membre-id-1')) .thenAnswer((_) async => membreAvecPassword()); return bloc; }, act: (b) => b.add(const ResetMotDePasse('membre-id-1')), expect: () => [ const MembresLoading(), isA() .having((s) => s.membre.id, 'id', 'membre-id-1') .having((s) => s.membre.motDePasseTemporaire, 'password', 'NewP@ss1234'), ], verify: (_) { verify(mockRepository.resetMotDePasse('membre-id-1')).called(1); }, ); blocTest( 'émet [MembresLoading, MembresError] si le repository lève une exception', build: () { when(mockRepository.resetMotDePasse('membre-id-1')) .thenThrow(Exception('Membre sans compte Keycloak')); return bloc; }, act: (b) => b.add(const ResetMotDePasse('membre-id-1')), expect: () => [ const MembresLoading(), isA(), ], ); }); group('MembresLoaded.copyWith', () { test('preserve organisationId si non fourni', () { const state = MembresLoaded( membres: [], totalElements: 5, totalPages: 1, organisationId: orgId, ); final copy = state.copyWith(totalElements: 10); expect(copy.organisationId, equals(orgId)); expect(copy.totalElements, equals(10)); }); test('met à jour organisationId si fourni', () { const state = MembresLoaded( membres: [], totalElements: 5, totalPages: 1, organisationId: orgId, ); final copy = state.copyWith(organisationId: 'new-org'); expect(copy.organisationId, equals('new-org')); }); test('organisationId null par défaut si non défini', () { const state = MembresLoaded( membres: [], totalElements: 0, totalPages: 0, ); expect(state.organisationId, isNull); final copy = state.copyWith(totalElements: 1, totalPages: 1); expect(copy.organisationId, isNull); }); }); }