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

270 lines
9.8 KiB
Dart

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/reports/presentation/bloc/reports_bloc.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/generate_report.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/schedule_report.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
@GenerateMocks([GenerateReport, ScheduleReport, IReportsRepository])
import 'reports_bloc_test.mocks.dart';
void main() {
late ReportsBloc bloc;
late MockGenerateReport mockGenerateReport;
late MockScheduleReport mockScheduleReport;
late MockIReportsRepository mockRepository;
// ── Fixtures ──────────────────────────────────────────────────────────────
Map<String, dynamic> fakePerformance() =>
{'totalMembres': 150, 'cotisationsCollectees': 1500000.0};
Map<String, dynamic> fakeStatsMembres() =>
{'totalMembres': 150, 'membresActifs': 120};
Map<String, dynamic> fakeStatsCotisations() =>
{'totalCotisations': 200, 'totalMontant': 1500000.0};
Map<String, dynamic> fakeStatsEvenements() =>
{'totalEvenements': 12, 'participantsTotal': 300};
setUp(() {
mockGenerateReport = MockGenerateReport();
mockScheduleReport = MockScheduleReport();
mockRepository = MockIReportsRepository();
bloc = ReportsBloc(mockGenerateReport, mockScheduleReport, mockRepository);
});
tearDown(() => bloc.close());
// ── Initial state ─────────────────────────────────────────────────────────
test('initial state is ReportsInitial', () {
expect(bloc.state, isA<ReportsInitial>());
});
// ── LoadDashboardReports ──────────────────────────────────────────────────
group('LoadDashboardReports', () {
void stubDashboardSuccess() {
when(mockRepository.getPerformanceGlobale())
.thenAnswer((_) async => fakePerformance());
when(mockRepository.getStatistiquesMembres())
.thenAnswer((_) async => fakeStatsMembres());
when(mockRepository.getStatistiquesCotisations(any))
.thenAnswer((_) async => fakeStatsCotisations());
when(mockRepository.getStatistiquesEvenements())
.thenAnswer((_) async => fakeStatsEvenements());
}
blocTest<ReportsBloc, ReportsState>(
'emits [ReportsLoading, ReportsDashboardLoaded] on success',
build: () {
stubDashboardSuccess();
return bloc;
},
act: (b) => b.add(const LoadDashboardReports()),
expect: () => [
isA<ReportsLoading>(),
isA<ReportsDashboardLoaded>()
.having(
(s) => s.performance['totalMembres'],
'performance.totalMembres',
150,
)
.having(
(s) => s.statsMembres['membresActifs'],
'statsMembres.membresActifs',
120,
)
.having(
(s) => s.statsCotisations['totalCotisations'],
'statsCotisations.totalCotisations',
200,
)
.having(
(s) => s.statsEvenements['totalEvenements'],
'statsEvenements.totalEvenements',
12,
),
],
verify: (_) {
verify(mockRepository.getPerformanceGlobale()).called(1);
verify(mockRepository.getStatistiquesMembres()).called(1);
verify(mockRepository.getStatistiquesCotisations(any)).called(1);
verify(mockRepository.getStatistiquesEvenements()).called(1);
},
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportsLoading, ReportsError] when performance call fails',
build: () {
when(mockRepository.getPerformanceGlobale())
.thenThrow(Exception('network error'));
when(mockRepository.getStatistiquesMembres())
.thenAnswer((_) async => fakeStatsMembres());
when(mockRepository.getStatistiquesCotisations(any))
.thenAnswer((_) async => fakeStatsCotisations());
when(mockRepository.getStatistiquesEvenements())
.thenAnswer((_) async => fakeStatsEvenements());
return bloc;
},
act: (b) => b.add(const LoadDashboardReports()),
expect: () => [
isA<ReportsLoading>(),
isA<ReportsError>().having(
(s) => s.message,
'message',
contains('Erreur'),
),
],
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportsLoading, ReportsError] when stats membres call fails',
build: () {
when(mockRepository.getPerformanceGlobale())
.thenAnswer((_) async => fakePerformance());
when(mockRepository.getStatistiquesMembres())
.thenThrow(Exception('membres error'));
when(mockRepository.getStatistiquesCotisations(any))
.thenAnswer((_) async => fakeStatsCotisations());
when(mockRepository.getStatistiquesEvenements())
.thenAnswer((_) async => fakeStatsEvenements());
return bloc;
},
act: (b) => b.add(const LoadDashboardReports()),
expect: () => [isA<ReportsLoading>(), isA<ReportsError>()],
);
blocTest<ReportsBloc, ReportsState>(
'passes current year to getStatistiquesCotisations',
build: () {
stubDashboardSuccess();
return bloc;
},
act: (b) => b.add(const LoadDashboardReports()),
verify: (_) {
final captured = verify(
mockRepository.getStatistiquesCotisations(captureAny),
).captured;
expect(captured.first, equals(DateTime.now().year));
},
);
});
// ── ScheduleReportRequested ───────────────────────────────────────────────
group('ScheduleReportRequested', () {
blocTest<ReportsBloc, ReportsState>(
'emits [ReportScheduled] on schedule success',
build: () {
when(mockScheduleReport(cronExpression: anyNamed('cronExpression')))
.thenAnswer((_) async {});
return bloc;
},
act: (b) => b.add(const ScheduleReportRequested(cronExpression: '0 0 1 * *')),
expect: () => [isA<ReportScheduled>()],
verify: (_) {
verify(mockScheduleReport(cronExpression: '0 0 1 * *')).called(1);
},
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportScheduled] on schedule without cron expression',
build: () {
when(mockScheduleReport(cronExpression: anyNamed('cronExpression')))
.thenAnswer((_) async {});
return bloc;
},
act: (b) => b.add(const ScheduleReportRequested()),
expect: () => [isA<ReportScheduled>()],
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportsError] on schedule failure',
build: () {
when(mockScheduleReport(cronExpression: anyNamed('cronExpression')))
.thenThrow(Exception('schedule error'));
return bloc;
},
act: (b) => b.add(const ScheduleReportRequested()),
expect: () => [
isA<ReportsError>().having(
(s) => s.message,
'message',
contains('Impossible de programmer'),
),
],
);
});
// ── GenerateReportRequested ───────────────────────────────────────────────
group('GenerateReportRequested', () {
blocTest<ReportsBloc, ReportsState>(
'emits [ReportGenerated] on generate success without format',
build: () {
when(mockGenerateReport('membres', format: anyNamed('format')))
.thenAnswer((_) async {});
return bloc;
},
act: (b) => b.add(const GenerateReportRequested('membres')),
expect: () => [
isA<ReportGenerated>().having(
(s) => s.type,
'type',
'membres',
),
],
verify: (_) {
verify(mockGenerateReport('membres', format: null)).called(1);
},
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportGenerated] on generate success with pdf format',
build: () {
when(mockGenerateReport('cotisations', format: 'pdf'))
.thenAnswer((_) async {});
return bloc;
},
act: (b) => b.add(const GenerateReportRequested('cotisations', format: 'pdf')),
expect: () => [
isA<ReportGenerated>().having((s) => s.type, 'type', 'cotisations'),
],
verify: (_) {
verify(mockGenerateReport('cotisations', format: 'pdf')).called(1);
},
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportsError] on generate failure',
build: () {
when(mockGenerateReport(any, format: anyNamed('format')))
.thenThrow(Exception('generate error'));
return bloc;
},
act: (b) => b.add(const GenerateReportRequested('evenements')),
expect: () => [
isA<ReportsError>().having(
(s) => s.message,
'message',
contains('Impossible de générer'),
),
],
);
blocTest<ReportsBloc, ReportsState>(
'emits [ReportsError] for excel format failure',
build: () {
when(mockGenerateReport(any, format: 'excel')).thenThrow(Exception('excel error'));
return bloc;
},
act: (b) => b.add(const GenerateReportRequested('finance', format: 'excel')),
expect: () => [isA<ReportsError>()],
);
});
}