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

334 lines
12 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/admin/bloc/admin_users_bloc.dart';
import 'package:unionflow_mobile_apps/features/admin/data/repositories/admin_user_repository.dart';
import 'package:unionflow_mobile_apps/features/admin/data/models/admin_user_model.dart';
@GenerateMocks([AdminUserRepository])
import 'admin_users_bloc_test.mocks.dart';
void main() {
late AdminUsersBloc bloc;
late MockAdminUserRepository mockRepository;
// ── Fixtures ──────────────────────────────────────────────────────────────
AdminUserModel fakeUser({String id = 'usr-1'}) => AdminUserModel(
id: id,
username: 'jdupont',
email: 'j.dupont@test.com',
prenom: 'Jean',
nom: 'Dupont',
enabled: true,
);
AdminRoleModel fakeRole({String id = 'role-1', String name = 'MEMBRE'}) =>
AdminRoleModel(id: id, name: name, description: 'Role $name');
AdminUserSearchResult fakeSearchResult({List<AdminUserModel>? users}) =>
AdminUserSearchResult(
users: users ?? [fakeUser()],
totalCount: users?.length ?? 1,
currentPage: 0,
pageSize: 20,
totalPages: 1,
);
setUp(() {
mockRepository = MockAdminUserRepository();
bloc = AdminUsersBloc(mockRepository);
});
tearDown(() => bloc.close());
// ── Initial state ─────────────────────────────────────────────────────────
test('initial state is AdminUsersInitial', () {
expect(bloc.state, isA<AdminUsersInitial>());
});
// ── AdminUsersLoadRequested ───────────────────────────────────────────────
group('AdminUsersLoadRequested', () {
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersLoaded] on success',
build: () {
when(mockRepository.search(
page: anyNamed('page'),
size: anyNamed('size'),
search: anyNamed('search'),
)).thenAnswer((_) async => fakeSearchResult());
return bloc;
},
act: (b) => b.add(AdminUsersLoadRequested()),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUsersLoaded>()
.having((s) => s.users.length, 'users.length', 1)
.having((s) => s.totalCount, 'totalCount', 1)
.having((s) => s.currentPage, 'currentPage', 0),
],
verify: (_) {
verify(mockRepository.search(page: 0, size: 20, search: null)).called(1);
},
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersLoaded] with search term',
build: () {
when(mockRepository.search(
page: anyNamed('page'),
size: anyNamed('size'),
search: 'Jean',
)).thenAnswer((_) async => fakeSearchResult());
return bloc;
},
act: (b) => b.add(AdminUsersLoadRequested(search: 'Jean')),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUsersLoaded>(),
],
verify: (_) {
verify(mockRepository.search(page: 0, size: 20, search: 'Jean')).called(1);
},
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersLoaded] with pagination',
build: () {
when(mockRepository.search(
page: 2,
size: 10,
search: anyNamed('search'),
)).thenAnswer((_) async => fakeSearchResult());
return bloc;
},
act: (b) => b.add(AdminUsersLoadRequested(page: 2, size: 10)),
expect: () => [isA<AdminUsersLoading>(), isA<AdminUsersLoaded>()],
verify: (_) {
verify(mockRepository.search(page: 2, size: 10, search: null)).called(1);
},
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersError] on failure',
build: () {
when(mockRepository.search(
page: anyNamed('page'),
size: anyNamed('size'),
search: anyNamed('search'),
)).thenThrow(Exception('network error'));
return bloc;
},
act: (b) => b.add(AdminUsersLoadRequested()),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUsersError>().having(
(s) => s.message,
'message',
contains('Exception'),
),
],
);
});
// ── AdminUserDetailRequested ──────────────────────────────────────────────
group('AdminUserDetailRequested', () {
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUserDetailLoaded] on success',
build: () {
when(mockRepository.getById('usr-1')).thenAnswer((_) async => fakeUser());
when(mockRepository.getUserRoles('usr-1'))
.thenAnswer((_) async => [fakeRole()]);
return bloc;
},
act: (b) => b.add(AdminUserDetailRequested('usr-1')),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUserDetailLoaded>()
.having((s) => s.user.id, 'user.id', 'usr-1')
.having((s) => s.userRoles.length, 'userRoles.length', 1),
],
verify: (_) {
verify(mockRepository.getById('usr-1')).called(1);
verify(mockRepository.getUserRoles('usr-1')).called(1);
},
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersError] when user not found (null)',
build: () {
when(mockRepository.getById('usr-x')).thenAnswer((_) async => null);
return bloc;
},
act: (b) => b.add(AdminUserDetailRequested('usr-x')),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUsersError>().having(
(s) => s.message,
'message',
'Utilisateur non trouvé',
),
],
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersError] on repository failure',
build: () {
when(mockRepository.getById(any)).thenThrow(Exception('server error'));
return bloc;
},
act: (b) => b.add(AdminUserDetailRequested('usr-1')),
expect: () => [isA<AdminUsersLoading>(), isA<AdminUsersError>()],
);
});
// ── AdminUserDetailWithRolesRequested ─────────────────────────────────────
group('AdminUserDetailWithRolesRequested', () {
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUserDetailLoaded] with allRoles on success',
build: () {
when(mockRepository.getById('usr-1')).thenAnswer((_) async => fakeUser());
when(mockRepository.getUserRoles('usr-1'))
.thenAnswer((_) async => [fakeRole(name: 'MEMBRE')]);
when(mockRepository.getRealmRoles()).thenAnswer(
(_) async => [fakeRole(name: 'MEMBRE'), fakeRole(id: 'role-2', name: 'ADMIN')],
);
return bloc;
},
act: (b) => b.add(AdminUserDetailWithRolesRequested('usr-1')),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUserDetailLoaded>()
.having((s) => s.allRoles.length, 'allRoles.length', 2)
.having((s) => s.userRoles.length, 'userRoles.length', 1),
],
verify: (_) {
verify(mockRepository.getRealmRoles()).called(1);
},
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersError] when user not found',
build: () {
when(mockRepository.getById('unknown')).thenAnswer((_) async => null);
return bloc;
},
act: (b) => b.add(AdminUserDetailWithRolesRequested('unknown')),
expect: () => [
isA<AdminUsersLoading>(),
isA<AdminUsersError>().having(
(s) => s.message,
'message',
'Utilisateur non trouvé',
),
],
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersLoading, AdminUsersError] on getRealmRoles failure',
build: () {
when(mockRepository.getById('usr-1')).thenAnswer((_) async => fakeUser());
when(mockRepository.getUserRoles(any)).thenAnswer((_) async => []);
when(mockRepository.getRealmRoles()).thenThrow(Exception('roles error'));
return bloc;
},
act: (b) => b.add(AdminUserDetailWithRolesRequested('usr-1')),
expect: () => [isA<AdminUsersLoading>(), isA<AdminUsersError>()],
);
});
// ── AdminUserRolesUpdateRequested ─────────────────────────────────────────
group('AdminUserRolesUpdateRequested', () {
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUserRolesUpdated] on update success then reloads detail',
build: () {
when(mockRepository.setUserRoles('usr-1', ['ADMIN', 'MEMBRE']))
.thenAnswer((_) async {});
// For the auto-dispatched AdminUserDetailWithRolesRequested
when(mockRepository.getById('usr-1')).thenAnswer((_) async => fakeUser());
when(mockRepository.getUserRoles('usr-1'))
.thenAnswer((_) async => [fakeRole(name: 'ADMIN')]);
when(mockRepository.getRealmRoles()).thenAnswer(
(_) async => [fakeRole(name: 'ADMIN'), fakeRole(id: 'role-2', name: 'MEMBRE')],
);
return bloc;
},
act: (b) => b.add(AdminUserRolesUpdateRequested('usr-1', ['ADMIN', 'MEMBRE'])),
expect: () => [
isA<AdminUserRolesUpdated>(),
isA<AdminUsersLoading>(),
isA<AdminUserDetailLoaded>(),
],
verify: (_) {
verify(mockRepository.setUserRoles('usr-1', ['ADMIN', 'MEMBRE'])).called(1);
},
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersError] on update failure',
build: () {
when(mockRepository.setUserRoles(any, any)).thenThrow(Exception('roles update error'));
return bloc;
},
act: (b) => b.add(AdminUserRolesUpdateRequested('usr-1', ['ADMIN'])),
expect: () => [isA<AdminUsersError>()],
);
});
// ── AdminRolesLoadRequested ───────────────────────────────────────────────
group('AdminRolesLoadRequested', () {
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminRolesLoaded] on success',
build: () {
when(mockRepository.getRealmRoles()).thenAnswer(
(_) async => [
fakeRole(name: 'MEMBRE'),
fakeRole(id: 'role-2', name: 'ADMIN'),
fakeRole(id: 'role-3', name: 'SUPER_ADMIN'),
],
);
return bloc;
},
act: (b) => b.add(AdminRolesLoadRequested()),
expect: () => [
isA<AdminRolesLoaded>().having(
(s) => s.roles.length,
'roles.length',
3,
),
],
verify: (_) => verify(mockRepository.getRealmRoles()).called(1),
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminRolesLoaded] with empty list',
build: () {
when(mockRepository.getRealmRoles()).thenAnswer((_) async => []);
return bloc;
},
act: (b) => b.add(AdminRolesLoadRequested()),
expect: () => [
isA<AdminRolesLoaded>().having((s) => s.roles, 'roles', isEmpty),
],
);
blocTest<AdminUsersBloc, AdminUsersState>(
'emits [AdminUsersError] on roles load failure',
build: () {
when(mockRepository.getRealmRoles()).thenThrow(Exception('keycloak error'));
return bloc;
},
act: (b) => b.add(AdminRolesLoadRequested()),
expect: () => [isA<AdminUsersError>()],
);
});
}