## 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>
295 lines
9.6 KiB
Dart
295 lines
9.6 KiB
Dart
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));
|
|
});
|
|
});
|
|
}
|