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,716 @@
/// Tests unitaires pour ContributionsBloc
library contributions_bloc_test;
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/contributions/bloc/contributions_bloc.dart';
import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_event.dart';
import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_state.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contributions.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_by_id.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/create_contribution.dart'
as uc;
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/update_contribution.dart'
as uc;
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/delete_contribution.dart'
as uc;
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/pay_contribution.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_stats.dart';
@GenerateMocks([
GetContributions,
GetContributionById,
uc.CreateContribution,
uc.UpdateContribution,
uc.DeleteContribution,
PayContribution,
GetContributionStats,
IContributionRepository,
])
import 'contributions_bloc_test.mocks.dart';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
ContributionModel _makeContribution({
String id = 'c1',
ContributionStatus statut = ContributionStatus.payee,
}) =>
ContributionModel(
id: id,
membreId: 'membre1',
montant: 5000,
dateEcheance: DateTime(2025, 12, 31),
annee: 2025,
statut: statut,
);
ContributionPageResult _makePageResult(List<ContributionModel> items) =>
ContributionPageResult(
contributions: items,
total: items.length,
page: 0,
size: 20,
totalPages: items.isEmpty ? 0 : 1,
);
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
late MockGetContributions mockGetContributions;
late MockGetContributionById mockGetContributionById;
late MockCreateContribution mockCreateContribution;
late MockUpdateContribution mockUpdateContribution;
late MockDeleteContribution mockDeleteContribution;
late MockPayContribution mockPayContribution;
late MockGetContributionStats mockGetContributionStats;
late MockIContributionRepository mockRepository;
ContributionsBloc buildBloc() => ContributionsBloc(
mockGetContributions,
mockGetContributionById,
mockCreateContribution,
mockUpdateContribution,
mockDeleteContribution,
mockPayContribution,
mockGetContributionStats,
mockRepository,
);
setUp(() {
mockGetContributions = MockGetContributions();
mockGetContributionById = MockGetContributionById();
mockCreateContribution = MockCreateContribution();
mockUpdateContribution = MockUpdateContribution();
mockDeleteContribution = MockDeleteContribution();
mockPayContribution = MockPayContribution();
mockGetContributionStats = MockGetContributionStats();
mockRepository = MockIContributionRepository();
});
// ---- initial state -------------------------------------------------------
test('initial state is ContributionsInitial', () {
final bloc = buildBloc();
expect(bloc.state, isA<ContributionsInitial>());
bloc.close();
});
// ---- LoadContributions ---------------------------------------------------
group('LoadContributions', () {
final contribution = _makeContribution();
final pageResult = _makePageResult([contribution]);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] on success',
build: () {
when(mockGetContributions(
page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => pageResult);
return buildBloc();
},
act: (b) => b.add(const LoadContributions()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>()
.having((s) => s.contributions.length, 'count', 1)
.having((s) => s.total, 'total', 1),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] with empty list',
build: () {
when(mockGetContributions(
page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => _makePageResult([]));
return buildBloc();
},
act: (b) => b.add(const LoadContributions()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>()
.having((s) => s.contributions, 'contributions', isEmpty),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on exception',
build: () {
when(mockGetContributions(
page: anyNamed('page'), size: anyNamed('size')))
.thenThrow(Exception('network'));
return buildBloc();
},
act: (b) => b.add(const LoadContributions()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'respects custom page and size parameters',
build: () {
final bigResult = _makePageResult(
List.generate(5, (i) => _makeContribution(id: 'c$i')));
when(mockGetContributions(page: 2, size: 5))
.thenAnswer((_) async => bigResult);
return buildBloc();
},
act: (b) => b.add(const LoadContributions(page: 2, size: 5)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>()
.having((s) => s.contributions.length, 'count', 5),
],
);
});
// ---- LoadContributionById ------------------------------------------------
group('LoadContributionById', () {
final contribution = _makeContribution();
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, DetailLoaded] on success',
build: () {
when(mockGetContributionById.call(any))
.thenAnswer((_) async => contribution);
return buildBloc();
},
act: (b) => b.add(const LoadContributionById(id: 'c1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionDetailLoaded>()
.having((s) => s.contribution.id, 'id', 'c1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] when not found',
build: () {
when(mockGetContributionById.call(any))
.thenThrow(Exception('not found'));
return buildBloc();
},
act: (b) => b.add(const LoadContributionById(id: 'missing')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>()
.having((s) => s.message, 'message', contains('Contribution non trouvée')),
],
);
});
// ---- CreateContribution --------------------------------------------------
group('CreateContribution', () {
final newContribution = _makeContribution(id: 'new1');
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, ContributionCreated] on success',
build: () {
when(mockCreateContribution.call(any))
.thenAnswer((_) async => newContribution);
return buildBloc();
},
act: (b) => b.add(CreateContribution(contribution: newContribution)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionCreated>()
.having((s) => s.contribution.id, 'id', 'new1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockCreateContribution.call(any))
.thenThrow(Exception('validation error'));
return buildBloc();
},
act: (b) => b.add(CreateContribution(contribution: newContribution)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- UpdateContribution --------------------------------------------------
group('UpdateContribution', () {
final updatedContribution = _makeContribution(id: 'c1');
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, ContributionUpdated] on success',
build: () {
when(mockUpdateContribution.call(any, any))
.thenAnswer((_) async => updatedContribution);
return buildBloc();
},
act: (b) => b.add(UpdateContribution(
id: 'c1', contribution: updatedContribution)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionUpdated>()
.having((s) => s.contribution.id, 'id', 'c1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockUpdateContribution.call(any, any))
.thenThrow(Exception('update failed'));
return buildBloc();
},
act: (b) => b.add(UpdateContribution(
id: 'c1', contribution: updatedContribution)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- DeleteContribution --------------------------------------------------
group('DeleteContribution', () {
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, ContributionDeleted] on success',
build: () {
when(mockDeleteContribution.call(any)).thenAnswer((_) async => null);
return buildBloc();
},
act: (b) => b.add(const DeleteContribution(id: 'c1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionDeleted>()
.having((s) => s.id, 'id', 'c1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockDeleteContribution.call(any))
.thenThrow(Exception('delete failed'));
return buildBloc();
},
act: (b) => b.add(const DeleteContribution(id: 'c1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- SearchContributions -------------------------------------------------
group('SearchContributions', () {
final results = [_makeContribution(id: 'sr1')];
final pageResult = _makePageResult(results);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] on success with filters',
build: () {
when(mockRepository.getCotisations(
page: anyNamed('page'),
size: anyNamed('size'),
membreId: anyNamed('membreId'),
statut: anyNamed('statut'),
type: anyNamed('type'),
annee: anyNamed('annee'),
)).thenAnswer((_) async => pageResult);
return buildBloc();
},
act: (b) => b.add(const SearchContributions(
membreId: 'membre1',
statut: ContributionStatus.payee,
annee: 2025,
)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>(),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.getCotisations(
page: anyNamed('page'),
size: anyNamed('size'),
membreId: anyNamed('membreId'),
statut: anyNamed('statut'),
type: anyNamed('type'),
annee: anyNamed('annee'),
)).thenThrow(Exception('search failed'));
return buildBloc();
},
act: (b) => b.add(const SearchContributions()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- LoadContributionsByMembre -------------------------------------------
group('LoadContributionsByMembre', () {
final pageResult = _makePageResult([_makeContribution()]);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] on success',
build: () {
when(mockRepository.getCotisations(
page: anyNamed('page'),
size: anyNamed('size'),
membreId: anyNamed('membreId'),
statut: anyNamed('statut'),
type: anyNamed('type'),
annee: anyNamed('annee'),
)).thenAnswer((_) async => pageResult);
return buildBloc();
},
act: (b) => b.add(const LoadContributionsByMembre(membreId: 'membre1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>(),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.getCotisations(
page: anyNamed('page'),
size: anyNamed('size'),
membreId: anyNamed('membreId'),
statut: anyNamed('statut'),
type: anyNamed('type'),
annee: anyNamed('annee'),
)).thenThrow(Exception('load failed'));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsByMembre(membreId: 'membre1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- LoadContributionsPayees ---------------------------------------------
group('LoadContributionsPayees', () {
final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee);
final nonPayee =
_makeContribution(id: 'np1', statut: ContributionStatus.nonPayee);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] with only paid contributions',
build: () {
when(mockRepository.getMesCotisations())
.thenAnswer((_) async => _makePageResult([payee, nonPayee]));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsPayees()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>()
.having((s) => s.contributions.length, 'count', 1)
.having((s) => s.contributions.first.id, 'id', 'p1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.getMesCotisations())
.thenThrow(Exception('server error'));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsPayees()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- LoadContributionsNonPayees ------------------------------------------
group('LoadContributionsNonPayees', () {
final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee);
final nonPayee =
_makeContribution(id: 'np1', statut: ContributionStatus.nonPayee);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] with only unpaid contributions',
build: () {
when(mockRepository.getMesCotisations())
.thenAnswer((_) async => _makePageResult([payee, nonPayee]));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsNonPayees()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>()
.having((s) => s.contributions.length, 'count', 1)
.having((s) => s.contributions.first.id, 'id', 'np1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.getMesCotisations())
.thenThrow(Exception('server error'));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsNonPayees()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- LoadContributionsEnRetard -------------------------------------------
group('LoadContributionsEnRetard', () {
final enRetard =
_makeContribution(id: 'r1', statut: ContributionStatus.enRetard);
final payee = _makeContribution(id: 'p1', statut: ContributionStatus.payee);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Loaded] with only late contributions',
build: () {
when(mockRepository.getMesCotisations())
.thenAnswer((_) async => _makePageResult([enRetard, payee]));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsEnRetard()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsLoaded>()
.having((s) => s.contributions.length, 'count', 1)
.having((s) => s.contributions.first.id, 'id', 'r1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.getMesCotisations())
.thenThrow(Exception('server error'));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsEnRetard()),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- RecordPayment -------------------------------------------------------
group('RecordPayment', () {
final paid = _makeContribution(statut: ContributionStatus.payee);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, PaymentRecorded] on success',
build: () {
when(mockPayContribution.call(
cotisationId: anyNamed('cotisationId'),
montant: anyNamed('montant'),
datePaiement: anyNamed('datePaiement'),
methodePaiement: anyNamed('methodePaiement'),
numeroPaiement: anyNamed('numeroPaiement'),
referencePaiement: anyNamed('referencePaiement'),
)).thenAnswer((_) async => paid);
return buildBloc();
},
act: (b) => b.add(RecordPayment(
contributionId: 'c1',
montant: 5000,
methodePaiement: PaymentMethod.especes,
datePaiement: DateTime(2025, 6, 1),
)),
expect: () => [
isA<ContributionsLoading>(),
isA<PaymentRecorded>(),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockPayContribution.call(
cotisationId: anyNamed('cotisationId'),
montant: anyNamed('montant'),
datePaiement: anyNamed('datePaiement'),
methodePaiement: anyNamed('methodePaiement'),
numeroPaiement: anyNamed('numeroPaiement'),
referencePaiement: anyNamed('referencePaiement'),
)).thenThrow(Exception('payment failed'));
return buildBloc();
},
act: (b) => b.add(RecordPayment(
contributionId: 'c1',
montant: 5000,
methodePaiement: PaymentMethod.waveMoney,
datePaiement: DateTime(2025, 6, 1),
)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- LoadContributionsStats ----------------------------------------------
group('LoadContributionsStats', () {
final synthese = {
'montantDu': 10000.0,
'totalPayeAnnee': 5000.0,
'cotisationsEnAttente': 2,
'prochaineEcheance': '2025-07-31',
'anneeEnCours': 2025,
};
blocTest<ContributionsBloc, ContributionsState>(
'emits ContributionsStatsLoaded using synthese when non-null',
build: () {
when(mockGetContributionStats.call()).thenAnswer((_) async => synthese);
// getContributions called when no preserved list
when(mockGetContributions(
page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => _makePageResult([]));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsStats()),
expect: () => [
isA<ContributionsStatsLoaded>()
.having((s) => s.stats['isMesSynthese'], 'isMesSynthese', true),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'falls back to repository.getStatistiques when synthese is null',
build: () {
when(mockGetContributionStats.call()).thenAnswer((_) async => null);
when(mockGetContributions(
page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => _makePageResult([]));
when(mockRepository.getStatistiques())
.thenAnswer((_) async => {'totalCotisations': 10});
return buildBloc();
},
act: (b) => b.add(const LoadContributionsStats()),
expect: () => [
isA<ContributionsStatsLoaded>(),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits ContributionsError on failure',
build: () {
when(mockGetContributionStats.call())
.thenThrow(Exception('stats failed'));
return buildBloc();
},
act: (b) => b.add(const LoadContributionsStats()),
expect: () => [isA<ContributionsError>()],
);
});
// ---- GenerateAnnualContributions -----------------------------------------
group('GenerateAnnualContributions', () {
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, ContributionsGenerated] on success',
build: () {
when(mockRepository.genererCotisationsAnnuelles(any))
.thenAnswer((_) async => 42);
return buildBloc();
},
act: (b) => b.add(GenerateAnnualContributions(
annee: 2025,
montant: 10000,
dateEcheance: DateTime(2025, 12, 31),
)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsGenerated>()
.having((s) => s.nombreGenere, 'nombreGenere', 42),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.genererCotisationsAnnuelles(any))
.thenThrow(Exception('generate failed'));
return buildBloc();
},
act: (b) => b.add(GenerateAnnualContributions(
annee: 2025,
montant: 10000,
dateEcheance: DateTime(2025, 12, 31),
)),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
// ---- SendPaymentReminder -------------------------------------------------
group('SendPaymentReminder', () {
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, ReminderSent] on success',
build: () {
when(mockRepository.envoyerRappel(any)).thenAnswer((_) async => null);
return buildBloc();
},
act: (b) => b.add(const SendPaymentReminder(contributionId: 'c1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ReminderSent>()
.having((s) => s.contributionId, 'contributionId', 'c1'),
],
);
blocTest<ContributionsBloc, ContributionsState>(
'emits [Loading, Error] on failure',
build: () {
when(mockRepository.envoyerRappel(any))
.thenThrow(Exception('reminder failed'));
return buildBloc();
},
act: (b) => b.add(const SendPaymentReminder(contributionId: 'c1')),
expect: () => [
isA<ContributionsLoading>(),
isA<ContributionsError>(),
],
);
});
}