Files
unionflow-mobile-apps/test/features/explore/bloc/network_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

303 lines
11 KiB
Dart

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/explore/presentation/bloc/network_bloc.dart';
import 'package:unionflow_mobile_apps/features/explore/presentation/bloc/network_event.dart';
import 'package:unionflow_mobile_apps/features/explore/presentation/bloc/network_state.dart';
import 'package:unionflow_mobile_apps/features/explore/data/repositories/network_repository.dart';
@GenerateMocks([NetworkRepository])
import 'network_bloc_test.mocks.dart';
void main() {
late NetworkBloc bloc;
late MockNetworkRepository mockRepository;
// ── Fixtures ──────────────────────────────────────────────────────────────
NetworkItem fakeMember({
String id = 'mbr-1',
bool isConnected = false,
}) =>
NetworkItem(
id: id,
name: 'Jean Dupont',
subtitle: 'Comptable',
type: 'Member',
isConnected: isConnected,
);
NetworkItem fakeOrg({String id = 'org-1'}) => NetworkItem(
id: id,
name: 'Association Test',
subtitle: 'ASSOCIATION',
type: 'Organization',
isConnected: false,
);
setUp(() {
mockRepository = MockNetworkRepository();
bloc = NetworkBloc(mockRepository);
});
tearDown(() => bloc.close());
// ── Initial state ─────────────────────────────────────────────────────────
test('initial state is NetworkInitial', () {
expect(bloc.state, isA<NetworkInitial>());
});
// ── LoadNetworkRequested ──────────────────────────────────────────────────
group('LoadNetworkRequested', () {
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkLoaded] on success with followed members',
build: () {
when(mockRepository.getFollowedIds())
.thenAnswer((_) async => ['mbr-1', 'mbr-2']);
when(mockRepository.search('', followedIds: anyNamed('followedIds')))
.thenAnswer((_) async => [
fakeMember(isConnected: true),
fakeMember(id: 'mbr-2', isConnected: true),
fakeOrg(),
]);
return bloc;
},
act: (b) => b.add(LoadNetworkRequested()),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkLoaded>()
.having((s) => s.items.length, 'items.length', 3)
.having((s) => s.currentQuery, 'currentQuery', ''),
],
verify: (_) {
verify(mockRepository.getFollowedIds()).called(1);
verify(mockRepository.search('', followedIds: anyNamed('followedIds'))).called(1);
},
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkLoaded] with empty list',
build: () {
when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
when(mockRepository.search('', followedIds: anyNamed('followedIds')))
.thenAnswer((_) async => []);
return bloc;
},
act: (b) => b.add(LoadNetworkRequested()),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkLoaded>().having((s) => s.items, 'items', isEmpty),
],
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkError] on getFollowedIds failure',
build: () {
when(mockRepository.getFollowedIds()).thenThrow(Exception('auth error'));
return bloc;
},
act: (b) => b.add(LoadNetworkRequested()),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkError>().having(
(s) => s.message,
'message',
contains('Erreur'),
),
],
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkError] on search failure',
build: () {
when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
when(mockRepository.search(any, followedIds: anyNamed('followedIds')))
.thenThrow(Exception('search error'));
return bloc;
},
act: (b) => b.add(LoadNetworkRequested()),
expect: () => [isA<NetworkLoading>(), isA<NetworkError>()],
);
});
// ── SearchNetworkRequested ────────────────────────────────────────────────
group('SearchNetworkRequested', () {
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkLoaded] with search results',
build: () {
when(mockRepository.getFollowedIds()).thenAnswer((_) async => ['mbr-1']);
when(mockRepository.search('Jean', followedIds: anyNamed('followedIds')))
.thenAnswer((_) async => [fakeMember(isConnected: true)]);
return bloc;
},
act: (b) => b.add(const SearchNetworkRequested('Jean')),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkLoaded>()
.having((s) => s.currentQuery, 'currentQuery', 'Jean')
.having((s) => s.items.length, 'items.length', 1),
],
verify: (_) {
verify(mockRepository.search('Jean', followedIds: anyNamed('followedIds'))).called(1);
},
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkLoaded] with empty query (no search)',
build: () {
when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
when(mockRepository.search('', followedIds: anyNamed('followedIds')))
.thenAnswer((_) async => [fakeMember()]);
return bloc;
},
act: (b) => b.add(const SearchNetworkRequested('')),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkLoaded>().having((s) => s.currentQuery, 'currentQuery', ''),
],
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkLoaded] with whitespace-only query (treated as empty)',
build: () {
when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
when(mockRepository.search('', followedIds: anyNamed('followedIds')))
.thenAnswer((_) async => []);
return bloc;
},
act: (b) => b.add(const SearchNetworkRequested(' ')),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkLoaded>().having((s) => s.currentQuery, 'currentQuery', ''),
],
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkLoading, NetworkError] on search failure',
build: () {
when(mockRepository.getFollowedIds()).thenAnswer((_) async => []);
when(mockRepository.search(any, followedIds: anyNamed('followedIds')))
.thenThrow(Exception('search failed'));
return bloc;
},
act: (b) => b.add(const SearchNetworkRequested('error')),
expect: () => [
isA<NetworkLoading>(),
isA<NetworkError>(),
],
);
});
// ── ToggleFollowRequested ─────────────────────────────────────────────────
group('ToggleFollowRequested — Member type', () {
// Prerequisite: put bloc in NetworkLoaded state first
final loadedState = NetworkLoaded(
items: [
fakeMember(id: 'mbr-1', isConnected: false),
fakeOrg(),
],
currentQuery: 'test',
);
blocTest<NetworkBloc, NetworkState>(
'follows an unfollowed Member and emits NetworkLoaded with isConnected=true',
build: () {
when(mockRepository.follow('mbr-1')).thenAnswer((_) async => true);
return bloc;
},
seed: () => loadedState,
act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
expect: () => [
isA<NetworkLoaded>().having(
(s) => s.items.firstWhere((i) => i.id == 'mbr-1').isConnected,
'mbr-1.isConnected',
true,
),
],
verify: (_) => verify(mockRepository.follow('mbr-1')).called(1),
);
blocTest<NetworkBloc, NetworkState>(
'unfollows a followed Member and emits NetworkLoaded with isConnected=false',
build: () {
when(mockRepository.unfollow('mbr-1')).thenAnswer((_) async => false);
return bloc;
},
seed: () => NetworkLoaded(
items: [fakeMember(id: 'mbr-1', isConnected: true)],
currentQuery: '',
),
act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
expect: () => [
isA<NetworkLoaded>().having(
(s) => s.items.first.isConnected,
'isConnected',
false,
),
],
verify: (_) => verify(mockRepository.unfollow('mbr-1')).called(1),
);
blocTest<NetworkBloc, NetworkState>(
'emits [NetworkError] when follow call fails',
build: () {
when(mockRepository.follow(any)).thenThrow(Exception('follow failed'));
return bloc;
},
seed: () => loadedState,
act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
expect: () => [isA<NetworkError>()],
);
});
group('ToggleFollowRequested — Organization type (local toggle only)', () {
blocTest<NetworkBloc, NetworkState>(
'toggles Organization follow locally without calling repository',
build: () => bloc,
seed: () => NetworkLoaded(
items: [fakeOrg(id: 'org-1')],
currentQuery: '',
),
act: (b) => b.add(const ToggleFollowRequested('org-1')),
expect: () => [
isA<NetworkLoaded>().having(
(s) => s.items.first.isConnected,
'isConnected',
true,
),
],
verify: (_) {
verifyNever(mockRepository.follow(any));
verifyNever(mockRepository.unfollow(any));
},
);
});
group('ToggleFollowRequested — guard: item not in list', () {
blocTest<NetworkBloc, NetworkState>(
'does nothing when item id not found in current list',
build: () => bloc,
seed: () => NetworkLoaded(
items: [fakeMember(id: 'mbr-1')],
currentQuery: '',
),
act: (b) => b.add(const ToggleFollowRequested('non-existent')),
expect: () => [],
);
blocTest<NetworkBloc, NetworkState>(
'does nothing when state is not NetworkLoaded',
build: () => bloc,
// Default initial state (NetworkInitial)
act: (b) => b.add(const ToggleFollowRequested('mbr-1')),
expect: () => [],
);
});
}