## 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>
276 lines
9.4 KiB
Dart
276 lines
9.4 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/backup/presentation/bloc/backup_bloc.dart';
|
|
import 'package:unionflow_mobile_apps/features/backup/data/repositories/backup_repository.dart';
|
|
import 'package:unionflow_mobile_apps/features/backup/data/models/backup_model.dart';
|
|
import 'package:unionflow_mobile_apps/features/backup/data/models/backup_config_model.dart';
|
|
|
|
@GenerateMocks([BackupRepository])
|
|
import 'backup_bloc_test.mocks.dart';
|
|
|
|
void main() {
|
|
late BackupBloc bloc;
|
|
late MockBackupRepository mockRepository;
|
|
|
|
// ── Fixtures ──────────────────────────────────────────────────────────────
|
|
|
|
BackupModel fakeBackup({String id = 'bk-1', String name = 'Test'}) => BackupModel(
|
|
id: id,
|
|
name: name,
|
|
type: 'MANUAL',
|
|
status: 'COMPLETED',
|
|
sizeBytes: 1024,
|
|
sizeFormatted: '1 KB',
|
|
);
|
|
|
|
BackupConfigModel fakeConfig() => const BackupConfigModel(
|
|
autoBackupEnabled: true,
|
|
frequency: 'DAILY',
|
|
retentionDays: 30,
|
|
totalBackups: 5,
|
|
);
|
|
|
|
setUp(() {
|
|
mockRepository = MockBackupRepository();
|
|
bloc = BackupBloc(mockRepository);
|
|
});
|
|
|
|
tearDown(() => bloc.close());
|
|
|
|
// ── Initial state ─────────────────────────────────────────────────────────
|
|
|
|
test('initial state is BackupInitial', () {
|
|
expect(bloc.state, isA<BackupInitial>());
|
|
});
|
|
|
|
// ── LoadBackups ───────────────────────────────────────────────────────────
|
|
|
|
group('LoadBackups', () {
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupsLoaded] on success',
|
|
build: () {
|
|
when(mockRepository.getAll())
|
|
.thenAnswer((_) async => [fakeBackup()]);
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(LoadBackups()),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupsLoaded>().having(
|
|
(s) => s.backups.length,
|
|
'backups.length',
|
|
1,
|
|
),
|
|
],
|
|
verify: (_) => verify(mockRepository.getAll()).called(1),
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupError] on failure',
|
|
build: () {
|
|
when(mockRepository.getAll()).thenThrow(Exception('network error'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(LoadBackups()),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupError>().having(
|
|
(s) => s.error,
|
|
'error',
|
|
contains('Erreur'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupsLoaded] with empty list',
|
|
build: () {
|
|
when(mockRepository.getAll()).thenAnswer((_) async => []);
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(LoadBackups()),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupsLoaded>().having((s) => s.backups, 'backups', isEmpty),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ── CreateBackup ──────────────────────────────────────────────────────────
|
|
|
|
group('CreateBackup', () {
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupsLoaded, BackupSuccess] on success',
|
|
build: () {
|
|
when(mockRepository.create('Ma sauvegarde', description: anyNamed('description')))
|
|
.thenAnswer((_) async => fakeBackup());
|
|
when(mockRepository.getAll())
|
|
.thenAnswer((_) async => [fakeBackup()]);
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(CreateBackup('Ma sauvegarde', description: 'desc')),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupsLoaded>(),
|
|
isA<BackupSuccess>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
'Sauvegarde créée',
|
|
),
|
|
],
|
|
verify: (_) {
|
|
verify(mockRepository.create('Ma sauvegarde', description: 'desc')).called(1);
|
|
verify(mockRepository.getAll()).called(1);
|
|
},
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupError] on create failure',
|
|
build: () {
|
|
when(mockRepository.create(any, description: anyNamed('description')))
|
|
.thenThrow(Exception('Server error'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(CreateBackup('Test')),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupError>(),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ── RestoreBackup ─────────────────────────────────────────────────────────
|
|
|
|
group('RestoreBackup', () {
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupSuccess] on restore success',
|
|
build: () {
|
|
when(mockRepository.restore('bk-1')).thenAnswer((_) async {});
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(RestoreBackup('bk-1')),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupSuccess>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
'Restauration en cours',
|
|
),
|
|
],
|
|
verify: (_) => verify(mockRepository.restore('bk-1')).called(1),
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupError] on restore failure',
|
|
build: () {
|
|
when(mockRepository.restore(any)).thenThrow(Exception('restore failed'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(RestoreBackup('bk-1')),
|
|
expect: () => [isA<BackupLoading>(), isA<BackupError>()],
|
|
);
|
|
});
|
|
|
|
// ── DeleteBackup ──────────────────────────────────────────────────────────
|
|
|
|
group('DeleteBackup', () {
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupsLoaded, BackupSuccess] on delete success',
|
|
build: () {
|
|
when(mockRepository.delete('bk-1')).thenAnswer((_) async {});
|
|
when(mockRepository.getAll()).thenAnswer((_) async => []);
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(DeleteBackup('bk-1')),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupsLoaded>(),
|
|
isA<BackupSuccess>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
'Sauvegarde supprimée',
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupError] on delete failure',
|
|
build: () {
|
|
when(mockRepository.delete(any)).thenThrow(Exception('delete failed'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(DeleteBackup('bk-1')),
|
|
expect: () => [isA<BackupLoading>(), isA<BackupError>()],
|
|
);
|
|
});
|
|
|
|
// ── LoadBackupConfig ──────────────────────────────────────────────────────
|
|
|
|
group('LoadBackupConfig', () {
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupConfigLoaded] on success',
|
|
build: () {
|
|
when(mockRepository.getConfig()).thenAnswer((_) async => fakeConfig());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(LoadBackupConfig()),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupConfigLoaded>().having(
|
|
(s) => s.config.autoBackupEnabled,
|
|
'autoBackupEnabled',
|
|
true,
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupError] on config load failure',
|
|
build: () {
|
|
when(mockRepository.getConfig()).thenThrow(Exception('config error'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(LoadBackupConfig()),
|
|
expect: () => [isA<BackupLoading>(), isA<BackupError>()],
|
|
);
|
|
});
|
|
|
|
// ── UpdateBackupConfig ────────────────────────────────────────────────────
|
|
|
|
group('UpdateBackupConfig', () {
|
|
final configMap = <String, dynamic>{'autoBackupEnabled': false, 'frequency': 'WEEKLY'};
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupConfigLoaded, BackupSuccess] on success',
|
|
build: () {
|
|
when(mockRepository.updateConfig(configMap))
|
|
.thenAnswer((_) async => const BackupConfigModel(autoBackupEnabled: false, frequency: 'WEEKLY'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(UpdateBackupConfig(configMap)),
|
|
expect: () => [
|
|
isA<BackupLoading>(),
|
|
isA<BackupConfigLoaded>(),
|
|
isA<BackupSuccess>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
'Configuration mise à jour',
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<BackupBloc, BackupState>(
|
|
'emits [BackupLoading, BackupError] on update failure',
|
|
build: () {
|
|
when(mockRepository.updateConfig(any)).thenThrow(Exception('update error'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(UpdateBackupConfig({})),
|
|
expect: () => [isA<BackupLoading>(), isA<BackupError>()],
|
|
);
|
|
});
|
|
}
|