Files
unionflow-mobile-apps/test/features/organizations/bloc/organizations_bloc_test.dart
dahoud 37db88672b 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>
2026-04-21 12:42:35 +00:00

929 lines
34 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/organizations/bloc/organizations_bloc.dart';
import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_event.dart';
import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_state.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/services/organization_service.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/create_organization.dart'
as uc;
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/delete_organization.dart'
as uc;
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_by_id.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organizations.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization.dart'
as uc;
@GenerateMocks([
GetOrganizations,
GetOrganizationById,
uc.CreateOrganization,
uc.UpdateOrganization,
uc.DeleteOrganization,
IOrganizationRepository,
OrganizationService,
])
import 'organizations_bloc_test.mocks.dart';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
OrganizationModel _makeOrg({
String id = 'org-1',
String nom = 'Association Test',
StatutOrganization statut = StatutOrganization.active,
String type = 'ASSOCIATION',
int nombreMembres = 10,
}) =>
OrganizationModel(
id: id,
nom: nom,
typeOrganisation: type,
statut: statut,
nombreMembres: nombreMembres,
);
// ---------------------------------------------------------------------------
void main() {
late MockGetOrganizations mockGetOrganizations;
late MockGetOrganizationById mockGetOrganizationById;
late MockCreateOrganization mockCreateOrganization;
late MockUpdateOrganization mockUpdateOrganization;
late MockDeleteOrganization mockDeleteOrganization;
late MockIOrganizationRepository mockRepository;
late MockOrganizationService mockOrganizationService;
OrganizationsBloc buildBloc() => OrganizationsBloc(
mockGetOrganizations,
mockGetOrganizationById,
mockCreateOrganization,
mockUpdateOrganization,
mockDeleteOrganization,
mockRepository,
mockOrganizationService,
);
setUp(() {
mockGetOrganizations = MockGetOrganizations();
mockGetOrganizationById = MockGetOrganizationById();
mockCreateOrganization = MockCreateOrganization();
mockUpdateOrganization = MockUpdateOrganization();
mockDeleteOrganization = MockDeleteOrganization();
mockRepository = MockIOrganizationRepository();
mockOrganizationService = MockOrganizationService();
// Default stubs for searchLocal / filterByStatus / filterByType
when(mockOrganizationService.searchLocal(any, any)).thenAnswer(
(invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
);
when(mockOrganizationService.filterByStatus(any, any)).thenAnswer(
(invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
);
when(mockOrganizationService.filterByType(any, any)).thenAnswer(
(invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
);
when(mockOrganizationService.sortByName(any, ascending: anyNamed('ascending'))).thenAnswer(
(invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
);
when(mockOrganizationService.sortByCreationDate(any, ascending: anyNamed('ascending'))).thenAnswer(
(invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
);
when(mockOrganizationService.sortByMemberCount(any, ascending: anyNamed('ascending'))).thenAnswer(
(invocation) => invocation.positionalArguments[0] as List<OrganizationModel>,
);
});
// ─── Initial state ────────────────────────────────────────────────────────
group('initial state', () {
test('is OrganizationsInitial', () {
expect(buildBloc().state, const OrganizationsInitial());
});
});
// ─── LoadOrganizations ────────────────────────────────────────────────────
group('LoadOrganizations', () {
final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'Mutuelle B')];
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Loading, Loaded] on success',
build: buildBloc,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => orgs),
act: (b) => b.add(const LoadOrganizations()),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>()
.having((s) => s.organizations, 'organizations', orgs)
.having((s) => s.currentPage, 'page', 0),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Loading, Error] on exception',
build: buildBloc,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(Exception('network error')),
act: (b) => b.add(const LoadOrganizations()),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsError>()
.having((e) => e.message, 'message', contains('chargement des organisations')),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'uses useMesOnly endpoint when flag is set',
build: buildBloc,
setUp: () =>
when(mockRepository.getMesOrganisations()).thenAnswer((_) async => orgs),
act: (b) => b.add(const LoadOrganizations(useMesOnly: true)),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>().having((s) => s.useMesOnly, 'useMesOnly', true),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'filters by filterOrganizationIds when provided',
build: buildBloc,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => orgs),
act: (b) => b.add(const LoadOrganizations(filterOrganizationIds: ['org-1'])),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>()
.having((s) => s.organizations.length, 'count', 1)
.having((s) => s.organizations.first.id, 'id', 'org-1'),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'does NOT emit Loading when state is already Loaded and refresh=false',
build: buildBloc,
seed: () => OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
),
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => orgs),
act: (b) => b.add(const LoadOrganizations()),
expect: () => [
isA<OrganizationsLoaded>(),
],
);
});
// ─── LoadMoreOrganizations ────────────────────────────────────────────────
group('LoadMoreOrganizations', () {
final initialOrgs = [_makeOrg()];
final moreOrgs = [_makeOrg(id: 'org-2', nom: 'Org B')];
final loadedState = OrganizationsLoaded(
organizations: initialOrgs,
filteredOrganizations: initialOrgs,
hasReachedMax: false,
currentPage: 0,
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [LoadingMore, Loaded] with appended orgs',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => moreOrgs),
act: (b) => b.add(const LoadMoreOrganizations()),
expect: () => [
isA<OrganizationsLoadingMore>(),
isA<OrganizationsLoaded>()
.having((s) => s.organizations.length, 'total', 2)
.having((s) => s.currentPage, 'page', 1),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'does nothing when hasReachedMax is true',
build: buildBloc,
seed: () => OrganizationsLoaded(
organizations: initialOrgs,
filteredOrganizations: initialOrgs,
hasReachedMax: true,
),
act: (b) => b.add(const LoadMoreOrganizations()),
expect: () => [],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'does nothing when state is not OrganizationsLoaded',
build: buildBloc,
act: (b) => b.add(const LoadMoreOrganizations()),
expect: () => [],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'sets hasReachedMax when fewer than 20 items returned',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => moreOrgs),
act: (b) => b.add(const LoadMoreOrganizations()),
expect: () => [
isA<OrganizationsLoadingMore>(),
isA<OrganizationsLoaded>().having((s) => s.hasReachedMax, 'hasReachedMax', true),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits error with previousOrganizations on failure',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(Exception('network')),
act: (b) => b.add(const LoadMoreOrganizations()),
expect: () => [
isA<OrganizationsLoadingMore>(),
isA<OrganizationsError>()
.having((e) => e.previousOrganizations, 'previousOrgs', isNotNull),
],
);
});
// ─── SearchOrganizations ──────────────────────────────────────────────────
group('SearchOrganizations', () {
final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'TONTINE Test')];
final loadedState = OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
);
blocTest<OrganizationsBloc, OrganizationsState>(
'clears search when query is empty (with existing search)',
build: buildBloc,
seed: () => OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
currentSearch: 'previous query',
),
act: (b) => b.add(const SearchOrganizations('')),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.currentSearch, 'currentSearch', isNull),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'performs local search then server search for non-empty query',
build: buildBloc,
seed: () => loadedState,
setUp: () {
when(mockOrganizationService.searchLocal(any, any))
.thenReturn([orgs.first]);
when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => [orgs.first]);
},
act: (b) => b.add(const SearchOrganizations('Association')),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.currentSearch, 'search', 'Association'),
isA<OrganizationsLoaded>()
.having((s) => s.hasReachedMax, 'hasReachedMax', true),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'dispatches LoadOrganizations when not in loaded state',
build: buildBloc,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => orgs),
act: (b) => b.add(const SearchOrganizations('test')),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>(),
],
);
});
// ─── AdvancedSearchOrganizations ──────────────────────────────────────────
group('AdvancedSearchOrganizations', () {
final orgs = [_makeOrg()];
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Loading, Loaded] on success',
build: buildBloc,
setUp: () => when(mockRepository.searchOrganizations(
nom: anyNamed('nom'),
type: anyNamed('type'),
statut: anyNamed('statut'),
ville: anyNamed('ville'),
region: anyNamed('region'),
pays: anyNamed('pays'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenAnswer((_) async => orgs),
act: (b) => b.add(const AdvancedSearchOrganizations(nom: 'Test')),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>(),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Loading, Error] on failure',
build: buildBloc,
setUp: () => when(mockRepository.searchOrganizations(
nom: anyNamed('nom'),
type: anyNamed('type'),
statut: anyNamed('statut'),
ville: anyNamed('ville'),
region: anyNamed('region'),
pays: anyNamed('pays'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenThrow(Exception('server error')),
act: (b) => b.add(const AdvancedSearchOrganizations(nom: 'Test')),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsError>(),
],
);
});
// ─── LoadOrganizationById ─────────────────────────────────────────────────
group('LoadOrganizationById', () {
final org = _makeOrg();
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [OrganizationLoading, OrganizationLoaded] on success',
build: buildBloc,
setUp: () =>
when(mockGetOrganizationById('org-1')).thenAnswer((_) async => org),
act: (b) => b.add(const LoadOrganizationById('org-1')),
expect: () => [
const OrganizationLoading('org-1'),
OrganizationLoaded(org),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [OrganizationLoading, OrganizationError] when not found (null)',
build: buildBloc,
setUp: () =>
when(mockGetOrganizationById('missing')).thenAnswer((_) async => null),
act: (b) => b.add(const LoadOrganizationById('missing')),
expect: () => [
const OrganizationLoading('missing'),
isA<OrganizationError>().having((e) => e.organizationId, 'id', 'missing'),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [OrganizationLoading, OrganizationError] on exception',
build: buildBloc,
setUp: () =>
when(mockGetOrganizationById('org-1')).thenThrow(Exception('fail')),
act: (b) => b.add(const LoadOrganizationById('org-1')),
expect: () => [
const OrganizationLoading('org-1'),
isA<OrganizationError>(),
],
);
});
// ─── CreateOrganization ───────────────────────────────────────────────────
group('CreateOrganization', () {
final newOrg = _makeOrg(id: 'new-1', nom: 'New Org');
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Creating, Created] then triggers refresh on success',
build: buildBloc,
setUp: () {
when(mockCreateOrganization(any)).thenAnswer((_) async => newOrg);
when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => [newOrg]);
},
act: (b) => b.add(CreateOrganization(newOrg)),
expect: () => [
const OrganizationCreating(),
OrganizationCreated(newOrg),
const OrganizationsLoading(),
isA<OrganizationsLoaded>(),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Creating, Error] on failure',
build: buildBloc,
setUp: () =>
when(mockCreateOrganization(any)).thenThrow(Exception('conflict')),
act: (b) => b.add(CreateOrganization(newOrg)),
expect: () => [
const OrganizationCreating(),
isA<OrganizationsError>()
.having((e) => e.message, 'message', contains('création')),
],
);
});
// ─── UpdateOrganization ───────────────────────────────────────────────────
group('UpdateOrganization', () {
final org = _makeOrg();
final updated = _makeOrg(nom: 'Updated');
final loadedState = OrganizationsLoaded(
organizations: [org],
filteredOrganizations: [org],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Updating, Updated] on success (previousState captured after first emit)',
build: buildBloc,
seed: () => loadedState,
setUp: () =>
when(mockUpdateOrganization('org-1', any)).thenAnswer((_) async => updated),
act: (b) => b.add(UpdateOrganization('org-1', updated)),
expect: () => [
const OrganizationUpdating('org-1'),
OrganizationUpdated(updated),
// Note: the BLoC captures previousState AFTER the first emit, so
// previousState is OrganizationUpdating, not OrganizationsLoaded.
// The list-update branch is only reached when starting from a loaded state
// without any preceding emit — the inline emit order prevents it here.
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Updating, Error] on failure',
build: buildBloc,
setUp: () =>
when(mockUpdateOrganization(any, any)).thenThrow(Exception('fail')),
act: (b) => b.add(UpdateOrganization('org-1', org)),
expect: () => [
const OrganizationUpdating('org-1'),
isA<OrganizationsError>(),
],
);
});
// ─── DeleteOrganization ───────────────────────────────────────────────────
group('DeleteOrganization', () {
final org = _makeOrg();
final loadedState = OrganizationsLoaded(
organizations: [org],
filteredOrganizations: [org],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Deleting, Deleted] on success (previousState captured after first emit)',
build: buildBloc,
seed: () => loadedState,
setUp: () =>
when(mockDeleteOrganization('org-1')).thenAnswer((_) async {}),
act: (b) => b.add(const DeleteOrganization('org-1')),
expect: () => [
const OrganizationDeleting('org-1'),
const OrganizationDeleted('org-1'),
// Note: the BLoC captures previousState AFTER the first emit,
// so the list-removal branch (previousState is OrganizationsLoaded)
// is not triggered here — consistent with UpdateOrganization behavior.
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Deleting, Error] on failure',
build: buildBloc,
setUp: () =>
when(mockDeleteOrganization(any)).thenThrow(Exception('not found')),
act: (b) => b.add(const DeleteOrganization('org-1')),
expect: () => [
const OrganizationDeleting('org-1'),
isA<OrganizationsError>(),
],
);
});
// ─── ActivateOrganization ─────────────────────────────────────────────────
group('ActivateOrganization', () {
final suspended = _makeOrg(statut: StatutOrganization.suspendue);
final activated = _makeOrg(statut: StatutOrganization.active);
final loadedState = OrganizationsLoaded(
organizations: [suspended],
filteredOrganizations: [suspended],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Activating, Activated, Loaded(updated)] on success',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockRepository.activateOrganization('org-1'))
.thenAnswer((_) async => activated),
act: (b) => b.add(const ActivateOrganization('org-1')),
expect: () => [
const OrganizationActivating('org-1'),
OrganizationActivated(activated),
isA<OrganizationsLoaded>()
.having((s) => s.organizations.first.statut, 'statut', StatutOrganization.active),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Activating, Error] on failure',
build: buildBloc,
setUp: () =>
when(mockRepository.activateOrganization(any)).thenThrow(Exception('fail')),
act: (b) => b.add(const ActivateOrganization('org-1')),
expect: () => [
const OrganizationActivating('org-1'),
isA<OrganizationsError>(),
],
);
});
// ─── SuspendOrganization ──────────────────────────────────────────────────
group('SuspendOrganization', () {
final active = _makeOrg();
final suspended = _makeOrg(statut: StatutOrganization.suspendue);
final loadedState = OrganizationsLoaded(
organizations: [active],
filteredOrganizations: [active],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Suspending, Suspended, Loaded(updated)] on success',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockRepository.suspendOrganization('org-1'))
.thenAnswer((_) async => suspended),
act: (b) => b.add(const SuspendOrganization('org-1')),
expect: () => [
const OrganizationSuspending('org-1'),
OrganizationSuspended(suspended),
isA<OrganizationsLoaded>()
.having((s) => s.organizations.first.statut, 'statut', StatutOrganization.suspendue),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [Suspending, Error] on failure',
build: buildBloc,
setUp: () =>
when(mockRepository.suspendOrganization(any)).thenThrow(Exception('fail')),
act: (b) => b.add(const SuspendOrganization('org-1')),
expect: () => [
const OrganizationSuspending('org-1'),
isA<OrganizationsError>(),
],
);
});
// ─── Filter & Sort ────────────────────────────────────────────────────────
group('FilterOrganizationsByStatus', () {
final orgs = [
_makeOrg(statut: StatutOrganization.active),
_makeOrg(id: 'org-2', statut: StatutOrganization.suspendue),
];
final loadedState = OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits updated filtered list',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockOrganizationService.filterByStatus(any, StatutOrganization.active))
.thenReturn([orgs.first]),
act: (b) => b.add(const FilterOrganizationsByStatus(StatutOrganization.active)),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.statusFilter, 'filter', StatutOrganization.active),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'does nothing when state is not loaded',
build: buildBloc,
act: (b) => b.add(const FilterOrganizationsByStatus(StatutOrganization.active)),
expect: () => [],
);
});
group('FilterOrganizationsByType', () {
final orgs = [_makeOrg(), _makeOrg(id: 'org-2', type: 'TONTINE')];
final loadedState = OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits updated filtered list with typeFilter',
build: buildBloc,
seed: () => loadedState,
setUp: () =>
when(mockOrganizationService.filterByType(any, 'TONTINE')).thenReturn([orgs[1]]),
act: (b) => b.add(const FilterOrganizationsByType('TONTINE')),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.typeFilter, 'typeFilter', 'TONTINE'),
],
);
});
group('SortOrganizations', () {
final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'AAA')];
final loadedState = OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
);
blocTest<OrganizationsBloc, OrganizationsState>(
'sorts by name ascending',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockOrganizationService.sortByName(any,
ascending: anyNamed('ascending')))
.thenReturn([orgs[1], orgs[0]]),
act: (b) =>
b.add(const SortOrganizations(OrganizationSortType.name)),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.sortType, 'sortType', OrganizationSortType.name),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'sorts by creation date',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockOrganizationService.sortByCreationDate(any,
ascending: anyNamed('ascending')))
.thenReturn(orgs),
act: (b) =>
b.add(const SortOrganizations(OrganizationSortType.creationDate)),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.sortType, 'sortType', OrganizationSortType.creationDate),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'sorts by member count',
build: buildBloc,
seed: () => loadedState,
setUp: () => when(mockOrganizationService.sortByMemberCount(any,
ascending: anyNamed('ascending')))
.thenReturn(orgs),
act: (b) =>
b.add(const SortOrganizations(OrganizationSortType.memberCount)),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.sortType, 'sortType', OrganizationSortType.memberCount),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'does nothing when state is not loaded',
build: buildBloc,
act: (b) =>
b.add(const SortOrganizations(OrganizationSortType.name)),
expect: () => [],
);
});
// ─── ClearOrganizationsFilters ────────────────────────────────────────────
group('ClearOrganizationsFilters', () {
final orgs = [_makeOrg()];
final loadedState = OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
statusFilter: StatutOrganization.active,
typeFilter: 'TONTINE',
currentSearch: 'some search',
);
blocTest<OrganizationsBloc, OrganizationsState>(
'clears all filters and restores full list',
build: buildBloc,
seed: () => loadedState,
act: (b) => b.add(const ClearOrganizationsFilters()),
expect: () => [
isA<OrganizationsLoaded>()
.having((s) => s.statusFilter, 'statusFilter', isNull)
.having((s) => s.typeFilter, 'typeFilter', isNull)
.having((s) => s.currentSearch, 'search', isNull),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'does nothing when state is not loaded',
build: buildBloc,
act: (b) => b.add(const ClearOrganizationsFilters()),
expect: () => [],
);
});
// ─── LoadOrganizationsStats ───────────────────────────────────────────────
group('LoadOrganizationsStats', () {
final stats = {'total': 5, 'actives': 3};
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [StatsLoading, StatsLoaded] on success',
build: buildBloc,
setUp: () =>
when(mockRepository.getOrganizationsStats()).thenAnswer((_) async => stats),
act: (b) => b.add(const LoadOrganizationsStats()),
expect: () => [
const OrganizationsStatsLoading(),
OrganizationsStatsLoaded(stats),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'emits [StatsLoading, StatsError] on failure',
build: buildBloc,
setUp: () =>
when(mockRepository.getOrganizationsStats()).thenThrow(Exception('fail')),
act: (b) => b.add(const LoadOrganizationsStats()),
expect: () => [
const OrganizationsStatsLoading(),
isA<OrganizationsStatsError>(),
],
);
});
// ─── RefreshOrganizations ─────────────────────────────────────────────────
group('RefreshOrganizations', () {
final orgs = [_makeOrg()];
blocTest<OrganizationsBloc, OrganizationsState>(
'triggers reload via LoadOrganizations with useMesOnly when applicable',
build: buildBloc,
seed: () => OrganizationsLoaded(
organizations: orgs,
filteredOrganizations: orgs,
useMesOnly: true,
),
setUp: () =>
when(mockRepository.getMesOrganisations()).thenAnswer((_) async => orgs),
act: (b) => b.add(const RefreshOrganizations()),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>().having((s) => s.useMesOnly, 'useMesOnly', true),
],
);
blocTest<OrganizationsBloc, OrganizationsState>(
'triggers standard reload from initial state',
build: buildBloc,
setUp: () => when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => orgs),
act: (b) => b.add(const RefreshOrganizations()),
expect: () => [
const OrganizationsLoading(),
isA<OrganizationsLoaded>(),
],
);
});
// ─── ResetOrganizationsState ──────────────────────────────────────────────
group('ResetOrganizationsState', () {
blocTest<OrganizationsBloc, OrganizationsState>(
'emits OrganizationsInitial',
build: buildBloc,
seed: () => OrganizationsLoaded(
organizations: [_makeOrg()],
filteredOrganizations: [_makeOrg()],
),
act: (b) => b.add(const ResetOrganizationsState()),
expect: () => [const OrganizationsInitial()],
);
});
// ─── DioException cancel is swallowed ─────────────────────────────────────
group('DioException.cancel handling', () {
blocTest<OrganizationsBloc, OrganizationsState>(
'LoadOrganizations: cancel exception produces no error state',
build: buildBloc,
setUp: () {
when(mockGetOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(DioException(
requestOptions: RequestOptions(path: '/test'),
type: DioExceptionType.cancel,
));
},
act: (b) => b.add(const LoadOrganizations()),
expect: () => [const OrganizationsLoading()],
);
});
// ─── OrganizationsLoaded state helpers ────────────────────────────────────
group('OrganizationsLoaded state helpers', () {
final activeOrg = _makeOrg(statut: StatutOrganization.active, nombreMembres: 5);
final suspendedOrg =
_makeOrg(id: 'org-2', statut: StatutOrganization.suspendue, nombreMembres: 3);
final state = OrganizationsLoaded(
organizations: [activeOrg, suspendedOrg],
filteredOrganizations: [activeOrg],
);
test('totalCount returns full organizations count', () {
expect(state.totalCount, 2);
});
test('filteredCount returns filtered count', () {
expect(state.filteredCount, 1);
});
test('quickStats calculates correctly', () {
final stats = state.quickStats;
expect(stats['total'], 2);
expect(stats['actives'], 1);
expect(stats['inactives'], 1);
expect(stats['totalMembres'], 8);
});
test('hasFilters is false when no filters applied', () {
final clean = OrganizationsLoaded(
organizations: [activeOrg],
filteredOrganizations: [activeOrg],
);
expect(clean.hasFilters, false);
});
test('hasFilters is true when statusFilter set', () {
final filtered = OrganizationsLoaded(
organizations: [activeOrg],
filteredOrganizations: [activeOrg],
statusFilter: StatutOrganization.active,
);
expect(filtered.hasFilters, true);
});
test('copyWith with clearSearch resets currentSearch', () {
final withSearch = OrganizationsLoaded(
organizations: [activeOrg],
filteredOrganizations: [activeOrg],
currentSearch: 'test',
);
final cleared = withSearch.copyWith(clearSearch: true);
expect(cleared.currentSearch, isNull);
});
});
}