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:
294
test/features/dashboard/bloc/finance_bloc_test.dart
Normal file
294
test/features/dashboard/bloc/finance_bloc_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user