Files
unionflow-mobile-apps/test/features/events/bloc/evenements_bloc_test.dart
dahoud 37db88672b feat: BLoC tests complets + sécurité production + freerasp 7.5.1 migration
## Tests BLoC (Task P2.4 Mobile)
- 25 nouveaux fichiers *_bloc_test.dart + mocks générés (build_runner)
- Features couvertes : authentication, admin_users, adhesions, backup,
  communication/messaging, contributions, dashboard, finance (approval/budget),
  events, explore/network, feed, logs_monitoring, notifications, onboarding,
  organizations (switcher/types/CRUD), profile, reports, settings, solidarity
- ~380 tests, > 80% coverage BLoCs

## Sécurité Production (Task P2.2)
- lib/core/security/app_integrity_service.dart (freerasp 7.5.1)
- Migration API breaking changes freerasp 7.5.1 :
  - onRootDetected → onPrivilegedAccess
  - onDebuggerDetected → onDebug
  - onSignatureDetected → onAppIntegrity
  - onHookDetected → onHooks
  - onEmulatorDetected → onSimulator
  - onUntrustedInstallationSourceDetected → onUnofficialStore
  - onDeviceBindingDetected → onDeviceBinding
  - onObfuscationIssuesDetected → onObfuscationIssues
  - Talsec.start() split → start() + attachListener()
  - const AndroidConfig/IOSConfig → final (constructors call ConfigVerifier)
  - supportedAlternativeStores → supportedStores

## Pubspec
- bloc_test: ^9.1.7 → ^10.0.0 (compat flutter_bloc ^9.0.0)
- freerasp 7.5.1

## Config
- android/app/build.gradle : ajustements release
- lib/core/config/environment.dart : URLs API actualisées
- lib/main.dart + app_router : intégrations sécurité/BLoC

## Cleanup
- Suppression docs intermédiaires (TACHES_*.md, TASK_*_COMPLETION_REPORT.md,
  TESTS_UNITAIRES_PROGRESS.md)
- .g.dart régénérés (json_serializable)
- .mocks.dart régénérés (mockito)

## Résultat
- 142 fichiers, +27 596 insertions
- Toutes les tâches P2 mobile complétées

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:42:35 +00:00

686 lines
22 KiB
Dart

/// 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<EvenementModel> 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<EvenementsInitial>());
bloc.close();
});
// ---- LoadEvenements ------------------------------------------------------
group('LoadEvenements', () {
final evenement = _makeEvenement();
final searchResult = _makeSearchResult([evenement]);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsLoaded>()
.having((s) => s.evenements.length, 'count', 1)
.having((s) => s.total, 'total', 1),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsRefreshing>()
.having((s) => s.currentEvenements.length, 'current', 1),
isA<EvenementsLoaded>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsNetworkError>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsLoaded>(),
],
verify: (_) => verify(mockGetEvents(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: 'gala')),
);
});
// ---- LoadEvenementById ---------------------------------------------------
group('LoadEvenementById', () {
final evenement = _makeEvenement();
blocTest<EvenementsBloc, EvenementsState>(
'emits [Loading, EvenementDetailLoaded] when found',
build: () {
when(mockGetEventById.call(any)).thenAnswer((_) async => evenement);
return buildBloc();
},
act: (b) => b.add(const LoadEvenementById('1')),
expect: () => [
isA<EvenementsLoading>(),
isA<EvenementDetailLoaded>()
.having((s) => s.evenement.id, 'id', 1),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>()
.having((s) => s.code, 'code', '404'),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- CreateEvenement -----------------------------------------------------
group('CreateEvenement', () {
final evenement = _makeEvenement(id: 99);
blocTest<EvenementsBloc, EvenementsState>(
'emits [Loading, EvenementCreated] on success',
build: () {
when(mockCreateEvent.call(any)).thenAnswer((_) async => evenement);
return buildBloc();
},
act: (b) => b.add(CreateEvenement(evenement)),
expect: () => [
isA<EvenementsLoading>(),
isA<EvenementCreated>()
.having((s) => s.evenement.id, 'id', 99),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsValidationError>()
.having((s) => s.code, 'code', '400'),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- UpdateEvenement -----------------------------------------------------
group('UpdateEvenement', () {
final evenement = _makeEvenement();
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementUpdated>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsValidationError>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- DeleteEvenement -----------------------------------------------------
group('DeleteEvenement', () {
blocTest<EvenementsBloc, EvenementsState>(
'emits [Loading, EvenementDeleted] on success',
build: () {
when(mockDeleteEvent.call(any)).thenAnswer((_) async => null);
return buildBloc();
},
act: (b) => b.add(const DeleteEvenement('1')),
expect: () => [
isA<EvenementsLoading>(),
isA<EvenementDeleted>()
.having((s) => s.id, 'id', '1'),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- LoadEvenementsAVenir ------------------------------------------------
group('LoadEvenementsAVenir', () {
final result = _makeSearchResult([_makeEvenement()]);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsLoaded>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsNetworkError>(),
],
);
});
// ---- LoadEvenementsEnCours -----------------------------------------------
group('LoadEvenementsEnCours', () {
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsLoaded>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- LoadEvenementsPasses ------------------------------------------------
group('LoadEvenementsPasses', () {
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsLoaded>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- InscrireEvenement ---------------------------------------------------
group('InscrireEvenement', () {
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementInscrit>()
.having((s) => s.evenementId, 'evenementId', 'evt-1'),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsNetworkError>(),
],
);
});
// ---- DesinscrireEvenement ------------------------------------------------
group('DesinscrireEvenement', () {
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementDesinscrit>()
.having((s) => s.evenementId, 'evenementId', 'evt-1'),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- LoadParticipants ----------------------------------------------------
group('LoadParticipants', () {
final participants = [
{'id': 'm1', 'nom': 'Dupont', 'statut': 'CONFIRME'},
{'id': 'm2', 'nom': 'Martin', 'statut': 'EN_ATTENTE'},
];
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<ParticipantsLoaded>()
.having((s) => s.evenementId, 'evenementId', 'evt-1')
.having((s) => s.participants.length, 'count', 2),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'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<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
// ---- LoadEvenementsStats -------------------------------------------------
group('LoadEvenementsStats', () {
final stats = {'total': 20, 'aVenir': 5, 'enCours': 3};
blocTest<EvenementsBloc, EvenementsState>(
'emits [Loading, EvenementsStatsLoaded] on success',
build: () {
when(mockRepository.getEvenementsStats())
.thenAnswer((_) async => stats);
return buildBloc();
},
act: (b) => b.add(const LoadEvenementsStats()),
expect: () => [
isA<EvenementsLoading>(),
isA<EvenementsStatsLoaded>()
.having((s) => s.stats['total'], 'total', 20),
],
);
blocTest<EvenementsBloc, EvenementsState>(
'emits [Loading, Error] on exception',
build: () {
when(mockRepository.getEvenementsStats())
.thenThrow(Exception('stats error'));
return buildBloc();
},
act: (b) => b.add(const LoadEvenementsStats()),
expect: () => [
isA<EvenementsLoading>(),
isA<EvenementsError>(),
],
);
});
}