Files
unionflow-mobile-apps/test/features/adhesions/bloc/adhesions_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

541 lines
18 KiB
Dart

/// 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<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesions.length, 'count', 2),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesions, 'adhesions', isEmpty),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded),
],
verify: (_) => verify(mockRepository.getAll(page: 1, size: 5)),
);
});
// ---- LoadAdhesionsByMembre -----------------------------------------------
group('LoadAdhesionsByMembre', () {
final list = [_makeAdhesion()];
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesions.length, 'count', 1),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- LoadAdhesionsEnAttente ----------------------------------------------
group('LoadAdhesionsEnAttente', () {
final enAttente = [_makeAdhesion(statut: 'EN_ATTENTE')];
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesions.length, 'count', 1),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- LoadAdhesionsByStatut -----------------------------------------------
group('LoadAdhesionsByStatut', () {
final approved = [_makeAdhesion(statut: 'APPROUVEE')];
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- LoadAdhesionById ----------------------------------------------------
group('LoadAdhesionById', () {
final adhesion = _makeAdhesion();
blocTest<AdhesionsBloc, AdhesionsState>(
'emits loading then loaded with adhesionDetail',
build: () {
when(mockRepository.getById(any))
.thenAnswer((_) async => adhesion);
return buildBloc();
},
act: (b) => b.add(LoadAdhesionById('adh1')),
expect: () => [
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesionDetail?.id, 'detail id', 'adh1'),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'emits error when not found',
build: () {
when(mockRepository.getById(any))
.thenThrow(Exception('not found'));
return buildBloc();
},
act: (b) => b.add(LoadAdhesionById('missing')),
expect: () => [
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- CreateAdhesion ------------------------------------------------------
group('CreateAdhesion', () {
final adhesion = _makeAdhesion();
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
// Loading from auto-triggered LoadAdhesions
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
// Loaded from LoadAdhesions result
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'emits error on failure',
build: () {
when(mockRepository.create(any))
.thenThrow(Exception('creation failed'));
return buildBloc();
},
act: (b) => b.add(CreateAdhesion(adhesion)),
expect: () => [
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- ApprouverAdhesion ---------------------------------------------------
group('ApprouverAdhesion', () {
final approved = _makeAdhesion(statut: 'APPROUVEE');
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesionDetail?.statut, 'statut', 'APPROUVEE'),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- RejeterAdhesion -----------------------------------------------------
group('RejeterAdhesion', () {
final rejected = _makeAdhesion(statut: 'REJETEE');
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesionDetail?.statut, 'statut', 'REJETEE'),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- EnregistrerPaiementAdhesion -----------------------------------------
group('EnregistrerPaiementAdhesion', () {
final paid = _makeAdhesion(statut: 'PAYEE');
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded)
.having((s) => s.adhesionDetail?.statut, 'statut', 'PAYEE'),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loaded),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.loading),
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
// ---- LoadAdhesionsStats --------------------------------------------------
group('LoadAdhesionsStats', () {
final stats = {
'total': 10,
'enAttente': 3,
'approuvees': 5,
'rejetees': 2,
};
blocTest<AdhesionsBloc, AdhesionsState>(
'emits state with stats populated on success',
build: () {
when(mockRepository.getStats()).thenAnswer((_) async => stats);
return buildBloc();
},
act: (b) => b.add(const LoadAdhesionsStats()),
expect: () => [
isA<AdhesionsState>()
.having((s) => s.stats, 'stats', stats),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'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<AdhesionsState>()
.having((s) => s.stats, 'stats', isNull),
],
);
blocTest<AdhesionsBloc, AdhesionsState>(
'emits error on failure',
build: () {
when(mockRepository.getStats())
.thenThrow(Exception('stats error'));
return buildBloc();
},
act: (b) => b.add(const LoadAdhesionsStats()),
expect: () => [
isA<AdhesionsState>()
.having((s) => s.status, 'status', AdhesionsStatus.error),
],
);
});
}