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>
This commit is contained in:
dahoud
2026-04-21 12:42:35 +00:00
parent 33f5b5a707
commit 37db88672b
142 changed files with 27599 additions and 16068 deletions

View File

@@ -0,0 +1,294 @@
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/dashboard/data/repositories/finance_repository.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_bloc.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_event.dart';
import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_state.dart';
@GenerateMocks([FinanceRepository])
import 'finance_bloc_test.mocks.dart';
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
const _summary = FinanceSummary(
totalContributionsPaid: 150000,
totalContributionsPending: 25000,
epargneBalance: 300000,
);
final _transactions = [
const FinanceTransaction(
id: 'tx-1',
title: 'Cotisation Janvier',
date: '01/01/2026',
amount: 5000,
status: 'En attente',
),
const FinanceTransaction(
id: 'tx-2',
title: 'Cotisation Février',
date: '01/02/2026',
amount: 5000,
status: 'Payé',
),
];
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
late MockFinanceRepository mockRepository;
setUp(() {
mockRepository = MockFinanceRepository();
});
FinanceBloc buildBloc() => FinanceBloc(mockRepository);
// ─── initial state ───────────────────────────────────────────────────────
test('initial state is FinanceInitial', () {
final bloc = buildBloc();
expect(bloc.state, isA<FinanceInitial>());
bloc.close();
});
// ─── LoadFinanceRequested ────────────────────────────────────────────────
group('LoadFinanceRequested', () {
blocTest<FinanceBloc, FinanceState>(
'emits [FinanceLoading, FinanceLoaded] on success',
build: () {
when(mockRepository.getFinancialSummary())
.thenAnswer((_) async => _summary);
when(mockRepository.getTransactions())
.thenAnswer((_) async => _transactions);
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
expect: () => [
isA<FinanceLoading>(),
isA<FinanceLoaded>(),
],
verify: (_) {
verify(mockRepository.getFinancialSummary()).called(1);
verify(mockRepository.getTransactions()).called(1);
},
);
blocTest<FinanceBloc, FinanceState>(
'FinanceLoaded contains correct summary and transactions',
build: () {
when(mockRepository.getFinancialSummary())
.thenAnswer((_) async => _summary);
when(mockRepository.getTransactions())
.thenAnswer((_) async => _transactions);
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
expect: () => [
isA<FinanceLoading>(),
FinanceLoaded(summary: _summary, transactions: _transactions),
],
);
blocTest<FinanceBloc, FinanceState>(
'emits [FinanceLoading, FinanceLoaded] with empty transactions list',
build: () {
when(mockRepository.getFinancialSummary())
.thenAnswer((_) async => _summary);
when(mockRepository.getTransactions())
.thenAnswer((_) async => []);
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
expect: () => [
isA<FinanceLoading>(),
const FinanceLoaded(summary: _summary, transactions: []),
],
);
blocTest<FinanceBloc, FinanceState>(
'emits [FinanceLoading, FinanceError] when getFinancialSummary throws',
build: () {
when(mockRepository.getFinancialSummary())
.thenThrow(Exception('network error'));
when(mockRepository.getTransactions())
.thenAnswer((_) async => []);
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
expect: () => [
isA<FinanceLoading>(),
isA<FinanceError>(),
],
);
blocTest<FinanceBloc, FinanceState>(
'emits [FinanceLoading, FinanceError] when getTransactions throws',
build: () {
when(mockRepository.getFinancialSummary())
.thenAnswer((_) async => _summary);
when(mockRepository.getTransactions())
.thenThrow(Exception('transactions error'));
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
expect: () => [
isA<FinanceLoading>(),
isA<FinanceError>(),
],
);
blocTest<FinanceBloc, FinanceState>(
'FinanceError message contains error information',
build: () {
when(mockRepository.getFinancialSummary())
.thenThrow(Exception('server down'));
when(mockRepository.getTransactions())
.thenAnswer((_) async => []);
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
expect: () => [
isA<FinanceLoading>(),
predicate<FinanceState>(
(s) => s is FinanceError && s.message.contains('Erreur chargement'),
'FinanceError with expected prefix',
),
],
);
blocTest<FinanceBloc, FinanceState>(
'does NOT emit error when DioException type is cancel',
build: () {
final dioException = DioException(
requestOptions: RequestOptions(path: '/test'),
type: DioExceptionType.cancel,
);
when(mockRepository.getFinancialSummary())
.thenThrow(dioException);
when(mockRepository.getTransactions())
.thenAnswer((_) async => []);
return buildBloc();
},
act: (b) => b.add(LoadFinanceRequested()),
// Only FinanceLoading emitted; the cancel exception is swallowed
expect: () => [isA<FinanceLoading>()],
);
blocTest<FinanceBloc, FinanceState>(
'handles multiple consecutive LoadFinanceRequested events',
build: () {
when(mockRepository.getFinancialSummary())
.thenAnswer((_) async => _summary);
when(mockRepository.getTransactions())
.thenAnswer((_) async => _transactions);
return buildBloc();
},
act: (b) async {
b.add(LoadFinanceRequested());
// Wait for the first load to complete before firing the second
await b.stream.firstWhere((s) => s is FinanceLoaded);
b.add(LoadFinanceRequested());
},
expect: () => [
isA<FinanceLoading>(),
isA<FinanceLoaded>(),
isA<FinanceLoading>(),
isA<FinanceLoaded>(),
],
);
});
// ─── FinancePaymentInitiated ─────────────────────────────────────────────
group('FinancePaymentInitiated', () {
blocTest<FinanceBloc, FinanceState>(
'does not change state when current state is FinanceInitial',
build: buildBloc,
act: (b) => b.add(const FinancePaymentInitiated('contrib-1')),
expect: () => <FinanceState>[],
);
blocTest<FinanceBloc, FinanceState>(
'does not change state when current state is FinanceLoaded (no-op for now)',
build: () {
// No repository calls needed — the handler is a no-op when loaded
return buildBloc();
},
seed: () =>
FinanceLoaded(summary: _summary, transactions: _transactions),
act: (b) =>
b.add(const FinancePaymentInitiated('contrib-42')),
// The bloc intentionally keeps FinanceLoaded unchanged
expect: () => <FinanceState>[],
);
blocTest<FinanceBloc, FinanceState>(
'does not call any repository method',
build: buildBloc,
act: (b) => b.add(const FinancePaymentInitiated('contrib-1')),
verify: (_) {
verifyZeroInteractions(mockRepository);
},
);
});
// ─── State equality ──────────────────────────────────────────────────────
group('State equality', () {
test('FinanceSummary equality works correctly', () {
const s1 = FinanceSummary(
totalContributionsPaid: 100,
totalContributionsPending: 50,
epargneBalance: 200,
);
const s2 = FinanceSummary(
totalContributionsPaid: 100,
totalContributionsPending: 50,
epargneBalance: 200,
);
expect(s1, equals(s2));
});
test('FinanceTransaction equality works correctly', () {
const tx1 = FinanceTransaction(
id: 'tx-1',
title: 'Title',
date: '01/01/2026',
amount: 5000,
status: 'Payé',
);
const tx2 = FinanceTransaction(
id: 'tx-1',
title: 'Title',
date: '01/01/2026',
amount: 5000,
status: 'Payé',
);
expect(tx1, equals(tx2));
});
test('FinanceLoaded equality works correctly', () {
final l1 =
FinanceLoaded(summary: _summary, transactions: _transactions);
final l2 =
FinanceLoaded(summary: _summary, transactions: _transactions);
expect(l1, equals(l2));
});
test('FinanceError equality works correctly', () {
const e1 = FinanceError('some error');
const e2 = FinanceError('some error');
expect(e1, equals(e2));
});
});
}