## 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>
270 lines
9.8 KiB
Dart
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>()],
|
|
);
|
|
});
|
|
}
|