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:
275
test/features/backup/bloc/backup_bloc_test.dart
Normal file
275
test/features/backup/bloc/backup_bloc_test.dart
Normal file
@@ -0,0 +1,275 @@
|
||||
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>()],
|
||||
);
|
||||
});
|
||||
}
|
||||
145
test/features/backup/bloc/backup_bloc_test.mocks.dart
Normal file
145
test/features/backup/bloc/backup_bloc_test.mocks.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in unionflow_mobile_apps/test/features/backup/bloc/backup_bloc_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i5;
|
||||
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:unionflow_mobile_apps/features/backup/data/models/backup_config_model.dart'
|
||||
as _i3;
|
||||
import 'package:unionflow_mobile_apps/features/backup/data/models/backup_model.dart'
|
||||
as _i2;
|
||||
import 'package:unionflow_mobile_apps/features/backup/data/repositories/backup_repository.dart'
|
||||
as _i4;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeBackupModel_0 extends _i1.SmartFake implements _i2.BackupModel {
|
||||
_FakeBackupModel_0(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeBackupConfigModel_1 extends _i1.SmartFake
|
||||
implements _i3.BackupConfigModel {
|
||||
_FakeBackupConfigModel_1(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [BackupRepository].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockBackupRepository extends _i1.Mock implements _i4.BackupRepository {
|
||||
MockBackupRepository() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i5.Future<List<_i2.BackupModel>> getAll() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getAll, []),
|
||||
returnValue: _i5.Future<List<_i2.BackupModel>>.value(
|
||||
<_i2.BackupModel>[],
|
||||
),
|
||||
)
|
||||
as _i5.Future<List<_i2.BackupModel>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.BackupModel> getById(String? id) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getById, [id]),
|
||||
returnValue: _i5.Future<_i2.BackupModel>.value(
|
||||
_FakeBackupModel_0(this, Invocation.method(#getById, [id])),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i2.BackupModel>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.BackupModel> create(String? name, {String? description}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#create, [name], {#description: description}),
|
||||
returnValue: _i5.Future<_i2.BackupModel>.value(
|
||||
_FakeBackupModel_0(
|
||||
this,
|
||||
Invocation.method(#create, [name], {#description: description}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i2.BackupModel>);
|
||||
|
||||
@override
|
||||
_i5.Future<void> restore(
|
||||
String? backupId, {
|
||||
bool? createRestorePoint = true,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#restore,
|
||||
[backupId],
|
||||
{#createRestorePoint: createRestorePoint},
|
||||
),
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
)
|
||||
as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<void> delete(String? id) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#delete, [id]),
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
)
|
||||
as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.BackupConfigModel> getConfig() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getConfig, []),
|
||||
returnValue: _i5.Future<_i3.BackupConfigModel>.value(
|
||||
_FakeBackupConfigModel_1(this, Invocation.method(#getConfig, [])),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.BackupConfigModel>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.BackupConfigModel> updateConfig(
|
||||
Map<String, dynamic>? config,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateConfig, [config]),
|
||||
returnValue: _i5.Future<_i3.BackupConfigModel>.value(
|
||||
_FakeBackupConfigModel_1(
|
||||
this,
|
||||
Invocation.method(#updateConfig, [config]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.BackupConfigModel>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.BackupModel> createRestorePoint() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#createRestorePoint, []),
|
||||
returnValue: _i5.Future<_i2.BackupModel>.value(
|
||||
_FakeBackupModel_0(
|
||||
this,
|
||||
Invocation.method(#createRestorePoint, []),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i2.BackupModel>);
|
||||
}
|
||||
Reference in New Issue
Block a user