## 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>
929 lines
34 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|