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:
333
test/features/admin/bloc/admin_users_bloc_test.dart
Normal file
333
test/features/admin/bloc/admin_users_bloc_test.dart
Normal file
@@ -0,0 +1,333 @@
|
||||
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>()],
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user