feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -0,0 +1,270 @@
/// Tests unitaires pour OfflineManager
library offline_manager_test;
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:unionflow_mobile_apps/core/network/offline_manager.dart';
import 'package:unionflow_mobile_apps/core/storage/pending_operations_store.dart';
import 'package:unionflow_mobile_apps/core/config/environment.dart';
@GenerateMocks([
Connectivity,
PendingOperationsStore,
])
import 'offline_manager_test.mocks.dart';
void main() {
// Initialize AppConfig for tests
setUpAll(() {
AppConfig.initialize();
});
group('OfflineManager', () {
late OfflineManager offlineManager;
late MockConnectivity mockConnectivity;
late MockPendingOperationsStore mockOperationsStore;
late StreamController<List<ConnectivityResult>> connectivityController;
setUp(() {
mockConnectivity = MockConnectivity();
mockOperationsStore = MockPendingOperationsStore();
connectivityController = StreamController<List<ConnectivityResult>>.broadcast();
// Setup default stubs
when(mockConnectivity.onConnectivityChanged)
.thenAnswer((_) => connectivityController.stream);
when(mockConnectivity.checkConnectivity())
.thenAnswer((_) async => [ConnectivityResult.wifi]);
when(mockOperationsStore.getPendingOperations())
.thenAnswer((_) async => []);
when(mockOperationsStore.addPendingOperation(
operationType: anyNamed('operationType'),
endpoint: anyNamed('endpoint'),
data: anyNamed('data'),
headers: anyNamed('headers'),
)).thenAnswer((_) async => Future.value());
offlineManager = OfflineManager(mockConnectivity, mockOperationsStore);
});
tearDown(() {
connectivityController.close();
offlineManager.dispose();
});
group('Connectivity status', () {
test('should initialize with unknown status', () {
// Assert
expect(offlineManager.currentStatus, equals(ConnectivityStatus.unknown));
});
test('should detect online status when WiFi is connected', () async {
// Arrange
when(mockConnectivity.checkConnectivity())
.thenAnswer((_) async => [ConnectivityResult.wifi]);
// Act
final isOnline = await offlineManager.isOnline;
// Assert
expect(isOnline, isTrue);
});
test('should detect online status when mobile is connected', () async {
// Arrange
when(mockConnectivity.checkConnectivity())
.thenAnswer((_) async => [ConnectivityResult.mobile]);
// Act
final isOnline = await offlineManager.isOnline;
// Assert
expect(isOnline, isTrue);
});
test('should detect offline status when no connectivity', () async {
// Arrange
when(mockConnectivity.checkConnectivity())
.thenAnswer((_) async => [ConnectivityResult.none]);
// Act
final isOnline = await offlineManager.isOnline;
// Assert
expect(isOnline, isFalse);
});
});
group('Status stream', () {
test('should emit online status when WiFi connects', () async {
// Arrange
final statusUpdates = <ConnectivityStatus>[];
offlineManager.statusStream.listen(statusUpdates.add);
// Act
connectivityController.add([ConnectivityResult.wifi]);
await Future.delayed(const Duration(milliseconds: 100));
// Assert
expect(statusUpdates, contains(ConnectivityStatus.online));
});
test('should emit offline status when connection is lost', () async {
// Arrange
final statusUpdates = <ConnectivityStatus>[];
offlineManager.statusStream.listen(statusUpdates.add);
// Act - First connect, then disconnect
connectivityController.add([ConnectivityResult.wifi]);
await Future.delayed(const Duration(milliseconds: 100));
connectivityController.add([ConnectivityResult.none]);
await Future.delayed(const Duration(milliseconds: 100));
// Assert
expect(statusUpdates, contains(ConnectivityStatus.online));
expect(statusUpdates, contains(ConnectivityStatus.offline));
});
test('should not emit duplicate status updates', () async {
// Arrange
final statusUpdates = <ConnectivityStatus>[];
offlineManager.statusStream.listen(statusUpdates.add);
// Act - Send same status multiple times
connectivityController.add([ConnectivityResult.wifi]);
await Future.delayed(const Duration(milliseconds: 100));
connectivityController.add([ConnectivityResult.wifi]);
await Future.delayed(const Duration(milliseconds: 100));
connectivityController.add([ConnectivityResult.wifi]);
await Future.delayed(const Duration(milliseconds: 100));
// Assert - Should only emit once
expect(statusUpdates.where((s) => s == ConnectivityStatus.online).length, equals(1));
});
});
group('Operation queueing', () {
test('should queue operation when offline', () async {
// Arrange
const operationType = 'approveTransaction';
const endpoint = '/api/finance/approvals/123/approve';
final data = {'approvalId': '123', 'comment': 'Approved'};
// Act
await offlineManager.queueOperation(
operationType: operationType,
endpoint: endpoint,
data: data,
);
// Assert
verify(mockOperationsStore.addPendingOperation(
operationType: operationType,
endpoint: endpoint,
data: data,
headers: null,
)).called(1);
});
test('should include headers when queueing operation', () async {
// Arrange
const operationType = 'createBudget';
const endpoint = '/api/finance/budgets';
final data = {'name': 'Test Budget'};
final headers = {'Authorization': 'Bearer token123'};
// Act
await offlineManager.queueOperation(
operationType: operationType,
endpoint: endpoint,
data: data,
headers: headers,
);
// Assert
verify(mockOperationsStore.addPendingOperation(
operationType: operationType,
endpoint: endpoint,
data: data,
headers: headers,
)).called(1);
});
test('should get count of pending operations', () async {
// Arrange
when(mockOperationsStore.getPendingOperations()).thenAnswer(
(_) async => [
{'id': '1', 'operationType': 'approve'},
{'id': '2', 'operationType': 'reject'},
],
);
// Act
final count = await offlineManager.getPendingOperationsCount();
// Assert
expect(count, equals(2));
});
});
group('Clear operations', () {
test('should clear all pending operations', () async {
// Arrange
when(mockOperationsStore.clearAll()).thenAnswer((_) async => Future.value());
// Act
await offlineManager.clearPendingOperations();
// Assert
verify(mockOperationsStore.clearAll()).called(1);
});
});
group('Retry pending operations', () {
test('should not retry when offline', () async {
// Arrange
when(mockConnectivity.checkConnectivity())
.thenAnswer((_) async => [ConnectivityResult.none]);
connectivityController.add([ConnectivityResult.none]);
await Future.delayed(const Duration(milliseconds: 100));
// Act
await offlineManager.retryPendingOperations();
// Assert
verifyNever(mockOperationsStore.getPendingOperations());
});
test('should process pending operations when manually triggered and online', () async {
// Arrange
when(mockConnectivity.checkConnectivity())
.thenAnswer((_) async => [ConnectivityResult.wifi]);
connectivityController.add([ConnectivityResult.wifi]);
await Future.delayed(const Duration(milliseconds: 100));
when(mockOperationsStore.getPendingOperations()).thenAnswer(
(_) async => [
{
'id': '1',
'operationType': 'approve',
'endpoint': '/api/finance/approvals/123/approve',
'data': {},
},
],
);
// Act
await offlineManager.retryPendingOperations();
// Assert
verify(mockOperationsStore.getPendingOperations()).called(1);
});
});
});
}

View File

@@ -0,0 +1,296 @@
/// Tests unitaires pour RetryPolicy
library retry_policy_test;
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:unionflow_mobile_apps/core/network/retry_policy.dart';
void main() {
group('RetryPolicy', () {
late RetryPolicy retryPolicy;
setUp(() {
retryPolicy = RetryPolicy(
config: const RetryConfig(
maxAttempts: 3,
initialDelayMs: 100, // Short delay for tests
maxDelayMs: 1000,
backoffMultiplier: 2.0,
useJitter: false, // Disable jitter for predictable tests
),
);
});
group('execute - Happy path', () {
test('should return result when operation succeeds on first attempt', () async {
// Arrange
const expectedResult = 'success';
int attempts = 0;
Future<String> operation() async {
attempts++;
return expectedResult;
}
// Act
final result = await retryPolicy.execute(operation: operation);
// Assert
expect(result, equals(expectedResult));
expect(attempts, equals(1));
});
test('should retry and succeed on second attempt', () async {
// Arrange
const expectedResult = 'success';
int attempts = 0;
Future<String> operation() async {
attempts++;
if (attempts == 1) {
throw Exception('Temporary failure');
}
return expectedResult;
}
// Act
final result = await retryPolicy.execute(
operation: operation,
shouldRetry: (_) => true,
);
// Assert
expect(result, equals(expectedResult));
expect(attempts, equals(2));
});
test('should retry maximum attempts and succeed on last attempt', () async {
// Arrange
const expectedResult = 'success';
int attempts = 0;
Future<String> operation() async {
attempts++;
if (attempts < 3) {
throw Exception('Temporary failure');
}
return expectedResult;
}
// Act
final result = await retryPolicy.execute(
operation: operation,
shouldRetry: (_) => true,
);
// Assert
expect(result, equals(expectedResult));
expect(attempts, equals(3));
});
});
group('execute - Retry exhaustion', () {
test('should throw error when all retries are exhausted', () async {
// Arrange
int attempts = 0;
Future<String> operation() async {
attempts++;
throw Exception('Persistent failure');
}
// Act & Assert
expect(
() => retryPolicy.execute(
operation: operation,
shouldRetry: (_) => true,
),
throwsA(isA<Exception>()),
);
// Wait for all attempts
await Future.delayed(const Duration(milliseconds: 500));
expect(attempts, equals(3)); // maxAttempts
});
test('should not retry when shouldRetry returns false', () async {
// Arrange
int attempts = 0;
Future<String> operation() async {
attempts++;
throw Exception('Non-retryable error');
}
// Act & Assert
expect(
() => retryPolicy.execute(
operation: operation,
shouldRetry: (_) => false,
),
throwsA(isA<Exception>()),
);
expect(attempts, equals(1)); // No retries
});
});
group('execute - Error classification', () {
test('should retry timeout exceptions by default', () async {
// Arrange
int attempts = 0;
Future<String> operation() async {
attempts++;
if (attempts == 1) {
throw TimeoutException('Request timeout');
}
return 'success';
}
// Act
final result = await retryPolicy.execute(operation: operation);
// Assert
expect(result, equals('success'));
expect(attempts, equals(2));
});
test('should use custom shouldRetry function', () async {
// Arrange
int attempts = 0;
final specificError = Exception('Specific retryable error');
Future<String> operation() async {
attempts++;
if (attempts == 1) {
throw specificError;
}
return 'success';
}
bool customShouldRetry(dynamic error) {
return error == specificError;
}
// Act
final result = await retryPolicy.execute(
operation: operation,
shouldRetry: customShouldRetry,
);
// Assert
expect(result, equals('success'));
expect(attempts, equals(2));
});
});
group('execute - Retry callback', () {
test('should call onRetry callback with attempt details', () async {
// Arrange
int attempts = 0;
final retryCallbacks = <Map<String, dynamic>>[];
Future<String> operation() async {
attempts++;
if (attempts < 3) {
throw Exception('Temporary failure');
}
return 'success';
}
void onRetry(int attempt, dynamic error, Duration delay) {
retryCallbacks.add({
'attempt': attempt,
'error': error,
'delay': delay,
});
}
// Act
final result = await retryPolicy.execute(
operation: operation,
shouldRetry: (_) => true,
onRetry: onRetry,
);
// Assert
expect(result, equals('success'));
expect(retryCallbacks.length, equals(2)); // 2 retries before success
// Verify first retry callback
expect(retryCallbacks[0]['attempt'], equals(1));
expect(retryCallbacks[0]['error'], isA<Exception>());
expect(retryCallbacks[0]['delay'], isA<Duration>());
// Verify second retry callback
expect(retryCallbacks[1]['attempt'], equals(2));
expect(retryCallbacks[1]['error'], isA<Exception>());
expect(retryCallbacks[1]['delay'], isA<Duration>());
});
});
group('RetryConfig', () {
test('should use default configuration values', () {
// Arrange & Act
const config = RetryConfig();
// Assert
expect(config.maxAttempts, equals(3));
expect(config.initialDelayMs, equals(1000));
expect(config.maxDelayMs, equals(30000));
expect(config.backoffMultiplier, equals(2.0));
expect(config.useJitter, equals(true));
});
test('should use critical configuration preset', () {
// Arrange & Act
const config = RetryConfig.critical;
// Assert
expect(config.maxAttempts, equals(5));
expect(config.initialDelayMs, equals(500));
expect(config.maxDelayMs, equals(60000));
});
test('should use background sync configuration preset', () {
// Arrange & Act
const config = RetryConfig.backgroundSync;
// Assert
expect(config.maxAttempts, equals(10));
expect(config.initialDelayMs, equals(2000));
expect(config.maxDelayMs, equals(120000));
});
});
group('RetryExtension', () {
test('should add withRetry method to Future functions', () async {
// Arrange
int attempts = 0;
Future<String> operation() async {
attempts++;
if (attempts == 1) {
throw Exception('First attempt fails');
}
return 'success';
}
// Act
final result = await operation.withRetry(
config: const RetryConfig(
maxAttempts: 3,
initialDelayMs: 100,
useJitter: false,
),
shouldRetry: (_) => true,
);
// Assert
expect(result, equals('success'));
expect(attempts, equals(2));
});
});
});
}

View File

@@ -0,0 +1,368 @@
/// Tests unitaires pour les validators
library validators_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:unionflow_mobile_apps/core/validation/validators.dart';
void main() {
group('Validators', () {
group('required', () {
test('should return error for null value', () {
final validator = Validators.required();
expect(validator!(''), equals('Ce champ est requis'));
});
test('should return error for empty string', () {
final validator = Validators.required();
expect(validator!(''), equals('Ce champ est requis'));
});
test('should return error for whitespace only', () {
final validator = Validators.required();
expect(validator!(' '), equals('Ce champ est requis'));
});
test('should return null for valid value', () {
final validator = Validators.required();
expect(validator!('value'), isNull);
});
test('should use custom message', () {
final validator = Validators.required(message: 'Custom error');
expect(validator!(''), equals('Custom error'));
});
});
group('minLength', () {
test('should return error when value is too short', () {
final validator = Validators.minLength(5);
expect(validator!('abc'), equals('Minimum 5 caractères requis'));
});
test('should return null when value meets minimum', () {
final validator = Validators.minLength(5);
expect(validator!('abcde'), isNull);
});
test('should return null when value exceeds minimum', () {
final validator = Validators.minLength(5);
expect(validator!('abcdefgh'), isNull);
});
test('should trim value before checking length', () {
final validator = Validators.minLength(5);
expect(validator!(' abc '), equals('Minimum 5 caractères requis'));
});
});
group('maxLength', () {
test('should return error when value is too long', () {
final validator = Validators.maxLength(5);
expect(validator!('abcdefgh'), equals('Maximum 5 caractères autorisés'));
});
test('should return null when value meets maximum', () {
final validator = Validators.maxLength(5);
expect(validator!('abcde'), isNull);
});
test('should return null when value is under maximum', () {
final validator = Validators.maxLength(5);
expect(validator!('abc'), isNull);
});
});
group('email', () {
test('should return null for valid email', () {
final validator = Validators.email();
expect(validator!('test@example.com'), isNull);
expect(validator!('user.name@domain.co.uk'), isNull);
expect(validator!('user+tag@example.com'), isNull);
});
test('should return error for invalid email', () {
final validator = Validators.email();
expect(validator!('invalid'), equals('Adresse email invalide'));
expect(validator!('test@'), equals('Adresse email invalide'));
expect(validator!('@example.com'), equals('Adresse email invalide'));
expect(validator!('test @example.com'), equals('Adresse email invalide'));
});
test('should return null for empty value (use required separately)', () {
final validator = Validators.email();
expect(validator!(''), isNull);
});
});
group('numeric', () {
test('should return null for valid numbers', () {
final validator = Validators.numeric();
expect(validator!('123'), isNull);
expect(validator!('123.45'), isNull);
expect(validator!('-123'), isNull);
expect(validator!('0'), isNull);
});
test('should return error for non-numeric values', () {
final validator = Validators.numeric();
expect(validator!('abc'), equals('Veuillez entrer un nombre valide'));
expect(validator!('12.34.56'), equals('Veuillez entrer un nombre valide'));
});
test('should return null for empty value', () {
final validator = Validators.numeric();
expect(validator!(''), isNull);
});
});
group('minValue', () {
test('should return error when value is below minimum', () {
final validator = Validators.minValue(10);
expect(validator!('5'), equals('La valeur doit être au moins 10.0'));
});
test('should return null when value meets minimum', () {
final validator = Validators.minValue(10);
expect(validator!('10'), isNull);
});
test('should return null when value exceeds minimum', () {
final validator = Validators.minValue(10);
expect(validator!('15'), isNull);
});
test('should work with decimals', () {
final validator = Validators.minValue(10.5);
expect(validator!('10.4'), contains('au moins'));
expect(validator!('10.5'), isNull);
expect(validator!('10.6'), isNull);
});
});
group('maxValue', () {
test('should return error when value exceeds maximum', () {
final validator = Validators.maxValue(100);
expect(validator!('150'), equals('La valeur doit être au maximum 100.0'));
});
test('should return null when value meets maximum', () {
final validator = Validators.maxValue(100);
expect(validator!('100'), isNull);
});
test('should return null when value is below maximum', () {
final validator = Validators.maxValue(100);
expect(validator!('50'), isNull);
});
});
group('range', () {
test('should return error when value is below range', () {
final validator = Validators.range(10, 100);
expect(validator!('5'), contains('entre'));
});
test('should return error when value is above range', () {
final validator = Validators.range(10, 100);
expect(validator!('150'), contains('entre'));
});
test('should return null when value is within range', () {
final validator = Validators.range(10, 100);
expect(validator!('10'), isNull);
expect(validator!('50'), isNull);
expect(validator!('100'), isNull);
});
});
group('phone', () {
test('should return null for valid phone numbers', () {
final validator = Validators.phone();
expect(validator!('+33612345678'), isNull);
expect(validator!('06 12 34 56 78'), isNull);
expect(validator!('(123) 456-7890'), isNull);
});
test('should return error for invalid phone numbers', () {
final validator = Validators.phone();
expect(validator!('abc'), equals('Numéro de téléphone invalide'));
expect(validator!('123'), equals('Numéro de téléphone trop court'));
});
test('should return null for empty value', () {
final validator = Validators.phone();
expect(validator!(''), isNull);
});
});
group('pattern', () {
test('should validate against custom regex', () {
final validator = Validators.pattern(
RegExp(r'^[A-Z]{3}\d{3}$'),
message: 'Format: 3 lettres majuscules + 3 chiffres',
);
expect(validator!('ABC123'), isNull);
expect(validator!('XYZ999'), isNull);
expect(validator!('abc123'), equals('Format: 3 lettres majuscules + 3 chiffres'));
expect(validator!('AB123'), equals('Format: 3 lettres majuscules + 3 chiffres'));
});
});
group('match', () {
test('should return error when values do not match', () {
final validator = Validators.match('password123');
expect(validator!('password456'), equals('Les valeurs ne correspondent pas'));
});
test('should return null when values match', () {
final validator = Validators.match('password123');
expect(validator!('password123'), isNull);
});
});
group('composeValidators', () {
test('should run all validators in sequence', () {
final validator = composeValidators([
Validators.required(),
Validators.minLength(5),
Validators.maxLength(10),
]);
expect(validator!(''), equals('Ce champ est requis'));
expect(validator!('abc'), equals('Minimum 5 caractères requis'));
expect(validator!('12345678901'), equals('Maximum 10 caractères autorisés'));
expect(validator!('valid'), isNull);
});
test('should stop at first error', () {
final validator = composeValidators([
Validators.required(),
Validators.email(),
]);
// Should fail on required, not reach email validator
expect(validator!(''), equals('Ce champ est requis'));
});
});
});
group('FinanceValidators', () {
group('amount', () {
test('should return null for valid amounts', () {
final validator = FinanceValidators.amount();
expect(validator!('100'), isNull);
expect(validator!('100.50'), isNull);
expect(validator!('0.01'), isNull);
});
test('should return error for negative or zero amounts', () {
final validator = FinanceValidators.amount();
expect(validator!('0'), equals('Le montant doit être positif'));
expect(validator!('-10'), equals('Le montant doit être positif'));
});
test('should return error for invalid numbers', () {
final validator = FinanceValidators.amount();
expect(validator!('abc'), equals('Montant invalide'));
});
test('should enforce minimum amount', () {
final validator = FinanceValidators.amount(min: 100);
expect(validator!('50'), equals('Le montant minimum est 100.0'));
expect(validator!('100'), isNull);
expect(validator!('150'), isNull);
});
test('should enforce maximum amount', () {
final validator = FinanceValidators.amount(max: 1000);
expect(validator!('1500'), equals('Le montant maximum est 1000.0'));
expect(validator!('1000'), isNull);
expect(validator!('500'), isNull);
});
test('should enforce max 2 decimals', () {
final validator = FinanceValidators.amount();
expect(validator!('100.123'), equals('Maximum 2 décimales autorisées'));
expect(validator!('100.12'), isNull);
expect(validator!('100.1'), isNull);
});
});
group('budgetLineName', () {
test('should require name', () {
final validator = FinanceValidators.budgetLineName();
expect(validator!(''), contains('requis'));
});
test('should enforce min length', () {
final validator = FinanceValidators.budgetLineName();
expect(validator!('ab'), contains('Minimum 3 caractères'));
});
test('should enforce max length', () {
final validator = FinanceValidators.budgetLineName();
final longName = 'a' * 101;
expect(validator!(longName), contains('Maximum 100 caractères'));
});
test('should accept valid names', () {
final validator = FinanceValidators.budgetLineName();
expect(validator!('Cotisations'), isNull);
expect(validator!('Ligne budgétaire test'), isNull);
});
});
group('rejectionReason', () {
test('should require reason', () {
final validator = FinanceValidators.rejectionReason();
expect(validator!(''), contains('requis'));
});
test('should enforce min length', () {
final validator = FinanceValidators.rejectionReason();
expect(validator!('short'), contains('min 10 caractères'));
});
test('should enforce max length', () {
final validator = FinanceValidators.rejectionReason();
final longReason = 'a' * 501;
expect(validator!(longReason), contains('Maximum 500 caractères'));
});
test('should accept valid reasons', () {
final validator = FinanceValidators.rejectionReason();
expect(validator!('Cette transaction ne respecte pas les règles'), isNull);
});
});
group('fiscalYear', () {
test('should require year', () {
final validator = FinanceValidators.fiscalYear();
expect(validator!(''), contains('requis'));
});
test('should reject invalid year format', () {
final validator = FinanceValidators.fiscalYear();
expect(validator!('abc'), equals('Année invalide'));
});
test('should enforce year range', () {
final validator = FinanceValidators.fiscalYear();
final currentYear = DateTime.now().year;
expect(validator!('${currentYear - 10}'), contains('doit être entre'));
expect(validator!('${currentYear + 15}'), contains('doit être entre'));
});
test('should accept valid years', () {
final validator = FinanceValidators.fiscalYear();
final currentYear = DateTime.now().year;
expect(validator!('$currentYear'), isNull);
expect(validator!('${currentYear + 1}'), isNull);
expect(validator!('${currentYear - 1}'), isNull);
});
});
});
}

View File

@@ -0,0 +1,139 @@
/// Tests unitaires pour GetConversations use case
library get_conversations_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/usecases/get_conversations.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/repositories/messaging_repository.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/entities/conversation.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([MessagingRepository])
import 'get_conversations_test.mocks.dart';
void main() {
late GetConversations useCase;
late MockMessagingRepository mockRepository;
setUp(() {
mockRepository = MockMessagingRepository();
useCase = GetConversations(mockRepository);
});
group('GetConversations Use Case', () {
final tConversations = [
Conversation(
id: 'conv-1',
name: 'Discussion Projet Alpha',
type: ConversationType.group,
participantIds: ['user-1', 'user-2', 'user-3'],
organizationId: 'org-123',
unreadCount: 5,
isPinned: true,
createdAt: DateTime(2024, 12, 1),
),
Conversation(
id: 'conv-2',
name: 'Fatou Ndiaye',
type: ConversationType.individual,
participantIds: ['user-1', 'user-4'],
unreadCount: 0,
createdAt: DateTime(2024, 12, 10),
),
];
test('should return list of conversations successfully', () async {
// Arrange
when(mockRepository.getConversations(
organizationId: anyNamed('organizationId'),
includeArchived: anyNamed('includeArchived'),
)).thenAnswer((_) async => Right(tConversations));
// Act
final result = await useCase(organizationId: 'org-123');
// Assert
expect(result, Right(tConversations));
result.fold(
(failure) => fail('Should not return failure'),
(conversations) {
expect(conversations.length, equals(2));
expect(conversations[0].name, equals('Discussion Projet Alpha'));
expect(conversations[0].unreadCount, equals(5));
expect(conversations[0].isPinned, isTrue);
},
);
verify(mockRepository.getConversations(
organizationId: 'org-123',
includeArchived: false,
));
verifyNoMoreInteractions(mockRepository);
});
test('should return conversations with archived included', () async {
// Arrange
final archivedConv = Conversation(
id: 'conv-3',
name: 'Ancienne Discussion',
type: ConversationType.group,
participantIds: ['user-1', 'user-2'],
isArchived: true,
createdAt: DateTime(2024, 11, 1),
);
when(mockRepository.getConversations(
organizationId: anyNamed('organizationId'),
includeArchived: true,
)).thenAnswer((_) async => Right([...tConversations, archivedConv]));
// Act
final result = await useCase(includeArchived: true);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(conversations) {
expect(conversations.length, equals(3));
expect(conversations.any((c) => c.isArchived), isTrue);
},
);
});
test('should return empty list when no conversations exist', () async {
// Arrange
when(mockRepository.getConversations(
organizationId: anyNamed('organizationId'),
includeArchived: anyNamed('includeArchived'),
)).thenAnswer((_) async => Right([]));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(conversations) => expect(conversations, isEmpty),
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getConversations(
organizationId: anyNamed('organizationId'),
includeArchived: anyNamed('includeArchived'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase();
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(conversations) => fail('Should not return conversations'),
);
});
});
}

View File

@@ -0,0 +1,141 @@
/// Tests unitaires pour GetMessages use case
library get_messages_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/usecases/get_messages.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/repositories/messaging_repository.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/entities/message.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([MessagingRepository])
import 'get_messages_test.mocks.dart';
void main() {
late GetMessages useCase;
late MockMessagingRepository mockRepository;
setUp(() {
mockRepository = MockMessagingRepository();
useCase = GetMessages(mockRepository);
});
group('GetMessages Use Case', () {
const tConversationId = 'conv-123';
final tMessages = [
Message(
id: 'msg-1',
conversationId: tConversationId,
senderId: 'user-1',
senderName: 'Amadou Diallo',
content: 'Bonjour à tous!',
type: MessageType.individual,
status: MessageStatus.read,
priority: MessagePriority.normal,
recipientIds: ['user-2', 'user-3'],
createdAt: DateTime(2024, 12, 15, 10, 0),
),
Message(
id: 'msg-2',
conversationId: tConversationId,
senderId: 'user-2',
senderName: 'Fatou Ndiaye',
content: 'Salut Amadou!',
type: MessageType.individual,
status: MessageStatus.delivered,
priority: MessagePriority.normal,
recipientIds: ['user-1'],
createdAt: DateTime(2024, 12, 15, 10, 5),
),
];
test('should return list of messages successfully', () async {
// Arrange
when(mockRepository.getMessages(
conversationId: tConversationId,
limit: anyNamed('limit'),
beforeMessageId: anyNamed('beforeMessageId'),
)).thenAnswer((_) async => Right(tMessages));
// Act
final result = await useCase(conversationId: tConversationId);
// Assert
expect(result, Right(tMessages));
result.fold(
(failure) => fail('Should not return failure'),
(messages) {
expect(messages.length, equals(2));
expect(messages[0].content, equals('Bonjour à tous!'));
expect(messages[0].status, equals(MessageStatus.read));
},
);
verify(mockRepository.getMessages(
conversationId: tConversationId,
limit: null,
beforeMessageId: null,
));
verifyNoMoreInteractions(mockRepository);
});
test('should return paginated messages with limit', () async {
// Arrange
when(mockRepository.getMessages(
conversationId: tConversationId,
limit: 1,
beforeMessageId: anyNamed('beforeMessageId'),
)).thenAnswer((_) async => Right([tMessages[0]]));
// Act
final result = await useCase(conversationId: tConversationId, limit: 1);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(messages) {
expect(messages.length, equals(1));
},
);
});
test('should return empty list when no messages exist', () async {
// Arrange
when(mockRepository.getMessages(
conversationId: anyNamed('conversationId'),
limit: anyNamed('limit'),
beforeMessageId: anyNamed('beforeMessageId'),
)).thenAnswer((_) async => Right([]));
// Act
final result = await useCase(conversationId: 'empty-conv');
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(messages) => expect(messages, isEmpty),
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getMessages(
conversationId: anyNamed('conversationId'),
limit: anyNamed('limit'),
beforeMessageId: anyNamed('beforeMessageId'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(conversationId: tConversationId);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(messages) => fail('Should not return messages'),
);
});
});
}

View File

@@ -0,0 +1,169 @@
/// Tests unitaires pour SendBroadcast use case
library send_broadcast_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/usecases/send_broadcast.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/repositories/messaging_repository.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/entities/message.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([MessagingRepository])
import 'send_broadcast_test.mocks.dart';
void main() {
late SendBroadcast useCase;
late MockMessagingRepository mockRepository;
setUp(() {
mockRepository = MockMessagingRepository();
useCase = SendBroadcast(mockRepository);
});
group('SendBroadcast Use Case', () {
const tOrgId = 'org-123';
const tSubject = 'Assemblée Générale 2024';
const tContent = 'Chers membres, l\'AG aura lieu le 20 décembre...';
final tBroadcastMessage = Message(
id: 'broadcast-1',
conversationId: 'broadcast-conv',
senderId: 'admin-1',
senderName: 'Admin Organisation',
content: tContent,
type: MessageType.broadcast,
status: MessageStatus.sent,
priority: MessagePriority.high,
recipientIds: ['all'],
organizationId: tOrgId,
createdAt: DateTime.now(),
metadata: {'subject': tSubject},
);
test('should send broadcast message successfully', () async {
// Arrange
when(mockRepository.sendBroadcast(
organizationId: tOrgId,
subject: tSubject,
content: tContent,
priority: anyNamed('priority'),
attachments: anyNamed('attachments'),
)).thenAnswer((_) async => Right(tBroadcastMessage));
// Act
final result = await useCase(
organizationId: tOrgId,
subject: tSubject,
content: tContent,
);
// Assert
expect(result, Right(tBroadcastMessage));
result.fold(
(failure) => fail('Should not return failure'),
(message) {
expect(message.type, equals(MessageType.broadcast));
expect(message.content, equals(tContent));
expect(message.metadata?['subject'], equals(tSubject));
},
);
verify(mockRepository.sendBroadcast(
organizationId: tOrgId,
subject: tSubject,
content: tContent,
priority: MessagePriority.normal,
attachments: null,
));
verifyNoMoreInteractions(mockRepository);
});
test('should send urgent broadcast with attachments', () async {
// Arrange
final attachments = ['document.pdf', 'plan.jpg'];
final urgentBroadcast = Message(
id: 'broadcast-urgent',
conversationId: 'broadcast-conv',
senderId: 'admin-1',
senderName: 'Admin Organisation',
content: 'URGENT: Annulation événement',
type: MessageType.broadcast,
status: MessageStatus.sent,
priority: MessagePriority.urgent,
recipientIds: ['all'],
organizationId: tOrgId,
attachments: attachments,
createdAt: DateTime.now(),
);
when(mockRepository.sendBroadcast(
organizationId: tOrgId,
subject: 'URGENT',
content: 'URGENT: Annulation événement',
priority: MessagePriority.urgent,
attachments: attachments,
)).thenAnswer((_) async => Right(urgentBroadcast));
// Act
final result = await useCase(
organizationId: tOrgId,
subject: 'URGENT',
content: 'URGENT: Annulation événement',
priority: MessagePriority.urgent,
attachments: attachments,
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(message) {
expect(message.priority, equals(MessagePriority.urgent));
expect(message.attachments, equals(attachments));
},
);
});
test('should return ValidationFailure when subject or content is empty', () async {
// Act
final result = await useCase(
organizationId: tOrgId,
subject: '',
content: 'Content',
);
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(message) => fail('Should not return message'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur d\'envoi broadcast');
when(mockRepository.sendBroadcast(
organizationId: anyNamed('organizationId'),
subject: anyNamed('subject'),
content: anyNamed('content'),
priority: anyNamed('priority'),
attachments: anyNamed('attachments'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(
organizationId: tOrgId,
subject: tSubject,
content: tContent,
);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(message) => fail('Should not return message'),
);
});
});
}

View File

@@ -0,0 +1,158 @@
/// Tests unitaires pour SendMessage use case
library send_message_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/usecases/send_message.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/repositories/messaging_repository.dart';
import 'package:unionflow_mobile_apps/features/communication/domain/entities/message.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([MessagingRepository])
import 'send_message_test.mocks.dart';
void main() {
late SendMessage useCase;
late MockMessagingRepository mockRepository;
setUp(() {
mockRepository = MockMessagingRepository();
useCase = SendMessage(mockRepository);
});
group('SendMessage Use Case', () {
const tConversationId = 'conv-123';
const tContent = 'Bonjour, comment allez-vous?';
final tSentMessage = Message(
id: 'msg-new',
conversationId: tConversationId,
senderId: 'user-1',
senderName: 'Amadou Diallo',
content: tContent,
type: MessageType.individual,
status: MessageStatus.sent,
priority: MessagePriority.normal,
recipientIds: ['user-2'],
createdAt: DateTime.now(),
);
test('should send message successfully', () async {
// Arrange
when(mockRepository.sendMessage(
conversationId: tConversationId,
content: tContent,
attachments: anyNamed('attachments'),
priority: anyNamed('priority'),
)).thenAnswer((_) async => Right(tSentMessage));
// Act
final result = await useCase(
conversationId: tConversationId,
content: tContent,
);
// Assert
expect(result, Right(tSentMessage));
result.fold(
(failure) => fail('Should not return failure'),
(message) {
expect(message.id, equals('msg-new'));
expect(message.content, equals(tContent));
expect(message.status, equals(MessageStatus.sent));
},
);
verify(mockRepository.sendMessage(
conversationId: tConversationId,
content: tContent,
attachments: null,
priority: MessagePriority.normal,
));
verifyNoMoreInteractions(mockRepository);
});
test('should send urgent message with attachments', () async {
// Arrange
final attachments = ['file1.pdf', 'file2.jpg'];
final urgentMessage = Message(
id: 'msg-urgent',
conversationId: tConversationId,
senderId: 'user-1',
senderName: 'Amadou Diallo',
content: 'URGENT: Document important',
type: MessageType.individual,
status: MessageStatus.sent,
priority: MessagePriority.urgent,
recipientIds: ['user-2'],
attachments: attachments,
createdAt: DateTime.now(),
);
when(mockRepository.sendMessage(
conversationId: tConversationId,
content: 'URGENT: Document important',
attachments: attachments,
priority: MessagePriority.urgent,
)).thenAnswer((_) async => Right(urgentMessage));
// Act
final result = await useCase(
conversationId: tConversationId,
content: 'URGENT: Document important',
attachments: attachments,
priority: MessagePriority.urgent,
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(message) {
expect(message.priority, equals(MessagePriority.urgent));
expect(message.attachments, equals(attachments));
},
);
});
test('should return ValidationFailure when content is empty', () async {
// Act
final result = await useCase(
conversationId: tConversationId,
content: ' ',
);
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('ne peut pas être vide'));
},
(message) => fail('Should not return message'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur d\'envoi');
when(mockRepository.sendMessage(
conversationId: anyNamed('conversationId'),
content: anyNamed('content'),
attachments: anyNamed('attachments'),
priority: anyNamed('priority'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(
conversationId: tConversationId,
content: tContent,
);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(message) => fail('Should not return message'),
);
});
});
}

View File

@@ -0,0 +1,125 @@
/// Tests unitaires pour CreateContribution use case
library create_contribution_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/create_contribution.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
@GenerateMocks([IContributionRepository])
import 'create_contribution_test.mocks.dart';
void main() {
late CreateContribution useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = CreateContribution(mockRepository);
});
group('CreateContribution Use Case', () {
final tNewContribution = ContributionModel(
membreId: 'membre1',
montant: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
annee: 2024,
type: ContributionType.annuelle,
statut: ContributionStatus.nonPayee,
);
final tCreatedContribution = ContributionModel(
id: 'cont123',
membreId: 'membre1',
membreNom: 'Dupont',
membrePrenom: 'Jean',
montant: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
annee: 2024,
type: ContributionType.annuelle,
statut: ContributionStatus.nonPayee,
);
test('should create contribution successfully', () async {
// Arrange
when(mockRepository.createCotisation(tNewContribution))
.thenAnswer((_) async => tCreatedContribution);
// Act
final result = await useCase(tNewContribution);
// Assert
expect(result, equals(tCreatedContribution));
expect(result.id, isNotNull);
expect(result.id, equals('cont123'));
expect(result.montant, equals(5000.0));
verify(mockRepository.createCotisation(tNewContribution));
verifyNoMoreInteractions(mockRepository);
});
test('should create monthly contribution', () async {
// Arrange
final monthlyContribution = ContributionModel(
membreId: 'membre1',
montant: 2000.0,
dateEcheance: DateTime(2024, 1, 31),
annee: 2024,
mois: 1,
type: ContributionType.mensuelle,
statut: ContributionStatus.nonPayee,
);
final createdMonthly = ContributionModel(
id: 'cont456',
membreId: 'membre1',
montant: 2000.0,
dateEcheance: DateTime(2024, 1, 31),
annee: 2024,
mois: 1,
type: ContributionType.mensuelle,
statut: ContributionStatus.nonPayee,
);
when(mockRepository.createCotisation(monthlyContribution))
.thenAnswer((_) async => createdMonthly);
// Act
final result = await useCase(monthlyContribution);
// Assert
expect(result.type, equals(ContributionType.mensuelle));
expect(result.mois, equals(1));
});
test('should create contribution with description', () async {
// Arrange
final contributionWithDesc = ContributionModel(
membreId: 'membre1',
montant: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
annee: 2024,
type: ContributionType.exceptionnelle,
statut: ContributionStatus.nonPayee,
description: 'Cotisation exceptionnelle pour projet spécial',
);
when(mockRepository.createCotisation(any))
.thenAnswer((_) async => contributionWithDesc.copyWith(id: 'cont789'));
// Act
final result = await useCase(contributionWithDesc);
// Assert
expect(result.description, isNotNull);
expect(result.type, equals(ContributionType.exceptionnelle));
});
test('should throw exception when creation fails', () async {
// Arrange
when(mockRepository.createCotisation(any))
.thenThrow(Exception('Erreur de validation'));
// Act & Assert
expect(() => useCase(tNewContribution), throwsException);
});
});
}

View File

@@ -0,0 +1,66 @@
/// Tests unitaires pour DeleteContribution use case
library delete_contribution_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/delete_contribution.dart';
@GenerateMocks([IContributionRepository])
import 'delete_contribution_test.mocks.dart';
void main() {
late DeleteContribution useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = DeleteContribution(mockRepository);
});
group('DeleteContribution Use Case', () {
const tContributionId = 'cont123';
test('should delete contribution successfully', () async {
// Arrange
when(mockRepository.deleteCotisation(tContributionId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tContributionId);
// Assert
verify(mockRepository.deleteCotisation(tContributionId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when contribution not found', () async {
// Arrange
when(mockRepository.deleteCotisation(any))
.thenThrow(Exception('Contribution non trouvée'));
// Act & Assert
expect(() => useCase('nonexistent'), throwsA(isA<Exception>()));
verify(mockRepository.deleteCotisation('nonexistent'));
});
test('should throw exception when contribution is already paid', () async {
// Arrange
when(mockRepository.deleteCotisation(any))
.thenThrow(Exception('Impossible de supprimer une cotisation payée'));
// Act & Assert
expect(() => useCase(tContributionId), throwsA(isA<Exception>()));
});
test('should throw exception when deletion fails', () async {
// Arrange
when(mockRepository.deleteCotisation(any))
.thenThrow(Exception('Erreur de suppression'));
// Act & Assert
expect(() => useCase(tContributionId), throwsException);
});
});
}

View File

@@ -0,0 +1,103 @@
/// Tests unitaires pour GetContributionById use case
library get_contribution_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_by_id.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
@GenerateMocks([IContributionRepository])
import 'get_contribution_by_id_test.mocks.dart';
void main() {
late GetContributionById useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = GetContributionById(mockRepository);
});
group('GetContributionById Use Case', () {
const tContributionId = 'cont123';
final tContribution = ContributionModel(
id: tContributionId,
membreId: 'membre1',
membreNom: 'Dupont',
membrePrenom: 'Jean',
montant: 5000.0,
montantPaye: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
datePaiement: DateTime(2024, 11, 15),
annee: 2024,
type: ContributionType.annuelle,
statut: ContributionStatus.payee,
methodePaiement: PaymentMethod.waveMoney,
numeroPaiement: 'WAVE123456',
);
test('should return contribution by id', () async {
// Arrange
when(mockRepository.getCotisationById(tContributionId))
.thenAnswer((_) async => tContribution);
// Act
final result = await useCase(tContributionId);
// Assert
expect(result, equals(tContribution));
expect(result.id, equals(tContributionId));
expect(result.statut, equals(ContributionStatus.payee));
verify(mockRepository.getCotisationById(tContributionId));
verifyNoMoreInteractions(mockRepository);
});
test('should return contribution with payment details', () async {
// Arrange
when(mockRepository.getCotisationById(tContributionId))
.thenAnswer((_) async => tContribution);
// Act
final result = await useCase(tContributionId);
// Assert
expect(result.montantPaye, equals(5000.0));
expect(result.methodePaiement, equals(PaymentMethod.waveMoney));
expect(result.numeroPaiement, isNotNull);
});
test('should return unpaid contribution', () async {
// Arrange
final unpaidContribution = ContributionModel(
id: 'cont456',
membreId: 'membre2',
montant: 10000.0,
dateEcheance: DateTime(2025, 1, 31),
annee: 2025,
type: ContributionType.mensuelle,
statut: ContributionStatus.enRetard,
);
when(mockRepository.getCotisationById('cont456'))
.thenAnswer((_) async => unpaidContribution);
// Act
final result = await useCase('cont456');
// Assert
expect(result.statut, equals(ContributionStatus.enRetard));
expect(result.montantPaye, isNull);
expect(result.datePaiement, isNull);
});
test('should throw exception when contribution not found', () async {
// Arrange
when(mockRepository.getCotisationById(any))
.thenThrow(Exception('Contribution non trouvée'));
// Act & Assert
expect(() => useCase('nonexistent'), throwsException);
});
});
}

View File

@@ -0,0 +1,153 @@
/// Tests unitaires pour GetContributionHistory use case
library get_contribution_history_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_history.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/repositories/contribution_repository.dart';
@GenerateMocks([IContributionRepository])
import 'get_contribution_history_test.mocks.dart';
void main() {
late GetContributionHistory useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = GetContributionHistory(mockRepository);
});
group('GetContributionHistory Use Case', () {
final tHistoryList = [
ContributionModel(
id: 'cont1',
membreId: 'membre1',
montant: 5000.0,
montantPaye: 5000.0,
dateEcheance: DateTime(2024, 1, 31),
datePaiement: DateTime(2024, 1, 15),
annee: 2024,
mois: 1,
type: ContributionType.mensuelle,
statut: ContributionStatus.payee,
),
ContributionModel(
id: 'cont2',
membreId: 'membre1',
montant: 5000.0,
dateEcheance: DateTime(2024, 2, 28),
annee: 2024,
mois: 2,
type: ContributionType.mensuelle,
statut: ContributionStatus.enAttente,
),
];
final tPageResult = ContributionPageResult(
contributions: tHistoryList,
total: 2,
totalPages: 1,
page: 0,
size: 50,
);
test('should return contribution history', () async {
// Arrange
when(mockRepository.getMesCotisations(page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => tPageResult);
// Act
final result = await useCase(page: 0, size: 50);
// Assert
expect(result, equals(tPageResult));
expect(result.contributions.length, equals(2));
expect(result.contributions[0].statut, equals(ContributionStatus.payee));
expect(result.contributions[1].statut, equals(ContributionStatus.enAttente));
verify(mockRepository.getMesCotisations(page: 0, size: 50));
verifyNoMoreInteractions(mockRepository);
});
test('should return history for specific year', () async {
// Arrange
final year2023List = [
ContributionModel(
id: 'cont3',
membreId: 'membre1',
montant: 60000.0,
montantPaye: 60000.0,
dateEcheance: DateTime(2023, 12, 31),
datePaiement: DateTime(2023, 12, 15),
annee: 2023,
type: ContributionType.annuelle,
statut: ContributionStatus.payee,
),
];
final yearPageResult = ContributionPageResult(
contributions: year2023List,
total: 1,
totalPages: 1,
page: 0,
size: 50,
);
when(mockRepository.getMesCotisations(page: 0, size: 50))
.thenAnswer((_) async => yearPageResult);
// Act
final result = await useCase(page: 0, size: 50, annee: 2023);
// Assert
expect(result.contributions.length, equals(1));
expect(result.contributions.first.annee, equals(2023));
});
test('should return history filtered by status', () async {
// Arrange
final paidOnly = [tHistoryList[0]];
final paidPageResult = ContributionPageResult(
contributions: paidOnly,
total: 1,
totalPages: 1,
page: 0,
size: 50,
);
when(mockRepository.getMesCotisations(page: 0, size: 50))
.thenAnswer((_) async => paidPageResult);
// Act
final result = await useCase(
page: 0,
size: 50,
statut: ContributionStatus.payee,
);
// Assert
expect(result.contributions.length, equals(1));
expect(result.contributions.first.statut, equals(ContributionStatus.payee));
});
test('should return empty history when no contributions', () async {
// Arrange
final emptyResult = ContributionPageResult(
contributions: [],
total: 0,
totalPages: 0,
page: 0,
size: 50,
);
when(mockRepository.getMesCotisations(page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => emptyResult);
// Act
final result = await useCase();
// Assert
expect(result.contributions, isEmpty);
expect(result.total, equals(0));
});
});
}

View File

@@ -0,0 +1,89 @@
/// Tests unitaires pour GetContributionStats use case
library get_contribution_stats_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contribution_stats.dart';
@GenerateMocks([IContributionRepository])
import 'get_contribution_stats_test.mocks.dart';
void main() {
late GetContributionStats useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = GetContributionStats(mockRepository);
});
group('GetContributionStats Use Case', () {
final tStats = {
'montantDu': 60000.0,
'totalPayeAnnee': 45000.0,
'cotisationsEnAttente': 3,
'prochaineEcheance': '2024-12-31T00:00:00.000Z',
'tauxPaiement': 75.0,
'nombreCotisations': 12,
'montantMoyenCotisation': 5000.0,
};
test('should return contribution statistics', () async {
// Arrange
when(mockRepository.getMesCotisationsSynthese())
.thenAnswer((_) async => tStats);
// Act
final result = await useCase();
// Assert
expect(result, equals(tStats));
expect(result?['montantDu'], equals(60000.0));
expect(result?['totalPayeAnnee'], equals(45000.0));
expect(result?['tauxPaiement'], equals(75.0));
verify(mockRepository.getMesCotisationsSynthese());
verifyNoMoreInteractions(mockRepository);
});
test('should return stats with payment rate', () async {
// Arrange
when(mockRepository.getMesCotisationsSynthese())
.thenAnswer((_) async => tStats);
// Act
final result = await useCase();
// Assert
expect(result?['tauxPaiement'], equals(75.0));
expect(result?['cotisationsEnAttente'], equals(3));
});
test('should return stats with next deadline', () async {
// Arrange
when(mockRepository.getMesCotisationsSynthese())
.thenAnswer((_) async => tStats);
// Act
final result = await useCase();
// Assert
expect(result?['prochaineEcheance'], isNotNull);
expect(result?['prochaineEcheance'], contains('2024-12-31'));
});
test('should return null when no data available', () async {
// Arrange
when(mockRepository.getMesCotisationsSynthese())
.thenAnswer((_) async => null);
// Act
final result = await useCase();
// Assert
expect(result, isNull);
verify(mockRepository.getMesCotisationsSynthese());
});
});
}

View File

@@ -0,0 +1,124 @@
/// Tests unitaires pour GetContributions use case
library get_contributions_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/get_contributions.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/repositories/contribution_repository.dart';
@GenerateMocks([IContributionRepository])
import 'get_contributions_test.mocks.dart';
void main() {
late GetContributions useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = GetContributions(mockRepository);
});
group('GetContributions Use Case', () {
final tContributionList = [
ContributionModel(
id: 'cont1',
membreId: 'membre1',
membreNom: 'Dupont',
membrePrenom: 'Jean',
montant: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
annee: 2024,
type: ContributionType.mensuelle,
statut: ContributionStatus.payee,
),
ContributionModel(
id: 'cont2',
membreId: 'membre1',
membreNom: 'Dupont',
membrePrenom: 'Jean',
montant: 5000.0,
dateEcheance: DateTime(2025, 1, 31),
annee: 2025,
type: ContributionType.mensuelle,
statut: ContributionStatus.enAttente,
),
];
final tPageResult = ContributionPageResult(
contributions: tContributionList,
total: 2,
totalPages: 1,
page: 0,
size: 50,
);
test('should return paginated list of contributions', () async {
// Arrange
when(mockRepository.getMesCotisations(page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => tPageResult);
// Act
final result = await useCase(page: 0, size: 50);
// Assert
expect(result, equals(tPageResult));
expect(result.contributions.length, equals(2));
expect(result.total, equals(2));
verify(mockRepository.getMesCotisations(page: 0, size: 50));
verifyNoMoreInteractions(mockRepository);
});
test('should return contributions with custom page size', () async {
// Arrange
final smallPageResult = ContributionPageResult(
contributions: [tContributionList[0]],
total: 2,
totalPages: 2,
page: 0,
size: 1,
);
when(mockRepository.getMesCotisations(page: 0, size: 1))
.thenAnswer((_) async => smallPageResult);
// Act
final result = await useCase(page: 0, size: 1);
// Assert
expect(result.contributions.length, equals(1));
expect(result.size, equals(1));
verify(mockRepository.getMesCotisations(page: 0, size: 1));
});
test('should return empty result when no contributions exist', () async {
// Arrange
final emptyResult = ContributionPageResult(
contributions: [],
total: 0,
totalPages: 0,
page: 0,
size: 50,
);
when(mockRepository.getMesCotisations(page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => emptyResult);
// Act
final result = await useCase(page: 0, size: 50);
// Assert
expect(result.contributions, isEmpty);
expect(result.total, equals(0));
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getMesCotisations(page: anyNamed('page'), size: anyNamed('size')))
.thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(page: 0, size: 50), throwsException);
});
});
}

View File

@@ -0,0 +1,165 @@
/// Tests unitaires pour PayContribution use case
library pay_contribution_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/pay_contribution.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
@GenerateMocks([IContributionRepository])
import 'pay_contribution_test.mocks.dart';
void main() {
late PayContribution useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = PayContribution(mockRepository);
});
group('PayContribution Use Case', () {
const tContributionId = 'cont123';
const tMontant = 5000.0;
final tDatePaiement = DateTime(2024, 11, 15);
const tMethode = 'WAVE_MONEY';
const tNumero = 'WAVE123456';
final tPaidContribution = ContributionModel(
id: tContributionId,
membreId: 'membre1',
montant: 5000.0,
montantPaye: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
datePaiement: tDatePaiement,
annee: 2024,
type: ContributionType.annuelle,
statut: ContributionStatus.payee,
methodePaiement: PaymentMethod.waveMoney,
numeroPaiement: tNumero,
);
test('should record payment successfully', () async {
// Arrange
when(mockRepository.enregistrerPaiement(
tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
numeroPaiement: tNumero,
referencePaiement: anyNamed('referencePaiement'),
)).thenAnswer((_) async => tPaidContribution);
// Act
final result = await useCase(
cotisationId: tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
numeroPaiement: tNumero,
);
// Assert
expect(result, equals(tPaidContribution));
expect(result.statut, equals(ContributionStatus.payee));
expect(result.montantPaye, equals(5000.0));
expect(result.datePaiement, equals(tDatePaiement));
verify(mockRepository.enregistrerPaiement(
tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
numeroPaiement: tNumero,
referencePaiement: null,
));
verifyNoMoreInteractions(mockRepository);
});
test('should record partial payment', () async {
// Arrange
const partialMontant = 2500.0;
final partialPaid = ContributionModel(
id: tContributionId,
membreId: 'membre1',
montant: 5000.0,
montantPaye: 2500.0,
dateEcheance: DateTime(2024, 12, 31),
datePaiement: tDatePaiement,
annee: 2024,
type: ContributionType.annuelle,
statut: ContributionStatus.partielle,
methodePaiement: PaymentMethod.especes,
);
when(mockRepository.enregistrerPaiement(
tContributionId,
montant: partialMontant,
datePaiement: tDatePaiement,
methodePaiement: 'ESPECES',
numeroPaiement: null,
referencePaiement: null,
)).thenAnswer((_) async => partialPaid);
// Act
final result = await useCase(
cotisationId: tContributionId,
montant: partialMontant,
datePaiement: tDatePaiement,
methodePaiement: 'ESPECES',
);
// Assert
expect(result.statut, equals(ContributionStatus.partielle));
expect(result.montantPaye, equals(2500.0));
});
test('should record payment with reference', () async {
// Arrange
const reference = 'REF-2024-001';
when(mockRepository.enregistrerPaiement(
tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
numeroPaiement: null,
referencePaiement: reference,
)).thenAnswer((_) async => tPaidContribution.copyWith(referencePaiement: reference));
// Act
final result = await useCase(
cotisationId: tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
referencePaiement: reference,
);
// Assert
expect(result.referencePaiement, equals(reference));
});
test('should throw exception when payment fails', () async {
// Arrange
when(mockRepository.enregistrerPaiement(
tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
numeroPaiement: null,
referencePaiement: null,
)).thenThrow(Exception('Erreur lors de l\'enregistrement du paiement'));
// Act & Assert
expect(
() => useCase(
cotisationId: tContributionId,
montant: tMontant,
datePaiement: tDatePaiement,
methodePaiement: tMethode,
),
throwsException,
);
});
});
}

View File

@@ -0,0 +1,105 @@
/// Tests unitaires pour UpdateContribution use case
library update_contribution_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/repositories/contribution_repository.dart';
import 'package:unionflow_mobile_apps/features/contributions/domain/usecases/update_contribution.dart';
import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart';
@GenerateMocks([IContributionRepository])
import 'update_contribution_test.mocks.dart';
void main() {
late UpdateContribution useCase;
late MockIContributionRepository mockRepository;
setUp(() {
mockRepository = MockIContributionRepository();
useCase = UpdateContribution(mockRepository);
});
group('UpdateContribution Use Case', () {
const tContributionId = 'cont123';
final tUpdatedContribution = ContributionModel(
id: tContributionId,
membreId: 'membre1',
montant: 6000.0,
dateEcheance: DateTime(2025, 12, 31),
annee: 2025,
type: ContributionType.annuelle,
statut: ContributionStatus.nonPayee,
description: 'Montant mis à jour',
);
test('should update contribution successfully', () async {
// Arrange
when(mockRepository.updateCotisation(tContributionId, tUpdatedContribution))
.thenAnswer((_) async => tUpdatedContribution);
// Act
final result = await useCase(tContributionId, tUpdatedContribution);
// Assert
expect(result, equals(tUpdatedContribution));
expect(result.montant, equals(6000.0));
expect(result.description, equals('Montant mis à jour'));
verify(mockRepository.updateCotisation(tContributionId, tUpdatedContribution));
verifyNoMoreInteractions(mockRepository);
});
test('should update contribution status', () async {
// Arrange
final statusUpdate = ContributionModel(
id: tContributionId,
membreId: 'membre1',
montant: 5000.0,
dateEcheance: DateTime(2024, 12, 31),
annee: 2024,
type: ContributionType.annuelle,
statut: ContributionStatus.enRetard,
);
when(mockRepository.updateCotisation(tContributionId, statusUpdate))
.thenAnswer((_) async => statusUpdate);
// Act
final result = await useCase(tContributionId, statusUpdate);
// Assert
expect(result.statut, equals(ContributionStatus.enRetard));
});
test('should update contribution type', () async {
// Arrange
final typeUpdate = ContributionModel(
id: tContributionId,
membreId: 'membre1',
montant: 5000.0,
dateEcheance: DateTime(2024, 3, 31),
annee: 2024,
trimestre: 1,
type: ContributionType.trimestrielle,
statut: ContributionStatus.nonPayee,
);
when(mockRepository.updateCotisation(tContributionId, typeUpdate))
.thenAnswer((_) async => typeUpdate);
// Act
final result = await useCase(tContributionId, typeUpdate);
// Assert
expect(result.type, equals(ContributionType.trimestrielle));
expect(result.trimestre, equals(1));
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateCotisation(any, any))
.thenThrow(Exception('Mise à jour échouée'));
// Act & Assert
expect(() => useCase(tContributionId, tUpdatedContribution), throwsException);
});
});
}

View File

@@ -0,0 +1,141 @@
/// Tests unitaires pour GetCompteAdherent use case
library get_compte_adherent_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/usecases/get_compte_adherent.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/repositories/dashboard_repository.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/entities/compte_adherent_entity.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
import 'package:unionflow_mobile_apps/core/usecases/usecase.dart';
@GenerateMocks([DashboardRepository])
import 'get_compte_adherent_test.mocks.dart';
void main() {
late GetCompteAdherent useCase;
late MockDashboardRepository mockRepository;
setUp(() {
mockRepository = MockDashboardRepository();
useCase = GetCompteAdherent(mockRepository);
});
group('GetCompteAdherent Use Case', () {
final tCompteAdherent = CompteAdherentEntity(
numeroMembre: 'M-2024-001',
nomComplet: 'Amadou Diallo',
organisationNom: 'Association Alpha',
dateAdhesion: DateTime(2024, 1, 15),
statutCompte: 'ACTIF',
soldeCotisations: 50000.0,
soldeEpargne: 125000.0,
soldeBloque: 15000.0,
soldeTotalDisponible: 160000.0,
encoursCreditTotal: 75000.0,
capaciteEmprunt: 200000.0,
nombreCotisationsPayees: 12,
nombreCotisationsTotal: 12,
nombreCotisationsEnRetard: 0,
engagementRate: 1.0,
nombreComptesEpargne: 2,
dateCalcul: DateTime(2024, 12, 15),
);
test('should return compte adherent successfully', () async {
// Arrange
when(mockRepository.getCompteAdherent())
.thenAnswer((_) async => Right(tCompteAdherent));
// Act
final result = await useCase(NoParams());
// Assert
expect(result, Right(tCompteAdherent));
result.fold(
(failure) => fail('Should not return failure'),
(compte) {
expect(compte.numeroMembre, equals('M-2024-001'));
expect(compte.nomComplet, equals('Amadou Diallo'));
expect(compte.soldeTotalDisponible, equals(160000.0));
expect(compte.capaciteEmprunt, equals(200000.0));
},
);
verify(mockRepository.getCompteAdherent());
verifyNoMoreInteractions(mockRepository);
});
test('should return compte with multiple epargne accounts', () async {
// Arrange
when(mockRepository.getCompteAdherent())
.thenAnswer((_) async => Right(tCompteAdherent));
// Act
final result = await useCase(NoParams());
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(compte) {
expect(compte.nombreComptesEpargne, equals(2));
expect(compte.soldeEpargne, equals(125000.0));
expect(compte.engagementRate, equals(1.0));
},
);
});
test('should return compte with overdue contributions', () async {
// Arrange
final compteWithOverdue = CompteAdherentEntity(
numeroMembre: 'M-2024-002',
nomComplet: 'Fatou Ndiaye',
statutCompte: 'ACTIF',
soldeCotisations: 25000.0,
soldeEpargne: 50000.0,
soldeBloque: 0.0,
soldeTotalDisponible: 75000.0,
encoursCreditTotal: 0.0,
capaciteEmprunt: 100000.0,
nombreCotisationsPayees: 8,
nombreCotisationsTotal: 12,
nombreCotisationsEnRetard: 4,
engagementRate: 0.67,
nombreComptesEpargne: 1,
dateCalcul: DateTime(2024, 12, 15),
);
when(mockRepository.getCompteAdherent())
.thenAnswer((_) async => Right(compteWithOverdue));
// Act
final result = await useCase(NoParams());
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(compte) {
expect(compte.nombreCotisationsEnRetard, equals(4));
expect(compte.engagementRate, lessThan(1.0));
},
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getCompteAdherent())
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(NoParams());
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(compte) => fail('Should not return compte'),
);
});
});
}

View File

@@ -0,0 +1,138 @@
/// Tests unitaires pour GetDashboardData use case
library get_dashboard_data_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/usecases/get_dashboard_data.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/repositories/dashboard_repository.dart';
import 'package:unionflow_mobile_apps/features/dashboard/domain/entities/dashboard_entity.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([DashboardRepository])
import 'get_dashboard_data_test.mocks.dart';
void main() {
late GetDashboardData useCase;
late MockDashboardRepository mockRepository;
setUp(() {
mockRepository = MockDashboardRepository();
useCase = GetDashboardData(mockRepository);
});
group('GetDashboardData Use Case', () {
const tOrgId = 'org-123';
const tUserId = 'user-456';
final tParams = GetDashboardDataParams(
organizationId: tOrgId,
userId: tUserId,
);
final tDashboardStats = DashboardStatsEntity(
totalMembers: 250,
activeMembers: 180,
totalEvents: 45,
upcomingEvents: 12,
totalContributions: 1200,
totalContributionAmount: 5750000.0,
contributionsAmountOnly: 3250000.0,
pendingRequests: 8,
completedProjects: 23,
monthlyGrowth: 0.15,
engagementRate: 0.72,
lastUpdated: DateTime(2024, 12, 15, 10, 30),
totalOrganizations: 5,
organizationTypeDistribution: {
'association': 3,
'cooperative': 2,
},
);
test('should return dashboard stats successfully', () async {
// Arrange
when(mockRepository.getDashboardStats(tOrgId, tUserId))
.thenAnswer((_) async => Right(tDashboardStats));
// Act
final result = await GetDashboardStats(mockRepository)(
GetDashboardStatsParams(organizationId: tOrgId, userId: tUserId),
);
// Assert
expect(result, Right(tDashboardStats));
result.fold(
(failure) => fail('Should not return failure'),
(stats) {
expect(stats.totalMembers, equals(250));
expect(stats.activeMembers, equals(180));
expect(stats.totalContributionAmount, equals(5750000.0));
expect(stats.monthlyGrowth, equals(0.15));
expect(stats.hasGrowth, isTrue);
},
);
verify(mockRepository.getDashboardStats(tOrgId, tUserId));
verifyNoMoreInteractions(mockRepository);
});
test('should return stats with high engagement rate', () async {
// Arrange
when(mockRepository.getDashboardStats(tOrgId, tUserId))
.thenAnswer((_) async => Right(tDashboardStats));
// Act
final result = await GetDashboardStats(mockRepository)(
GetDashboardStatsParams(organizationId: tOrgId, userId: tUserId),
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(stats) {
expect(stats.engagementRate, equals(0.72));
expect(stats.isHighEngagement, isTrue);
expect(stats.memberActivityRate, closeTo(0.72, 0.01));
},
);
});
test('should format contribution amount correctly', () async {
// Arrange
when(mockRepository.getDashboardStats(tOrgId, tUserId))
.thenAnswer((_) async => Right(tDashboardStats));
// Act
final result = await GetDashboardStats(mockRepository)(
GetDashboardStatsParams(organizationId: tOrgId, userId: tUserId),
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(stats) {
expect(stats.formattedContributionAmount, equals('5.8M'));
},
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getDashboardStats(any, any))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await GetDashboardStats(mockRepository)(
GetDashboardStatsParams(organizationId: tOrgId, userId: tUserId),
);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(stats) => fail('Should not return stats'),
);
});
});
}

View File

@@ -0,0 +1,65 @@
/// Tests unitaires pour CancelRegistration use case
library cancel_registration_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/cancel_registration.dart';
@GenerateMocks([IEvenementRepository])
import 'cancel_registration_test.mocks.dart';
void main() {
late CancelRegistration useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = CancelRegistration(mockRepository);
});
group('CancelRegistration Use Case', () {
const tEventId = 'event123';
test('should cancel registration successfully', () async {
// Arrange
when(mockRepository.desinscrireEvenement(tEventId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tEventId);
// Assert
verify(mockRepository.desinscrireEvenement(tEventId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when event not found', () async {
// Arrange
when(mockRepository.desinscrireEvenement(any))
.thenThrow(Exception('Événement non trouvé'));
// Act & Assert
expect(() => useCase('nonexistent'), throwsA(isA<Exception>()));
});
test('should throw exception when not registered', () async {
// Arrange
when(mockRepository.desinscrireEvenement(any))
.thenThrow(Exception('Vous n\'êtes pas inscrit à cet événement'));
// Act & Assert
expect(() => useCase(tEventId), throwsA(isA<Exception>()));
});
test('should throw exception when cancellation fails', () async {
// Arrange
when(mockRepository.desinscrireEvenement(any))
.thenThrow(Exception('Erreur de désinscription'));
// Act & Assert
expect(() => useCase(tEventId), throwsException);
});
});
}

View File

@@ -0,0 +1,118 @@
/// Tests unitaires pour CreateEvent use case
library create_event_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/create_event.dart';
import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart';
@GenerateMocks([IEvenementRepository])
import 'create_event_test.mocks.dart';
void main() {
late CreateEvent useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = CreateEvent(mockRepository);
});
group('CreateEvent Use Case', () {
final tNewEvent = EvenementModel(
titre: 'Nouvelle Réunion',
description: 'Réunion mensuelle du comité',
dateDebut: DateTime(2025, 1, 15, 10, 0),
dateFin: DateTime(2025, 1, 15, 12, 0),
lieu: 'Salle de réunion',
type: TypeEvenement.reunion,
statut: StatutEvenement.planifie,
);
final tCreatedEvent = EvenementModel(
id: 456,
titre: 'Nouvelle Réunion',
description: 'Réunion mensuelle du comité',
dateDebut: DateTime(2025, 1, 15, 10, 0),
dateFin: DateTime(2025, 1, 15, 12, 0),
lieu: 'Salle de réunion',
type: TypeEvenement.reunion,
statut: StatutEvenement.planifie,
);
test('should create event successfully', () async {
// Arrange
when(mockRepository.createEvenement(tNewEvent))
.thenAnswer((_) async => tCreatedEvent);
// Act
final result = await useCase(tNewEvent);
// Assert
expect(result, equals(tCreatedEvent));
expect(result.id, isNotNull);
expect(result.id, equals(456));
expect(result.titre, equals('Nouvelle Réunion'));
verify(mockRepository.createEvenement(tNewEvent));
verifyNoMoreInteractions(mockRepository);
});
test('should create public event with registration', () async {
// Arrange
final publicEvent = EvenementModel(
titre: 'Conférence Publique',
dateDebut: DateTime(2025, 2, 1, 14, 0),
dateFin: DateTime(2025, 2, 1, 17, 0),
type: TypeEvenement.conference,
statut: StatutEvenement.planifie,
estPublic: true,
inscriptionRequise: true,
maxParticipants: 200,
);
final createdPublic = publicEvent.copyWith(id: 789);
when(mockRepository.createEvenement(publicEvent))
.thenAnswer((_) async => createdPublic);
// Act
final result = await useCase(publicEvent);
// Assert
expect(result.estPublic, isTrue);
expect(result.inscriptionRequise, isTrue);
expect(result.maxParticipants, equals(200));
});
test('should create event with cost', () async {
// Arrange
final paidEvent = EvenementModel(
titre: 'Séminaire Payant',
dateDebut: DateTime(2025, 3, 1, 9, 0),
dateFin: DateTime(2025, 3, 1, 18, 0),
type: TypeEvenement.seminaire,
statut: StatutEvenement.planifie,
cout: 50000.0,
devise: 'XOF',
);
when(mockRepository.createEvenement(any))
.thenAnswer((_) async => paidEvent.copyWith(id: 999));
// Act
final result = await useCase(paidEvent);
// Assert
expect(result.cout, equals(50000.0));
expect(result.devise, equals('XOF'));
});
test('should throw exception when creation fails', () async {
// Arrange
when(mockRepository.createEvenement(any))
.thenThrow(Exception('Validation error'));
// Act & Assert
expect(() => useCase(tNewEvent), throwsException);
});
});
}

View File

@@ -0,0 +1,66 @@
/// Tests unitaires pour DeleteEvent use case
library delete_event_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/delete_event.dart';
@GenerateMocks([IEvenementRepository])
import 'delete_event_test.mocks.dart';
void main() {
late DeleteEvent useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = DeleteEvent(mockRepository);
});
group('DeleteEvent Use Case', () {
const tEventId = 'event123';
test('should delete event successfully', () async {
// Arrange
when(mockRepository.deleteEvenement(tEventId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tEventId);
// Assert
verify(mockRepository.deleteEvenement(tEventId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when event not found', () async {
// Arrange
when(mockRepository.deleteEvenement(any))
.thenThrow(Exception('Événement non trouvé'));
// Act & Assert
expect(() => useCase('nonexistent'), throwsA(isA<Exception>()));
verify(mockRepository.deleteEvenement('nonexistent'));
});
test('should throw exception when event has participants', () async {
// Arrange
when(mockRepository.deleteEvenement(any))
.thenThrow(Exception('Impossible de supprimer un événement avec des participants'));
// Act & Assert
expect(() => useCase(tEventId), throwsA(isA<Exception>()));
});
test('should throw exception when deletion fails', () async {
// Arrange
when(mockRepository.deleteEvenement(any))
.thenThrow(Exception('Erreur de suppression'));
// Act & Assert
expect(() => useCase(tEventId), throwsException);
});
});
}

View File

@@ -0,0 +1,95 @@
/// Tests unitaires pour GetEventById use case
library get_event_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_event_by_id.dart';
import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart';
@GenerateMocks([IEvenementRepository])
import 'get_event_by_id_test.mocks.dart';
void main() {
late GetEventById useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = GetEventById(mockRepository);
});
group('GetEventById Use Case', () {
const tEventId = 'event123';
final tEvent = EvenementModel(
id: 123,
titre: 'Assemblée Générale 2024',
description: 'Assemblée générale annuelle de l\'organisation',
dateDebut: DateTime(2024, 12, 15, 14, 0),
dateFin: DateTime(2024, 12, 15, 18, 0),
lieu: 'Salle des Congrès',
adresse: '123 Rue de la République',
ville: 'Dakar',
type: TypeEvenement.assembleeGenerale,
statut: StatutEvenement.confirme,
priorite: PrioriteEvenement.haute,
participantsActuels: 45,
maxParticipants: 100,
estPublic: true,
inscriptionRequise: true,
);
test('should return event by id', () async {
// Arrange
when(mockRepository.getEvenementById(tEventId))
.thenAnswer((_) async => tEvent);
// Act
final result = await useCase(tEventId);
// Assert
expect(result, equals(tEvent));
expect(result?.id, equals(123));
expect(result?.titre, equals('Assemblée Générale 2024'));
verify(mockRepository.getEvenementById(tEventId));
verifyNoMoreInteractions(mockRepository);
});
test('should return event with all details populated', () async {
// Arrange
when(mockRepository.getEvenementById(tEventId))
.thenAnswer((_) async => tEvent);
// Act
final result = await useCase(tEventId);
// Assert
expect(result?.lieu, isNotNull);
expect(result?.adresse, isNotNull);
expect(result?.participantsActuels, equals(45));
expect(result?.maxParticipants, equals(100));
});
test('should return null when event not found', () async {
// Arrange
when(mockRepository.getEvenementById(any))
.thenAnswer((_) async => null);
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, isNull);
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getEvenementById(any))
.thenThrow(Exception('Database error'));
// Act & Assert
expect(() => useCase(tEventId), throwsException);
});
});
}

View File

@@ -0,0 +1,102 @@
/// Tests unitaires pour GetEventParticipants use case
library get_event_participants_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_event_participants.dart';
@GenerateMocks([IEvenementRepository])
import 'get_event_participants_test.mocks.dart';
void main() {
late GetEventParticipants useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = GetEventParticipants(mockRepository);
});
group('GetEventParticipants Use Case', () {
const tEventId = 'event123';
final tParticipantsList = [
{
'id': 'membre1',
'nom': 'Dupont',
'prenom': 'Jean',
'email': 'jean.dupont@example.com',
'dateInscription': '2024-11-01T10:00:00Z',
'statut': 'CONFIRME',
},
{
'id': 'membre2',
'nom': 'Martin',
'prenom': 'Marie',
'email': 'marie.martin@example.com',
'dateInscription': '2024-11-02T14:00:00Z',
'statut': 'EN_ATTENTE',
},
{
'id': 'membre3',
'nom': 'Diallo',
'prenom': 'Amadou',
'email': 'amadou.diallo@example.com',
'dateInscription': '2024-11-03T09:00:00Z',
'statut': 'CONFIRME',
},
];
test('should return list of event participants', () async {
// Arrange
when(mockRepository.getParticipants(tEventId))
.thenAnswer((_) async => tParticipantsList);
// Act
final result = await useCase(tEventId);
// Assert
expect(result, equals(tParticipantsList));
expect(result.length, equals(3));
expect(result[0]['nom'], equals('Dupont'));
expect(result[0]['statut'], equals('CONFIRME'));
verify(mockRepository.getParticipants(tEventId));
verifyNoMoreInteractions(mockRepository);
});
test('should return participants with different statuses', () async {
// Arrange
when(mockRepository.getParticipants(tEventId))
.thenAnswer((_) async => tParticipantsList);
// Act
final result = await useCase(tEventId);
// Assert
expect(result.where((p) => p['statut'] == 'CONFIRME').length, equals(2));
expect(result.where((p) => p['statut'] == 'EN_ATTENTE').length, equals(1));
});
test('should return empty list when no participants', () async {
// Arrange
when(mockRepository.getParticipants(tEventId))
.thenAnswer((_) async => []);
// Act
final result = await useCase(tEventId);
// Assert
expect(result, isEmpty);
});
test('should throw exception when event not found', () async {
// Arrange
when(mockRepository.getParticipants(any))
.thenThrow(Exception('Événement non trouvé'));
// Act & Assert
expect(() => useCase('nonexistent'), throwsException);
});
});
}

View File

@@ -0,0 +1,138 @@
/// Tests unitaires pour GetEvents use case
library get_events_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_events.dart';
import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart';
import 'package:unionflow_mobile_apps/features/events/data/repositories/evenement_repository_impl.dart';
@GenerateMocks([IEvenementRepository])
import 'get_events_test.mocks.dart';
void main() {
late GetEvents useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = GetEvents(mockRepository);
});
group('GetEvents Use Case', () {
final tEventsList = [
EvenementModel(
id: 1,
titre: 'Assemblée Générale 2024',
description: 'Assemblée générale annuelle',
dateDebut: DateTime(2024, 12, 15, 14, 0),
dateFin: DateTime(2024, 12, 15, 18, 0),
lieu: 'Salle des Congrès',
type: TypeEvenement.assembleeGenerale,
statut: StatutEvenement.confirme,
priorite: PrioriteEvenement.haute,
participantsActuels: 45,
maxParticipants: 100,
),
EvenementModel(
id: 2,
titre: 'Formation Leadership',
description: 'Formation sur le leadership',
dateDebut: DateTime(2024, 12, 20, 9, 0),
dateFin: DateTime(2024, 12, 20, 17, 0),
lieu: 'Centre de Formation',
type: TypeEvenement.formation,
statut: StatutEvenement.planifie,
participantsActuels: 15,
maxParticipants: 30,
),
];
final tSearchResult = EvenementSearchResult(
evenements: tEventsList,
total: 2,
page: 0,
size: 20,
totalPages: 1,
);
test('should return paginated list of events', () async {
// Arrange
when(mockRepository.getEvenements(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result, equals(tSearchResult));
expect(result.evenements.length, equals(2));
expect(result.total, equals(2));
verify(mockRepository.getEvenements(page: 0, size: 20));
verifyNoMoreInteractions(mockRepository);
});
test('should filter events by search query', () async {
// Arrange
final filteredResult = EvenementSearchResult(
evenements: [tEventsList[0]],
total: 1,
page: 0,
size: 20,
totalPages: 1,
);
when(mockRepository.getEvenements(
page: 0,
size: 20,
recherche: 'Assemblée',
)).thenAnswer((_) async => filteredResult);
// Act
final result = await useCase(page: 0, size: 20, recherche: 'Assemblée');
// Assert
expect(result.evenements.length, equals(1));
expect(result.evenements.first.titre, contains('Assemblée'));
});
test('should return empty result when no events exist', () async {
// Arrange
final emptyResult = EvenementSearchResult(
evenements: [],
total: 0,
page: 0,
size: 20,
totalPages: 0,
);
when(mockRepository.getEvenements(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => emptyResult);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result.evenements, isEmpty);
expect(result.total, equals(0));
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getEvenements(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(page: 0, size: 20), throwsException);
});
});
}

View File

@@ -0,0 +1,121 @@
/// Tests unitaires pour GetMyRegistrations use case
library get_my_registrations_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/get_my_registrations.dart';
import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart';
import 'package:unionflow_mobile_apps/features/events/data/repositories/evenement_repository_impl.dart';
@GenerateMocks([IEvenementRepository])
import 'get_my_registrations_test.mocks.dart';
void main() {
late GetMyRegistrations useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = GetMyRegistrations(mockRepository);
});
group('GetMyRegistrations Use Case', () {
final tRegisteredEvents = [
EvenementModel(
id: 1,
titre: 'Formation Leadership',
dateDebut: DateTime(2024, 12, 20, 9, 0),
dateFin: DateTime(2024, 12, 20, 17, 0),
type: TypeEvenement.formation,
statut: StatutEvenement.confirme,
inscriptionRequise: true,
participantsActuels: 15,
),
EvenementModel(
id: 2,
titre: 'Séminaire Annuel',
dateDebut: DateTime(2025, 1, 10, 14, 0),
dateFin: DateTime(2025, 1, 10, 18, 0),
type: TypeEvenement.seminaire,
statut: StatutEvenement.planifie,
inscriptionRequise: true,
participantsActuels: 30,
),
];
final tSearchResult = EvenementSearchResult(
evenements: tRegisteredEvents,
total: 2,
page: 0,
size: 20,
totalPages: 1,
);
test('should return list of registered events', () async {
// Arrange
when(mockRepository.getEvenementsAVenir(page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result, equals(tSearchResult));
expect(result.evenements.length, equals(2));
expect(result.total, equals(2));
verify(mockRepository.getEvenementsAVenir(page: 0, size: 20));
verifyNoMoreInteractions(mockRepository);
});
test('should return events with custom page size', () async {
// Arrange
final smallResult = EvenementSearchResult(
evenements: [tRegisteredEvents[0]],
total: 2,
page: 0,
size: 1,
totalPages: 2,
);
when(mockRepository.getEvenementsAVenir(page: 0, size: 1))
.thenAnswer((_) async => smallResult);
// Act
final result = await useCase(page: 0, size: 1);
// Assert
expect(result.evenements.length, equals(1));
expect(result.size, equals(1));
});
test('should return empty result when no registrations', () async {
// Arrange
final emptyResult = EvenementSearchResult(
evenements: [],
total: 0,
page: 0,
size: 20,
totalPages: 0,
);
when(mockRepository.getEvenementsAVenir(page: anyNamed('page'), size: anyNamed('size')))
.thenAnswer((_) async => emptyResult);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result.evenements, isEmpty);
expect(result.total, equals(0));
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getEvenementsAVenir(page: anyNamed('page'), size: anyNamed('size')))
.thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(page: 0, size: 20), throwsException);
});
});
}

View File

@@ -0,0 +1,65 @@
/// Tests unitaires pour RegisterForEvent use case
library register_for_event_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/register_for_event.dart';
@GenerateMocks([IEvenementRepository])
import 'register_for_event_test.mocks.dart';
void main() {
late RegisterForEvent useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = RegisterForEvent(mockRepository);
});
group('RegisterForEvent Use Case', () {
const tEventId = 'event123';
test('should register for event successfully', () async {
// Arrange
when(mockRepository.inscrireEvenement(tEventId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tEventId);
// Assert
verify(mockRepository.inscrireEvenement(tEventId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when event not found', () async {
// Arrange
when(mockRepository.inscrireEvenement(any))
.thenThrow(Exception('Événement non trouvé'));
// Act & Assert
expect(() => useCase('nonexistent'), throwsA(isA<Exception>()));
});
test('should throw exception when already registered', () async {
// Arrange
when(mockRepository.inscrireEvenement(any))
.thenThrow(Exception('Vous êtes déjà inscrit à cet événement'));
// Act & Assert
expect(() => useCase(tEventId), throwsA(isA<Exception>()));
});
test('should throw exception when event is full', () async {
// Arrange
when(mockRepository.inscrireEvenement(any))
.thenThrow(Exception('Événement complet'));
// Act & Assert
expect(() => useCase(tEventId), throwsException);
});
});
}

View File

@@ -0,0 +1,67 @@
/// Tests unitaires pour SubmitEventFeedback use case
library submit_event_feedback_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/submit_event_feedback.dart';
@GenerateMocks([IEvenementRepository])
import 'submit_event_feedback_test.mocks.dart';
void main() {
late SubmitEventFeedback useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = SubmitEventFeedback(mockRepository);
});
group('SubmitEventFeedback Use Case', () {
const tEventId = 'event123';
const tNote = 5;
const tCommentaire = 'Excellent événement, très enrichissant!';
test('should throw UnimplementedError as endpoint not available', () async {
// Act & Assert
expect(
() => useCase(evenementId: tEventId, note: tNote),
throwsA(isA<UnimplementedError>()),
);
});
test('should throw UnimplementedError with feedback message', () async {
// Act & Assert
expect(
() => useCase(
evenementId: tEventId,
note: tNote,
commentaire: tCommentaire,
),
throwsA(isA<UnimplementedError>()),
);
});
test('should throw UnimplementedError for minimum rating', () async {
// Act & Assert
expect(
() => useCase(evenementId: tEventId, note: 1),
throwsA(isA<UnimplementedError>()),
);
});
test('should throw UnimplementedError for maximum rating', () async {
// Act & Assert
expect(
() => useCase(
evenementId: tEventId,
note: 5,
commentaire: 'Parfait!',
),
throwsA(isA<UnimplementedError>()),
);
});
});
}

View File

@@ -0,0 +1,104 @@
/// Tests unitaires pour UpdateEvent use case
library update_event_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/events/domain/repositories/evenement_repository.dart';
import 'package:unionflow_mobile_apps/features/events/domain/usecases/update_event.dart';
import 'package:unionflow_mobile_apps/features/events/data/models/evenement_model.dart';
@GenerateMocks([IEvenementRepository])
import 'update_event_test.mocks.dart';
void main() {
late UpdateEvent useCase;
late MockIEvenementRepository mockRepository;
setUp(() {
mockRepository = MockIEvenementRepository();
useCase = UpdateEvent(mockRepository);
});
group('UpdateEvent Use Case', () {
const tEventId = 'event123';
final tUpdatedEvent = EvenementModel(
id: 123,
titre: 'Assemblée Générale 2024 - Modifiée',
description: 'Assemblée générale annuelle (mise à jour)',
dateDebut: DateTime(2024, 12, 16, 14, 0),
dateFin: DateTime(2024, 12, 16, 18, 0),
lieu: 'Nouveau lieu',
type: TypeEvenement.assembleeGenerale,
statut: StatutEvenement.confirme,
);
test('should update event successfully', () async {
// Arrange
when(mockRepository.updateEvenement(tEventId, tUpdatedEvent))
.thenAnswer((_) async => tUpdatedEvent);
// Act
final result = await useCase(tEventId, tUpdatedEvent);
// Assert
expect(result, equals(tUpdatedEvent));
expect(result.titre, contains('Modifiée'));
expect(result.lieu, equals('Nouveau lieu'));
verify(mockRepository.updateEvenement(tEventId, tUpdatedEvent));
verifyNoMoreInteractions(mockRepository);
});
test('should update event status', () async {
// Arrange
final statusUpdate = EvenementModel(
id: 123,
titre: 'Événement',
dateDebut: DateTime(2024, 12, 15, 14, 0),
dateFin: DateTime(2024, 12, 15, 18, 0),
type: TypeEvenement.reunion,
statut: StatutEvenement.annule,
);
when(mockRepository.updateEvenement(tEventId, statusUpdate))
.thenAnswer((_) async => statusUpdate);
// Act
final result = await useCase(tEventId, statusUpdate);
// Assert
expect(result.statut, equals(StatutEvenement.annule));
});
test('should update event capacity', () async {
// Arrange
final capacityUpdate = EvenementModel(
id: 123,
titre: 'Événement',
dateDebut: DateTime(2024, 12, 15, 14, 0),
dateFin: DateTime(2024, 12, 15, 18, 0),
type: TypeEvenement.formation,
statut: StatutEvenement.planifie,
maxParticipants: 50,
participantsActuels: 25,
);
when(mockRepository.updateEvenement(tEventId, capacityUpdate))
.thenAnswer((_) async => capacityUpdate);
// Act
final result = await useCase(tEventId, capacityUpdate);
// Assert
expect(result.maxParticipants, equals(50));
expect(result.participantsActuels, equals(25));
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateEvenement(any, any))
.thenThrow(Exception('Update failed'));
// Act & Assert
expect(() => useCase(tEventId, tUpdatedEvent), throwsException);
});
});
}

View File

@@ -0,0 +1,119 @@
/// Tests unitaires pour ApproveTransaction use case
library approve_transaction_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/approve_transaction.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'approve_transaction_test.mocks.dart';
void main() {
late ApproveTransaction useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = ApproveTransaction(mockRepository);
});
group('ApproveTransaction Use Case', () {
const tApprovalId = 'approval-123';
const tComment = 'Approuvé - Montant conforme au budget';
final tApprovedTransaction = TransactionApproval(
id: tApprovalId,
transactionId: 'tx-123',
transactionType: TransactionType.withdrawal,
amount: 500000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
requiredLevel: ApprovalLevel.level1,
status: ApprovalStatus.approved,
approvers: [],
createdAt: DateTime(2024, 12, 15),
);
test('should approve transaction successfully', () async {
// Arrange
when(mockRepository.approveTransaction(
approvalId: tApprovalId,
comment: tComment,
)).thenAnswer((_) async => Right(tApprovedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId, comment: tComment);
// Assert
expect(result, Right(tApprovedTransaction));
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.id, equals(tApprovalId));
expect(approval.status, equals(ApprovalStatus.approved));
},
);
verify(mockRepository.approveTransaction(
approvalId: tApprovalId,
comment: tComment,
));
verifyNoMoreInteractions(mockRepository);
});
test('should approve transaction without comment', () async {
// Arrange
when(mockRepository.approveTransaction(
approvalId: tApprovalId,
comment: null,
)).thenAnswer((_) async => Right(tApprovedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approval) => expect(approval.status, equals(ApprovalStatus.approved)),
);
});
test('should return ValidationFailure when approvalId is empty', () async {
// Act
final result = await useCase(approvalId: '');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('ID approbation requis'));
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Transaction déjà approuvée');
when(mockRepository.approveTransaction(
approvalId: anyNamed('approvalId'),
comment: anyNamed('comment'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(approvalId: tApprovalId);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(approval) => fail('Should not return approval'),
);
});
});
}

View File

@@ -0,0 +1,221 @@
/// Tests unitaires pour CreateBudget use case
library create_budget_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/create_budget.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'create_budget_test.mocks.dart';
void main() {
late CreateBudget useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = CreateBudget(mockRepository);
});
group('CreateBudget Use Case', () {
const tName = 'Budget 2025';
const tOrgId = 'org-123';
final tBudgetLines = [
BudgetLine(
id: 'line-1',
category: BudgetCategory.contributions,
name: 'Cotisations mensuelles',
description: 'Revenus des cotisations',
amountPlanned: 3000000.0,
),
BudgetLine(
id: 'line-2',
category: BudgetCategory.savings,
name: 'Dépôts épargne',
description: 'Collecte épargne',
amountPlanned: 2000000.0,
),
BudgetLine(
id: 'line-3',
category: BudgetCategory.solidarity,
name: 'Aide mutuelle',
description: 'Soutien membres',
amountPlanned: 1000000.0,
),
];
final tCreatedBudget = Budget(
id: 'budget-new',
name: tName,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
status: BudgetStatus.draft,
lines: tBudgetLines,
totalPlanned: 6000000.0,
totalRealized: 0.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime.now(),
startDate: DateTime(2025, 1, 1),
endDate: DateTime(2025, 12, 31),
);
test('should create budget successfully', () async {
// Arrange
when(mockRepository.createBudget(
name: tName,
description: anyNamed('description'),
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
month: anyNamed('month'),
lines: tBudgetLines,
)).thenAnswer((_) async => Right(tCreatedBudget));
// Act
final result = await useCase(
name: tName,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
lines: tBudgetLines,
);
// Assert
expect(result, Right(tCreatedBudget));
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.id, equals('budget-new'));
expect(budget.name, equals(tName));
expect(budget.status, equals(BudgetStatus.draft));
},
);
verify(mockRepository.createBudget(
name: tName,
description: null,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
month: null,
lines: tBudgetLines,
));
verifyNoMoreInteractions(mockRepository);
});
test('should create monthly budget with description', () async {
// Arrange
const description = 'Budget opérationnel janvier 2025';
final monthlyLines = [
BudgetLine(
id: 'line-monthly',
category: BudgetCategory.contributions,
name: 'Cotisations janvier',
amountPlanned: 500000.0,
),
];
final monthlyBudget = Budget(
id: 'budget-monthly',
name: 'Budget Janvier 2025',
description: description,
organizationId: tOrgId,
period: BudgetPeriod.monthly,
year: 2025,
month: 1,
status: BudgetStatus.draft,
lines: monthlyLines,
totalPlanned: 500000.0,
totalRealized: 0.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime.now(),
startDate: DateTime(2025, 1, 1),
endDate: DateTime(2025, 1, 31),
);
when(mockRepository.createBudget(
name: 'Budget Janvier 2025',
description: description,
organizationId: tOrgId,
period: BudgetPeriod.monthly,
year: 2025,
month: 1,
lines: monthlyLines,
)).thenAnswer((_) async => Right(monthlyBudget));
// Act
final result = await useCase(
name: 'Budget Janvier 2025',
description: description,
organizationId: tOrgId,
period: BudgetPeriod.monthly,
year: 2025,
month: 1,
lines: monthlyLines,
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.period, equals(BudgetPeriod.monthly));
expect(budget.month, equals(1));
},
);
});
test('should return ValidationFailure when name is empty', () async {
// Act
final result = await useCase(
name: '',
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
lines: [BudgetLine(id: 'test-1', category: BudgetCategory.operational, name: 'Test line', amountPlanned: 100.0)],
);
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(budget) => fail('Should not return budget'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur création budget');
when(mockRepository.createBudget(
name: anyNamed('name'),
description: anyNamed('description'),
organizationId: anyNamed('organizationId'),
period: anyNamed('period'),
year: anyNamed('year'),
month: anyNamed('month'),
lines: anyNamed('lines'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(
name: tName,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
lines: [BudgetLine(id: 'test-1', category: BudgetCategory.operational, name: 'Test line', amountPlanned: 100.0)],
);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(budget) => fail('Should not return budget'),
);
});
});
}

View File

@@ -0,0 +1,112 @@
/// Tests unitaires pour GetApprovalById use case
library get_approval_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_approval_by_id.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_approval_by_id_test.mocks.dart';
void main() {
late GetApprovalById useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetApprovalById(mockRepository);
});
group('GetApprovalById Use Case', () {
const tApprovalId = 'approval-123';
final tApproval = TransactionApproval(
id: tApprovalId,
transactionId: 'tx-456',
transactionType: TransactionType.solidarity,
amount: 350000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
organizationId: 'org-123',
requiredLevel: ApprovalLevel.level2,
status: ApprovalStatus.pending,
approvers: [],
createdAt: DateTime(2024, 12, 15),
);
test('should return approval details by ID', () async {
// Arrange
when(mockRepository.getApprovalById(tApprovalId))
.thenAnswer((_) async => Right(tApproval));
// Act
final result = await useCase(tApprovalId);
// Assert
expect(result, Right(tApproval));
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.id, equals(tApprovalId));
expect(approval.amount, equals(350000.0));
expect(approval.transactionType, equals(TransactionType.solidarity));
},
);
verify(mockRepository.getApprovalById(tApprovalId));
verifyNoMoreInteractions(mockRepository);
});
test('should return approval with level 2 requirement', () async {
// Arrange
when(mockRepository.getApprovalById(tApprovalId))
.thenAnswer((_) async => Right(tApproval));
// Act
final result = await useCase(tApprovalId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.requiredLevel, equals(ApprovalLevel.level2));
},
);
});
test('should return ValidationFailure when approvalId is empty', () async {
// Act
final result = await useCase('');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when approval not found', () async {
// Arrange
final tFailure = ServerFailure('Approbation non trouvée');
when(mockRepository.getApprovalById(any))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(approval) => fail('Should not return approval'),
);
});
});
}

View File

@@ -0,0 +1,116 @@
/// Tests unitaires pour GetBudgetById use case
library get_budget_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_by_id.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_budget_by_id_test.mocks.dart';
void main() {
late GetBudgetById useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetBudgetById(mockRepository);
});
group('GetBudgetById Use Case', () {
const tBudgetId = 'budget-123';
final tBudget = Budget(
id: tBudgetId,
name: 'Budget Annuel 2024',
description: 'Budget prévisionnel pour l\'année 2024',
organizationId: 'org-123',
period: BudgetPeriod.annual,
year: 2024,
status: BudgetStatus.active,
lines: [],
totalPlanned: 5000000.0,
totalRealized: 3250000.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime(2024, 1, 1),
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 12, 31),
);
test('should return budget details by ID', () async {
// Arrange
when(mockRepository.getBudgetById(tBudgetId))
.thenAnswer((_) async => Right(tBudget));
// Act
final result = await useCase(tBudgetId);
// Assert
expect(result, Right(tBudget));
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.id, equals(tBudgetId));
expect(budget.name, equals('Budget Annuel 2024'));
expect(budget.totalPlanned, equals(5000000.0));
},
);
verify(mockRepository.getBudgetById(tBudgetId));
verifyNoMoreInteractions(mockRepository);
});
test('should return budget with realized amount', () async {
// Arrange
when(mockRepository.getBudgetById(tBudgetId))
.thenAnswer((_) async => Right(tBudget));
// Act
final result = await useCase(tBudgetId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.totalRealized, equals(3250000.0));
expect(budget.totalRealized, lessThan(budget.totalPlanned));
},
);
});
test('should return ValidationFailure when budgetId is empty', () async {
// Act
final result = await useCase('');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(budget) => fail('Should not return budget'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when budget not found', () async {
// Arrange
final tFailure = ServerFailure('Budget non trouvé');
when(mockRepository.getBudgetById(any))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(budget) => fail('Should not return budget'),
);
});
});
}

View File

@@ -0,0 +1,114 @@
/// Tests unitaires pour GetBudgetTracking use case
library get_budget_tracking_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_tracking.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_budget_tracking_test.mocks.dart';
void main() {
late GetBudgetTracking useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetBudgetTracking(mockRepository);
});
group('GetBudgetTracking Use Case', () {
const tBudgetId = 'budget-123';
final tTrackingData = {
'budgetId': tBudgetId,
'totalPlanned': 5000000.0,
'totalRealized': 3250000.0,
'remainingAmount': 1750000.0,
'realizationRate': 0.65,
'categories': {
'contributions': {'planned': 2000000.0, 'realized': 1800000.0, 'rate': 0.9},
'savings': {'planned': 1500000.0, 'realized': 950000.0, 'rate': 0.63},
'solidarity': {'planned': 1000000.0, 'realized': 350000.0, 'rate': 0.35},
'events': {'planned': 500000.0, 'realized': 150000.0, 'rate': 0.3},
},
};
test('should return budget tracking data successfully', () async {
// Arrange
when(mockRepository.getBudgetTracking(budgetId: tBudgetId))
.thenAnswer((_) async => Right(tTrackingData));
// Act
final result = await useCase(budgetId: tBudgetId);
// Assert
expect(result, Right(tTrackingData));
result.fold(
(failure) => fail('Should not return failure'),
(tracking) {
expect(tracking['budgetId'], equals(tBudgetId));
expect(tracking['totalPlanned'], equals(5000000.0));
expect(tracking['realizationRate'], equals(0.65));
},
);
verify(mockRepository.getBudgetTracking(budgetId: tBudgetId));
verifyNoMoreInteractions(mockRepository);
});
test('should return tracking with category breakdown', () async {
// Arrange
when(mockRepository.getBudgetTracking(budgetId: tBudgetId))
.thenAnswer((_) async => Right(tTrackingData));
// Act
final result = await useCase(budgetId: tBudgetId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(tracking) {
final categories = tracking['categories'] as Map<String, dynamic>;
expect(categories.keys, contains('contributions'));
expect(categories.keys, contains('solidarity'));
final contribs = categories['contributions'] as Map<String, dynamic>;
expect(contribs['rate'], equals(0.9));
},
);
});
test('should return ValidationFailure when budgetId is empty', () async {
// Act
final result = await useCase(budgetId: '');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(tracking) => fail('Should not return tracking'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur suivi budget');
when(mockRepository.getBudgetTracking(budgetId: anyNamed('budgetId')))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(budgetId: tBudgetId);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(tracking) => fail('Should not return tracking'),
);
});
});
}

View File

@@ -0,0 +1,155 @@
/// Tests unitaires pour GetBudgets use case
library get_budgets_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budgets.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_budgets_test.mocks.dart';
void main() {
late GetBudgets useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetBudgets(mockRepository);
});
group('GetBudgets Use Case', () {
final tBudgets = [
Budget(
id: 'budget-1',
name: 'Budget Annuel 2024',
organizationId: 'org-123',
period: BudgetPeriod.annual,
year: 2024,
status: BudgetStatus.active,
lines: [],
totalPlanned: 5000000.0,
totalRealized: 3250000.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime(2024, 1, 1),
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 12, 31),
),
Budget(
id: 'budget-2',
name: 'Budget Q4 2024',
organizationId: 'org-123',
period: BudgetPeriod.quarterly,
year: 2024,
month: 10,
status: BudgetStatus.active,
lines: [],
totalPlanned: 1250000.0,
totalRealized: 850000.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime(2024, 10, 1),
startDate: DateTime(2024, 10, 1),
endDate: DateTime(2024, 12, 31),
),
];
test('should return list of budgets successfully', () async {
// Arrange
when(mockRepository.getBudgets(
organizationId: anyNamed('organizationId'),
status: anyNamed('status'),
year: anyNamed('year'),
)).thenAnswer((_) async => Right(tBudgets));
// Act
final result = await useCase(organizationId: 'org-123');
// Assert
expect(result, Right(tBudgets));
result.fold(
(failure) => fail('Should not return failure'),
(budgets) {
expect(budgets.length, equals(2));
expect(budgets[0].name, equals('Budget Annuel 2024'));
expect(budgets[0].totalPlanned, equals(5000000.0));
},
);
verify(mockRepository.getBudgets(
organizationId: 'org-123',
status: null,
year: null,
));
verifyNoMoreInteractions(mockRepository);
});
test('should filter budgets by status and year', () async {
// Arrange
final activeBudgets = [tBudgets[0], tBudgets[1]];
when(mockRepository.getBudgets(
organizationId: 'org-123',
status: BudgetStatus.active,
year: 2024,
)).thenAnswer((_) async => Right(activeBudgets));
// Act
final result = await useCase(
organizationId: 'org-123',
status: BudgetStatus.active,
year: 2024,
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budgets) {
expect(budgets.every((b) => b.status == BudgetStatus.active), isTrue);
expect(budgets.every((b) => b.year == 2024), isTrue);
},
);
});
test('should return empty list when no budgets exist', () async {
// Arrange
when(mockRepository.getBudgets(
organizationId: anyNamed('organizationId'),
status: anyNamed('status'),
year: anyNamed('year'),
)).thenAnswer((_) async => Right([]));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budgets) => expect(budgets, isEmpty),
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getBudgets(
organizationId: anyNamed('organizationId'),
status: anyNamed('status'),
year: anyNamed('year'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase();
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(budgets) => fail('Should not return budgets'),
);
});
});
}

View File

@@ -0,0 +1,132 @@
/// Tests unitaires pour GetPendingApprovals use case
library get_pending_approvals_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_pending_approvals.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_pending_approvals_test.mocks.dart';
void main() {
late GetPendingApprovals useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetPendingApprovals(mockRepository);
});
group('GetPendingApprovals Use Case', () {
final tApprovals = [
TransactionApproval(
id: 'approval-1',
transactionId: 'tx-123',
transactionType: TransactionType.withdrawal,
amount: 500000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
organizationId: 'org-123',
requiredLevel: ApprovalLevel.level2,
status: ApprovalStatus.pending,
approvers: [],
createdAt: DateTime(2024, 12, 15),
),
TransactionApproval(
id: 'approval-2',
transactionId: 'tx-456',
transactionType: TransactionType.solidarity,
amount: 200000.0,
currency: 'XOF',
requesterId: 'user-2',
requesterName: 'Fatou Ndiaye',
requiredLevel: ApprovalLevel.level1,
status: ApprovalStatus.pending,
approvers: [],
createdAt: DateTime(2024, 12, 14),
),
];
test('should return list of pending approvals successfully', () async {
// Arrange
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Right(tApprovals));
// Act
final result = await useCase(organizationId: 'org-123');
// Assert
expect(result, Right(tApprovals));
result.fold(
(failure) => fail('Should not return failure'),
(approvals) {
expect(approvals.length, equals(2));
expect(approvals[0].status, equals(ApprovalStatus.pending));
expect(approvals[0].amount, equals(500000.0));
},
);
verify(mockRepository.getPendingApprovals(organizationId: 'org-123'));
verifyNoMoreInteractions(mockRepository);
});
test('should return approvals with different levels', () async {
// Arrange
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Right(tApprovals));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approvals) {
expect(approvals.any((a) => a.requiredLevel == ApprovalLevel.level2), isTrue);
expect(approvals.any((a) => a.requiredLevel == ApprovalLevel.level1), isTrue);
},
);
});
test('should return empty list when no pending approvals', () async {
// Arrange
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Right([]));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approvals) => expect(approvals, isEmpty),
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase();
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(approvals) => fail('Should not return approvals'),
);
});
});
}

View File

@@ -0,0 +1,116 @@
/// Tests unitaires pour RejectTransaction use case
library reject_transaction_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/reject_transaction.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'reject_transaction_test.mocks.dart';
void main() {
late RejectTransaction useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = RejectTransaction(mockRepository);
});
group('RejectTransaction Use Case', () {
const tApprovalId = 'approval-123';
const tReason = 'Montant trop élevé - Budget insuffisant';
final tRejectedTransaction = TransactionApproval(
id: tApprovalId,
transactionId: 'tx-123',
transactionType: TransactionType.withdrawal,
amount: 500000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
requiredLevel: ApprovalLevel.level2,
status: ApprovalStatus.rejected,
approvers: [],
createdAt: DateTime(2024, 12, 15),
);
test('should reject transaction successfully', () async {
// Arrange
when(mockRepository.rejectTransaction(
approvalId: tApprovalId,
reason: tReason,
)).thenAnswer((_) async => Right(tRejectedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId, reason: tReason);
// Assert
expect(result, Right(tRejectedTransaction));
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.id, equals(tApprovalId));
expect(approval.status, equals(ApprovalStatus.rejected));
},
);
verify(mockRepository.rejectTransaction(
approvalId: tApprovalId,
reason: tReason,
));
verifyNoMoreInteractions(mockRepository);
});
test('should reject transaction with detailed reason', () async {
// Arrange
const detailedReason = 'Refus: Documentation incomplète + montant non justifié';
when(mockRepository.rejectTransaction(
approvalId: tApprovalId,
reason: detailedReason,
)).thenAnswer((_) async => Right(tRejectedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId, reason: detailedReason);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approval) => expect(approval.status, equals(ApprovalStatus.rejected)),
);
});
test('should return ValidationFailure when approvalId is empty', () async {
// Act
final result = await useCase(approvalId: '', reason: tReason);
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('ID approbation requis'));
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ValidationFailure when reason is empty', () async {
// Act
final result = await useCase(approvalId: tApprovalId, reason: ' ');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('Raison du rejet requise'));
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
});
}

View File

@@ -0,0 +1,117 @@
/// Tests unitaires pour CreateMember use case
library create_member_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/create_member.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
@GenerateMocks([IMembreRepository])
import 'create_member_test.mocks.dart';
void main() {
late CreateMember useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = CreateMember(mockRepository);
});
group('CreateMember Use Case', () {
final tMemberData = MembreCompletModel(
nom: 'Ndiaye',
prenom: 'Fatou',
email: 'fatou.ndiaye@example.com',
telephone: '+221776543210',
genre: Genre.femme,
statut: StatutMembre.enAttente,
dateNaissance: DateTime(1990, 7, 22),
adresse: '45 Rue de la République, Dakar',
);
final tCreatedMember = MembreCompletModel(
id: '456',
nom: 'Ndiaye',
prenom: 'Fatou',
email: 'fatou.ndiaye@example.com',
telephone: '+221776543210',
genre: Genre.femme,
statut: StatutMembre.enAttente,
dateNaissance: DateTime(1990, 7, 22),
adresse: '45 Rue de la République, Dakar',
);
test('should create new member successfully', () async {
// Arrange
when(mockRepository.createMembre(tMemberData))
.thenAnswer((_) async => tCreatedMember);
// Act
final result = await useCase(tMemberData);
// Assert
expect(result, equals(tCreatedMember));
expect(result.id, equals('456'));
expect(result.nom, equals('Ndiaye'));
expect(result.email, equals('fatou.ndiaye@example.com'));
verify(mockRepository.createMembre(tMemberData));
verifyNoMoreInteractions(mockRepository);
});
test('should create member with minimal required fields', () async {
// Arrange
final minimalMember = MembreCompletModel(
nom: 'Ba',
prenom: 'Moussa',
email: 'moussa.ba@example.com',
genre: Genre.homme,
statut: StatutMembre.enAttente,
);
final createdMinimal = MembreCompletModel(
id: '789',
nom: 'Ba',
prenom: 'Moussa',
email: 'moussa.ba@example.com',
genre: Genre.homme,
statut: StatutMembre.enAttente,
);
when(mockRepository.createMembre(minimalMember))
.thenAnswer((_) async => createdMinimal);
// Act
final result = await useCase(minimalMember);
// Assert
expect(result.id, equals('789'));
expect(result.nom, equals('Ba'));
});
test('should throw exception when email already exists', () async {
// Arrange
when(mockRepository.createMembre(any))
.thenThrow(Exception('Email déjà utilisé'));
// Act & Assert
expect(() => useCase(tMemberData), throwsA(isA<Exception>()));
});
test('should throw exception when validation fails', () async {
// Arrange
final invalidMember = MembreCompletModel(
nom: '',
prenom: 'Test',
email: 'invalid-email',
genre: Genre.autre,
statut: StatutMembre.enAttente,
);
when(mockRepository.createMembre(any))
.thenThrow(Exception('Données invalides'));
// Act & Assert
expect(() => useCase(invalidMember), throwsException);
});
});
}

View File

@@ -0,0 +1,68 @@
/// Tests unitaires pour DeleteMember use case
library delete_member_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/delete_member.dart';
@GenerateMocks([IMembreRepository])
import 'delete_member_test.mocks.dart';
void main() {
late DeleteMember useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = DeleteMember(mockRepository);
});
group('DeleteMember Use Case', () {
const tMemberId = '123';
test('should delete member successfully', () async {
// Arrange
when(mockRepository.deleteMembre(tMemberId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tMemberId);
// Assert
verify(mockRepository.deleteMembre(tMemberId));
verifyNoMoreInteractions(mockRepository);
});
test('should delete member with confirmation', () async {
// Arrange
when(mockRepository.deleteMembre(tMemberId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tMemberId);
// Assert
verify(mockRepository.deleteMembre(tMemberId)).called(1);
});
test('should throw exception when member not found', () async {
// Arrange
when(mockRepository.deleteMembre(any))
.thenThrow(Exception('Membre non trouvé'));
// Act & Assert
expect(() => useCase('999'), throwsA(isA<Exception>()));
});
test('should throw exception when member has dependencies', () async {
// Arrange
when(mockRepository.deleteMembre(any))
.thenThrow(Exception('Impossible de supprimer: membre a des contributions actives'));
// Act & Assert
expect(() => useCase(tMemberId), throwsException);
});
});
}

View File

@@ -0,0 +1,134 @@
/// Tests unitaires pour ExportMembers use case
library export_members_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/export_members.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_result.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_criteria.dart';
@GenerateMocks([IMembreRepository])
import 'export_members_test.mocks.dart';
void main() {
late ExportMembers useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = ExportMembers(mockRepository);
});
group('ExportMembers Use Case', () {
const tFormat = 'csv';
final tCriteria = MembreSearchCriteria(
statut: 'ACTIF',
);
final tMembers = [
MembreCompletModel(
id: '1',
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou@example.com',
genre: Genre.homme,
statut: StatutMembre.actif,
),
MembreCompletModel(
id: '2',
nom: 'Ndiaye',
prenom: 'Fatou',
email: 'fatou@example.com',
genre: Genre.femme,
statut: StatutMembre.actif,
),
];
final tSearchResult = MembreSearchResult(
membres: tMembers,
totalElements: 2,
totalPages: 1,
currentPage: 0,
pageSize: 10000,
numberOfElements: 2,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: tCriteria,
executionTimeMs: 50,
);
test('should export members to CSV format successfully', () async {
// Arrange
when(mockRepository.searchMembres(
criteria: anyNamed('criteria'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(criteria: tCriteria, format: tFormat);
// Assert
expect(result, isA<List<Map<String, dynamic>>>());
expect(result.length, equals(2));
expect(result[0]['nom'], equals('Diallo'));
expect(result[0]['email'], equals('amadou@example.com'));
verify(mockRepository.searchMembres(
criteria: anyNamed('criteria'),
page: anyNamed('page'),
size: anyNamed('size'),
));
verifyNoMoreInteractions(mockRepository);
});
test('should export members to PDF format', () async {
// Arrange
const pdfFormat = 'pdf';
when(mockRepository.searchMembres(
criteria: anyNamed('criteria'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(criteria: tCriteria, format: pdfFormat);
// Assert
expect(result.length, equals(2));
expect(result[1]['prenom'], equals('Fatou'));
});
test('should export all members when no criteria provided', () async {
// Arrange
when(mockRepository.searchMembres(
criteria: anyNamed('criteria'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(criteria: null, format: tFormat);
// Assert
expect(result, isNotNull);
expect(result.length, equals(2));
});
test('should throw exception when export fails', () async {
// Arrange
when(mockRepository.searchMembres(
criteria: anyNamed('criteria'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenThrow(Exception('Échec de l\'export'));
// Act & Assert
expect(() => useCase(criteria: tCriteria, format: tFormat), throwsException);
});
});
}

View File

@@ -0,0 +1,92 @@
/// Tests unitaires pour GetMemberById use case
library get_member_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_by_id.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
@GenerateMocks([IMembreRepository])
import 'get_member_by_id_test.mocks.dart';
void main() {
late GetMemberById useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = GetMemberById(mockRepository);
});
group('GetMemberById Use Case', () {
const tMemberId = '123';
final tMember = MembreCompletModel(
id: tMemberId,
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou.diallo@example.com',
telephone: '+221771234567',
genre: Genre.homme,
statut: StatutMembre.actif,
dateNaissance: DateTime(1985, 3, 15),
adresse: '12 Avenue Bourguiba, Dakar',
profession: 'Ingénieur Informatique',
);
test('should return member details by ID', () async {
// Arrange
when(mockRepository.getMembreById(tMemberId))
.thenAnswer((_) async => tMember);
// Act
final result = await useCase(tMemberId);
// Assert
expect(result, equals(tMember));
expect(result!.id, equals(tMemberId));
expect(result.nom, equals('Diallo'));
expect(result.prenom, equals('Amadou'));
expect(result.email, equals('amadou.diallo@example.com'));
verify(mockRepository.getMembreById(tMemberId));
verifyNoMoreInteractions(mockRepository);
});
test('should return member with all optional fields populated', () async {
// Arrange
when(mockRepository.getMembreById(tMemberId))
.thenAnswer((_) async => tMember);
// Act
final result = await useCase(tMemberId);
// Assert
expect(result!.telephone, equals('+221771234567'));
expect(result.dateNaissance, isNotNull);
expect(result.adresse, equals('12 Avenue Bourguiba, Dakar'));
expect(result.profession, equals('Ingénieur Informatique'));
});
test('should return null when member not found', () async {
// Arrange
when(mockRepository.getMembreById(any))
.thenAnswer((_) async => null);
// Act
final result = await useCase('999');
// Assert
expect(result, isNull);
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getMembreById(any))
.thenThrow(Exception('Membre non trouvé'));
// Act & Assert
expect(() => useCase(tMemberId), throwsException);
});
});
}

View File

@@ -0,0 +1,111 @@
/// Tests unitaires pour GetMemberStats use case
library get_member_stats_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_stats.dart';
@GenerateMocks([IMembreRepository])
import 'get_member_stats_test.mocks.dart';
void main() {
late GetMemberStats useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = GetMemberStats(mockRepository);
});
group('GetMemberStats Use Case', () {
final tMemberStats = {
'totalMembres': 250,
'membresActifs': 180,
'membresInactifs': 50,
'membresSuspendus': 15,
'membresEnAttente': 5,
'repartitionGenre': {
'hommes': 130,
'femmes': 115,
'autre': 5,
},
'nouveauxMembresMois': 12,
'tauxActivation': 0.72,
'agesMoyens': {
'global': 42.5,
'hommes': 44.2,
'femmes': 40.8,
},
};
test('should return comprehensive member statistics', () async {
// Arrange
when(mockRepository.getMembresStats())
.thenAnswer((_) async => tMemberStats);
// Act
final result = await useCase();
// Assert
expect(result, equals(tMemberStats));
expect(result['totalMembres'], equals(250));
expect(result['membresActifs'], equals(180));
expect(result['tauxActivation'], equals(0.72));
verify(mockRepository.getMembresStats());
verifyNoMoreInteractions(mockRepository);
});
test('should return gender distribution statistics', () async {
// Arrange
when(mockRepository.getMembresStats())
.thenAnswer((_) async => tMemberStats);
// Act
final result = await useCase();
// Assert
final repartition = result['repartitionGenre'] as Map<String, dynamic>;
expect(repartition['hommes'], equals(130));
expect(repartition['femmes'], equals(115));
expect(repartition['autre'], equals(5));
});
test('should return empty stats when no members exist', () async {
// Arrange
final emptyStats = {
'totalMembres': 0,
'membresActifs': 0,
'membresInactifs': 0,
'membresSuspendus': 0,
'membresEnAttente': 0,
'repartitionGenre': {
'hommes': 0,
'femmes': 0,
'autre': 0,
},
'nouveauxMembresMois': 0,
'tauxActivation': 0.0,
};
when(mockRepository.getMembresStats())
.thenAnswer((_) async => emptyStats);
// Act
final result = await useCase();
// Assert
expect(result['totalMembres'], equals(0));
expect(result['tauxActivation'], equals(0.0));
});
test('should throw exception when stats retrieval fails', () async {
// Arrange
when(mockRepository.getMembresStats())
.thenThrow(Exception('Erreur serveur'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,157 @@
/// Tests unitaires pour GetMembers use case
library get_members_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_members.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_result.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_criteria.dart';
@GenerateMocks([IMembreRepository])
import 'get_members_test.mocks.dart';
void main() {
late GetMembers useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = GetMembers(mockRepository);
});
group('GetMembers Use Case', () {
final tMembersList = [
MembreCompletModel(
id: '1',
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou.diallo@example.com',
genre: Genre.homme,
statut: StatutMembre.actif,
),
MembreCompletModel(
id: '2',
nom: 'Ndiaye',
prenom: 'Fatou',
email: 'fatou.ndiaye@example.com',
genre: Genre.femme,
statut: StatutMembre.actif,
),
];
final tSearchResult = MembreSearchResult(
membres: tMembersList,
totalElements: 2,
totalPages: 1,
currentPage: 0,
pageSize: 50,
numberOfElements: 2,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: MembreSearchCriteria(),
executionTimeMs: 45,
);
test('should return paginated list of all members', () async {
// Arrange
when(mockRepository.getMembres(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(page: 0, size: 50);
// Assert
expect(result, equals(tSearchResult));
expect(result.membres.length, equals(2));
expect(result.totalElements, equals(2));
expect(result.membres[0].nom, equals('Diallo'));
verify(mockRepository.getMembres(
page: 0,
size: 50,
recherche: null,
));
verifyNoMoreInteractions(mockRepository);
});
test('should return members with custom page size', () async {
// Arrange
final smallResult = MembreSearchResult(
membres: [tMembersList[0]],
totalElements: 2,
totalPages: 2,
currentPage: 0,
pageSize: 1,
numberOfElements: 1,
hasNext: true,
hasPrevious: false,
isFirst: true,
isLast: false,
criteria: MembreSearchCriteria(),
executionTimeMs: 30,
);
when(mockRepository.getMembres(
page: 0,
size: 1,
recherche: null,
)).thenAnswer((_) async => smallResult);
// Act
final result = await useCase(page: 0, size: 1);
// Assert
expect(result.membres.length, equals(1));
expect(result.pageSize, equals(1));
expect(result.hasNext, isTrue);
});
test('should return empty result when no members exist', () async {
// Arrange
final emptyResult = MembreSearchResult(
membres: [],
totalElements: 0,
totalPages: 0,
currentPage: 0,
pageSize: 50,
numberOfElements: 0,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: MembreSearchCriteria(),
executionTimeMs: 20,
);
when(mockRepository.getMembres(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => emptyResult);
// Act
final result = await useCase(page: 0, size: 50);
// Assert
expect(result.membres, isEmpty);
expect(result.totalElements, equals(0));
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getMembres(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(Exception('Erreur serveur'));
// Act & Assert
expect(() => useCase(page: 0, size: 50), throwsException);
});
});
}

View File

@@ -0,0 +1,168 @@
/// Tests unitaires pour SearchMembers use case
library search_members_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/search_members.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_result.dart';
import 'package:unionflow_mobile_apps/shared/models/membre_search_criteria.dart';
@GenerateMocks([IMembreRepository])
import 'search_members_test.mocks.dart';
void main() {
late SearchMembers useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = SearchMembers(mockRepository);
});
group('SearchMembers Use Case', () {
final tSearchCriteria = MembreSearchCriteria(
query: 'Diallo',
statut: 'ACTIF',
);
final tMatchingMembers = [
MembreCompletModel(
id: '1',
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou.diallo@example.com',
genre: Genre.homme,
statut: StatutMembre.actif,
),
MembreCompletModel(
id: '2',
nom: 'Diallo',
prenom: 'Fatou',
email: 'fatou.diallo@example.com',
genre: Genre.femme,
statut: StatutMembre.actif,
),
];
final tSearchResult = MembreSearchResult(
membres: tMatchingMembers,
totalElements: 2,
totalPages: 1,
currentPage: 0,
pageSize: 20,
numberOfElements: 2,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: tSearchCriteria,
executionTimeMs: 65,
);
test('should return search results matching criteria', () async {
// Arrange
when(mockRepository.searchMembres(
criteria: tSearchCriteria,
page: 0,
size: 20,
)).thenAnswer((_) async => tSearchResult);
// Act
final result = await useCase(criteria: tSearchCriteria, page: 0, size: 20);
// Assert
expect(result, equals(tSearchResult));
expect(result.membres.length, equals(2));
expect(result.membres.every((m) => m.nom == 'Diallo'), isTrue);
expect(result.membres.every((m) => m.statut == StatutMembre.actif || m.statut == null), isTrue);
verify(mockRepository.searchMembres(
criteria: tSearchCriteria,
page: 0,
size: 20,
));
verifyNoMoreInteractions(mockRepository);
});
test('should search members by prenom', () async {
// Arrange
final prenomCriteria = MembreSearchCriteria(
prenom: 'Fatou',
);
final fatouMembers = [tMatchingMembers[1]];
final prenomResult = MembreSearchResult(
membres: fatouMembers,
totalElements: 1,
totalPages: 1,
currentPage: 0,
pageSize: 20,
numberOfElements: 1,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: prenomCriteria,
executionTimeMs: 40,
);
when(mockRepository.searchMembres(
criteria: prenomCriteria,
page: 0,
size: 20,
)).thenAnswer((_) async => prenomResult);
// Act
final result = await useCase(criteria: prenomCriteria, page: 0, size: 20);
// Assert
expect(result.membres.length, equals(1));
expect(result.membres[0].prenom, equals('Fatou'));
});
test('should return empty result when no matches found', () async {
// Arrange
final noMatchCriteria = MembreSearchCriteria(
query: 'NonExistant',
);
final emptyResult = MembreSearchResult(
membres: [],
totalElements: 0,
totalPages: 0,
currentPage: 0,
pageSize: 20,
numberOfElements: 0,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: noMatchCriteria,
executionTimeMs: 25,
);
when(mockRepository.searchMembres(
criteria: noMatchCriteria,
page: 0,
size: 20,
)).thenAnswer((_) async => emptyResult);
// Act
final result = await useCase(criteria: noMatchCriteria, page: 0, size: 20);
// Assert
expect(result.membres, isEmpty);
expect(result.totalElements, equals(0));
});
test('should throw exception when search fails', () async {
// Arrange
when(mockRepository.searchMembres(
criteria: anyNamed('criteria'),
page: anyNamed('page'),
size: anyNamed('size'),
)).thenThrow(Exception('Erreur de recherche'));
// Act & Assert
expect(() => useCase(criteria: tSearchCriteria, page: 0, size: 20), throwsException);
});
});
}

View File

@@ -0,0 +1,102 @@
/// Tests unitaires pour UpdateMember use case
library update_member_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
import 'package:unionflow_mobile_apps/features/members/domain/usecases/update_member.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
@GenerateMocks([IMembreRepository])
import 'update_member_test.mocks.dart';
void main() {
late UpdateMember useCase;
late MockIMembreRepository mockRepository;
setUp(() {
mockRepository = MockIMembreRepository();
useCase = UpdateMember(mockRepository);
});
group('UpdateMember Use Case', () {
const tMemberId = '123';
final tUpdatedData = MembreCompletModel(
id: tMemberId,
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou.diallo.updated@example.com',
telephone: '+221771112222',
genre: Genre.homme,
statut: StatutMembre.actif,
profession: 'Directeur IT',
);
final tUpdatedMember = MembreCompletModel(
id: tMemberId,
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou.diallo.updated@example.com',
telephone: '+221771112222',
genre: Genre.homme,
statut: StatutMembre.actif,
profession: 'Directeur IT',
);
test('should update member successfully', () async {
// Arrange
when(mockRepository.updateMembre(tMemberId, tUpdatedData))
.thenAnswer((_) async => tUpdatedMember);
// Act
final result = await useCase(tMemberId, tUpdatedData);
// Assert
expect(result, equals(tUpdatedMember));
expect(result.email, equals('amadou.diallo.updated@example.com'));
expect(result.telephone, equals('+221771112222'));
expect(result.profession, equals('Directeur IT'));
verify(mockRepository.updateMembre(tMemberId, tUpdatedData));
verifyNoMoreInteractions(mockRepository);
});
test('should update member status to suspended', () async {
// Arrange
final suspendedMember = MembreCompletModel(
id: tMemberId,
nom: 'Diallo',
prenom: 'Amadou',
email: 'amadou.diallo@example.com',
genre: Genre.homme,
statut: StatutMembre.suspendu,
);
when(mockRepository.updateMembre(tMemberId, suspendedMember))
.thenAnswer((_) async => suspendedMember);
// Act
final result = await useCase(tMemberId, suspendedMember);
// Assert
expect(result.statut, equals(StatutMembre.suspendu));
});
test('should throw exception when member not found', () async {
// Arrange
when(mockRepository.updateMembre(any, any))
.thenThrow(Exception('Membre non trouvé'));
// Act & Assert
expect(() => useCase('999', tUpdatedData), throwsA(isA<Exception>()));
});
test('should throw exception when validation fails', () async {
// Arrange
when(mockRepository.updateMembre(any, any))
.thenThrow(Exception('Email invalide'));
// Act & Assert
expect(() => useCase(tMemberId, tUpdatedData), throwsException);
});
});
}

View File

@@ -0,0 +1,103 @@
/// Tests unitaires pour CreateOrganization use case
library create_organization_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/create_organization.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'create_organization_test.mocks.dart';
void main() {
late CreateOrganization useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = CreateOrganization(mockRepository);
});
group('CreateOrganization Use Case', () {
final tOrganization = OrganizationModel(
nom: 'Nouvelle Organisation',
nomCourt: 'NO',
email: 'contact@nouvelle.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.enCreation,
);
final tCreatedOrganization = OrganizationModel(
id: 'org123',
nom: 'Nouvelle Organisation',
nomCourt: 'NO',
email: 'contact@nouvelle.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should create organization successfully', () async {
// Arrange
when(mockRepository.createOrganization(tOrganization))
.thenAnswer((_) async => tCreatedOrganization);
// Act
final result = await useCase(tOrganization);
// Assert
expect(result, equals(tCreatedOrganization));
expect(result.id, isNotNull);
expect(result.nom, equals('Nouvelle Organisation'));
verify(mockRepository.createOrganization(tOrganization));
verifyNoMoreInteractions(mockRepository);
});
test('should create organization with minimal required fields', () async {
// Arrange
final minimalOrg = OrganizationModel(
nom: 'Minimal Org',
nomCourt: 'MO',
email: 'minimal@org.com',
typeOrganisation: TypeOrganization.cooperative,
statut: StatutOrganization.enCreation,
);
final createdMinimal = OrganizationModel(
id: 'org456',
nom: 'Minimal Org',
nomCourt: 'MO',
email: 'minimal@org.com',
typeOrganisation: TypeOrganization.cooperative,
statut: StatutOrganization.active,
);
when(mockRepository.createOrganization(minimalOrg))
.thenAnswer((_) async => createdMinimal);
// Act
final result = await useCase(minimalOrg);
// Assert
expect(result.id, isNotNull);
expect(result.nom, equals('Minimal Org'));
});
test('should throw exception when email already exists', () async {
// Arrange
when(mockRepository.createOrganization(any))
.thenThrow(Exception('Email déjà utilisé'));
// Act & Assert
expect(() => useCase(tOrganization), throwsA(isA<Exception>()));
});
test('should throw exception when validation fails', () async {
// Arrange
when(mockRepository.createOrganization(any))
.thenThrow(Exception('Données invalides'));
// Act & Assert
expect(() => useCase(tOrganization), throwsException);
});
});
}

View File

@@ -0,0 +1,72 @@
/// Tests unitaires pour DeleteOrganization use case
library delete_organization_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/delete_organization.dart';
@GenerateMocks([IOrganizationRepository])
import 'delete_organization_test.mocks.dart';
void main() {
late DeleteOrganization useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = DeleteOrganization(mockRepository);
});
group('DeleteOrganization Use Case', () {
const tOrganizationId = 'org1';
test('should delete organization successfully', () async {
// Arrange
when(mockRepository.deleteOrganization(tOrganizationId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tOrganizationId);
// Assert
verify(mockRepository.deleteOrganization(tOrganizationId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when organization not found', () async {
// Arrange
when(mockRepository.deleteOrganization(any))
.thenThrow(Exception('Organisation non trouvée'));
// Act & Assert
expect(
() => useCase(tOrganizationId),
throwsA(isA<Exception>()),
);
verify(mockRepository.deleteOrganization(tOrganizationId));
});
test('should throw exception when organization has members', () async {
// Arrange
when(mockRepository.deleteOrganization(any))
.thenThrow(Exception('Organisation contient des membres'));
// Act & Assert
expect(
() => useCase(tOrganizationId),
throwsA(isA<Exception>()),
);
});
test('should throw exception when deletion fails', () async {
// Arrange
when(mockRepository.deleteOrganization(any))
.thenThrow(Exception('Suppression échouée'));
// Act & Assert
expect(() => useCase(tOrganizationId), throwsException);
});
});
}

View File

@@ -0,0 +1,87 @@
/// Tests unitaires pour GetOrganizationById use case
library get_organization_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_by_id.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'get_organization_by_id_test.mocks.dart';
void main() {
late GetOrganizationById useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = GetOrganizationById(mockRepository);
});
group('GetOrganizationById Use Case', () {
const tOrganizationId = 'org1';
final tOrganization = OrganizationModel(
id: tOrganizationId,
nom: 'Organisation Alpha',
nomCourt: 'OA',
email: 'contact@alpha.org',
telephone: '+33123456789',
adresse: '123 Rue de Paris',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should return organization by id', () async {
// Arrange
when(mockRepository.getOrganizationById(tOrganizationId))
.thenAnswer((_) async => tOrganization);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result, equals(tOrganization));
expect(result?.id, equals(tOrganizationId));
verify(mockRepository.getOrganizationById(tOrganizationId));
verifyNoMoreInteractions(mockRepository);
});
test('should return null when organization not found', () async {
// Arrange
when(mockRepository.getOrganizationById(any))
.thenAnswer((_) async => null);
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, isNull);
verify(mockRepository.getOrganizationById('nonexistent'));
});
test('should return organization with all fields populated', () async {
// Arrange
when(mockRepository.getOrganizationById(tOrganizationId))
.thenAnswer((_) async => tOrganization);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result?.nom, isNotNull);
expect(result?.email, isNotNull);
expect(result?.statut, equals(StatutOrganization.active));
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getOrganizationById(any))
.thenThrow(Exception('Database error'));
// Act & Assert
expect(() => useCase(tOrganizationId), throwsException);
});
});
}

View File

@@ -0,0 +1,88 @@
/// Tests unitaires pour GetOrganizationMembers use case
library get_organization_members_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_members.dart';
@GenerateMocks([IOrganizationRepository])
import 'get_organization_members_test.mocks.dart';
void main() {
late GetOrganizationMembers useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = GetOrganizationMembers(mockRepository);
});
group('GetOrganizationMembers Use Case', () {
const tOrganizationId = 'org1';
final tMembersList = [
{
'id': 'membre1',
'nom': 'Dupont',
'prenom': 'Jean',
'email': 'jean.dupont@example.com',
},
{
'id': 'membre2',
'nom': 'Martin',
'prenom': 'Marie',
'email': 'marie.martin@example.com',
},
];
test('should return list of organization members', () async {
// Arrange
when(mockRepository.getOrganizationMembers(tOrganizationId))
.thenAnswer((_) async => tMembersList);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result, equals(tMembersList));
expect(result.length, equals(2));
verify(mockRepository.getOrganizationMembers(tOrganizationId));
verifyNoMoreInteractions(mockRepository);
});
test('should return empty list when organization has no members', () async {
// Arrange
when(mockRepository.getOrganizationMembers(tOrganizationId))
.thenAnswer((_) async => []);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result, isEmpty);
verify(mockRepository.getOrganizationMembers(tOrganizationId));
});
test('should throw exception when organization not found', () async {
// Arrange
when(mockRepository.getOrganizationMembers(any))
.thenThrow(Exception('Organisation non trouvée'));
// Act & Assert
expect(
() => useCase(tOrganizationId),
throwsA(isA<Exception>()),
);
});
test('should throw exception when retrieval fails', () async {
// Arrange
when(mockRepository.getOrganizationMembers(any))
.thenThrow(Exception('Erreur de récupération'));
// Act & Assert
expect(() => useCase(tOrganizationId), throwsException);
});
});
}

View File

@@ -0,0 +1,106 @@
/// Tests unitaires pour GetOrganizations use case
library get_organizations_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organizations.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'get_organizations_test.mocks.dart';
void main() {
late GetOrganizations useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = GetOrganizations(mockRepository);
});
group('GetOrganizations Use Case', () {
final tOrganizationList = [
OrganizationModel(
id: 'org1',
nom: 'Organisation Alpha',
nomCourt: 'OA',
email: 'contact@alpha.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
),
OrganizationModel(
id: 'org2',
nom: 'Organisation Beta',
nomCourt: 'OB',
email: 'contact@beta.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
),
];
test('should return list of organizations', () async {
// Arrange
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => tOrganizationList);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result, equals(tOrganizationList));
expect(result.length, equals(2));
verify(mockRepository.getOrganizations(page: 0, size: 20));
verifyNoMoreInteractions(mockRepository);
});
test('should filter organizations by search query', () async {
// Arrange
final filteredList = [tOrganizationList[0]];
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: 'Alpha',
)).thenAnswer((_) async => filteredList);
// Act
final result = await useCase(page: 0, size: 20, recherche: 'Alpha');
// Assert
expect(result.length, equals(1));
expect(result.first.nom, contains('Alpha'));
verify(mockRepository.getOrganizations(page: 0, size: 20, recherche: 'Alpha'));
});
test('should return empty list when no organizations exist', () async {
// Arrange
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => []);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result, isEmpty);
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(page: 0, size: 20), throwsException);
});
});
}

View File

@@ -0,0 +1,91 @@
/// Tests unitaires pour UpdateOrganizationConfig use case
library update_organization_config_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization_config.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'update_organization_config_test.mocks.dart';
void main() {
late UpdateOrganizationConfig useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = UpdateOrganizationConfig(mockRepository);
});
group('UpdateOrganizationConfig Use Case', () {
const tOrganizationId = 'org1';
final tConfig = {
'theme': 'dark',
'language': 'fr',
'notifications': true,
'cotisationMensuelle': 5000.0,
};
final tUpdatedOrganization = OrganizationModel(
id: tOrganizationId,
nom: 'Organisation Alpha',
nomCourt: 'OA',
email: 'contact@alpha.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should update organization configuration successfully', () async {
// Arrange
when(mockRepository.updateOrganizationConfig(tOrganizationId, tConfig))
.thenAnswer((_) async => tUpdatedOrganization);
// Act
final result = await useCase(tOrganizationId, tConfig);
// Assert
expect(result, equals(tUpdatedOrganization));
verify(mockRepository.updateOrganizationConfig(tOrganizationId, tConfig));
verifyNoMoreInteractions(mockRepository);
});
test('should update partial configuration', () async {
// Arrange
final partialConfig = {'theme': 'light'};
when(mockRepository.updateOrganizationConfig(tOrganizationId, partialConfig))
.thenAnswer((_) async => tUpdatedOrganization);
// Act
final result = await useCase(tOrganizationId, partialConfig);
// Assert
expect(result, isNotNull);
verify(mockRepository.updateOrganizationConfig(tOrganizationId, partialConfig));
});
test('should handle empty configuration map', () async {
// Arrange
final emptyConfig = <String, dynamic>{};
when(mockRepository.updateOrganizationConfig(tOrganizationId, emptyConfig))
.thenAnswer((_) async => tUpdatedOrganization);
// Act
final result = await useCase(tOrganizationId, emptyConfig);
// Assert
expect(result, isNotNull);
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateOrganizationConfig(any, any))
.thenThrow(Exception('Configuration update failed'));
// Act & Assert
expect(() => useCase(tOrganizationId, tConfig), throwsException);
});
});
}

View File

@@ -0,0 +1,91 @@
/// Tests unitaires pour UpdateOrganization use case
library update_organization_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'update_organization_test.mocks.dart';
void main() {
late UpdateOrganization useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = UpdateOrganization(mockRepository);
});
group('UpdateOrganization Use Case', () {
const tOrganizationId = 'org1';
final tOrganization = OrganizationModel(
id: tOrganizationId,
nom: 'Organisation Mise à Jour',
nomCourt: 'OMA',
email: 'updated@org.com',
telephone: '+33987654321',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should update organization successfully', () async {
// Arrange
when(mockRepository.updateOrganization(tOrganizationId, tOrganization))
.thenAnswer((_) async => tOrganization);
// Act
final result = await useCase(tOrganizationId, tOrganization);
// Assert
expect(result, equals(tOrganization));
expect(result.nom, equals('Organisation Mise à Jour'));
verify(mockRepository.updateOrganization(tOrganizationId, tOrganization));
verifyNoMoreInteractions(mockRepository);
});
test('should update partial organization fields', () async {
// Arrange
final partialUpdate = OrganizationModel(
id: tOrganizationId,
nom: 'Nom Modifié',
nomCourt: 'OA',
email: 'contact@alpha.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
when(mockRepository.updateOrganization(tOrganizationId, partialUpdate))
.thenAnswer((_) async => partialUpdate);
// Act
final result = await useCase(tOrganizationId, partialUpdate);
// Assert
expect(result.nom, equals('Nom Modifié'));
});
test('should throw exception when organization not found', () async {
// Arrange
when(mockRepository.updateOrganization(any, any))
.thenThrow(Exception('Organisation non trouvée'));
// Act & Assert
expect(
() => useCase(tOrganizationId, tOrganization),
throwsA(isA<Exception>()),
);
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateOrganization(any, any))
.thenThrow(Exception('Mise à jour échouée'));
// Act & Assert
expect(() => useCase(tOrganizationId, tOrganization), throwsException);
});
});
}

View File

@@ -0,0 +1,75 @@
/// Tests unitaires pour DeleteAccount use case
library delete_account_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/repositories/profile_repository.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/delete_account.dart';
@GenerateMocks([IProfileRepository])
import 'delete_account_test.mocks.dart';
void main() {
late DeleteAccount useCase;
late MockIProfileRepository mockRepository;
setUp(() {
mockRepository = MockIProfileRepository();
useCase = DeleteAccount(mockRepository);
});
group('DeleteAccount Use Case', () {
const tMembreId = 'membre1';
test('should delete account successfully (soft delete)', () async {
// Arrange
when(mockRepository.deleteAccount(tMembreId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tMembreId);
// Assert
verify(mockRepository.deleteAccount(tMembreId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when account not found', () async {
// Arrange
when(mockRepository.deleteAccount(any))
.thenThrow(Exception('Compte non trouvé'));
// Act & Assert
expect(
() => useCase(tMembreId),
throwsA(isA<Exception>()),
);
verify(mockRepository.deleteAccount(tMembreId));
});
test('should throw exception when account is already deleted', () async {
// Arrange
when(mockRepository.deleteAccount(any))
.thenThrow(Exception('Compte déjà désactivé'));
// Act & Assert
expect(
() => useCase(tMembreId),
throwsA(isA<Exception>()),
);
});
test('should throw exception when deletion fails', () async {
// Arrange
when(mockRepository.deleteAccount(any))
.thenThrow(Exception('Deletion failed'));
// Act & Assert
expect(
() => useCase(tMembreId),
throwsException,
);
});
});
}

View File

@@ -0,0 +1,84 @@
/// Tests unitaires pour GetProfile use case
library get_profile_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/repositories/profile_repository.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/get_profile.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
@GenerateMocks([IProfileRepository])
import 'get_profile_test.mocks.dart';
void main() {
late GetProfile useCase;
late MockIProfileRepository mockRepository;
setUp(() {
mockRepository = MockIProfileRepository();
useCase = GetProfile(mockRepository);
});
group('GetProfile Use Case', () {
final tMembre = MembreCompletModel(
id: 'membre1',
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@example.com',
telephone: '+33612345678',
dateNaissance: DateTime(1990, 1, 1),
);
test('should return current user profile from repository', () async {
// Arrange
when(mockRepository.getMe()).thenAnswer((_) async => tMembre);
// Act
final result = await useCase();
// Assert
expect(result, equals(tMembre));
verify(mockRepository.getMe());
verifyNoMoreInteractions(mockRepository);
});
test('should return null when user is not authenticated', () async {
// Arrange
when(mockRepository.getMe()).thenAnswer((_) async => null);
// Act
final result = await useCase();
// Assert
expect(result, isNull);
verify(mockRepository.getMe());
});
test('should throw exception when repository throws', () async {
// Arrange
when(mockRepository.getMe()).thenThrow(Exception('Unauthorized'));
// Act & Assert
expect(
() => useCase(),
throwsA(isA<Exception>()),
);
verify(mockRepository.getMe());
});
test('should cache profile data on successful retrieval', () async {
// Arrange
when(mockRepository.getMe()).thenAnswer((_) async => tMembre);
// Act - Call twice
final result1 = await useCase();
final result2 = await useCase();
// Assert - Repository called twice (no caching at use case level)
expect(result1, equals(tMembre));
expect(result2, equals(tMembre));
verify(mockRepository.getMe()).called(2);
});
});
}

View File

@@ -0,0 +1,95 @@
/// Tests unitaires pour UpdateAvatar use case
library update_avatar_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/repositories/profile_repository.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/update_avatar.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
@GenerateMocks([IProfileRepository])
import 'update_avatar_test.mocks.dart';
void main() {
late UpdateAvatar useCase;
late MockIProfileRepository mockRepository;
setUp(() {
mockRepository = MockIProfileRepository();
useCase = UpdateAvatar(mockRepository);
});
group('UpdateAvatar Use Case', () {
const tMembreId = 'membre1';
const tPhotoUrl = 'https://example.com/avatar.jpg';
final tUpdatedMembre = MembreCompletModel(
id: tMembreId,
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@example.com',
photo: tPhotoUrl,
);
test('should update avatar successfully', () async {
// Arrange
when(mockRepository.updateAvatar(tMembreId, tPhotoUrl))
.thenAnswer((_) async => tUpdatedMembre);
// Act
final result = await useCase(tMembreId, tPhotoUrl);
// Assert
expect(result, equals(tUpdatedMembre));
expect(result.photo, equals(tPhotoUrl));
verify(mockRepository.updateAvatar(tMembreId, tPhotoUrl));
verifyNoMoreInteractions(mockRepository);
});
test('should handle empty photo URL', () async {
// Arrange
const emptyUrl = '';
final emptyPhotoMembre = MembreCompletModel(
id: tMembreId,
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@example.com',
photo: emptyUrl,
);
when(mockRepository.updateAvatar(tMembreId, emptyUrl))
.thenAnswer((_) async => emptyPhotoMembre);
// Act
final result = await useCase(tMembreId, emptyUrl);
// Assert
expect(result.photo, equals(emptyUrl));
verify(mockRepository.updateAvatar(tMembreId, emptyUrl));
});
test('should throw exception when member not found', () async {
// Arrange
when(mockRepository.updateAvatar(any, any))
.thenThrow(Exception('Membre non trouvé'));
// Act & Assert
expect(
() => useCase(tMembreId, tPhotoUrl),
throwsA(isA<Exception>()),
);
});
test('should throw exception when upload fails', () async {
// Arrange
when(mockRepository.updateAvatar(any, any))
.thenThrow(Exception('Upload failed'));
// Act & Assert
expect(
() => useCase(tMembreId, tPhotoUrl),
throwsException,
);
});
});
}

View File

@@ -0,0 +1,93 @@
/// Tests unitaires pour UpdatePreferences use case
library update_preferences_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/repositories/profile_repository.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/update_preferences.dart';
@GenerateMocks([IProfileRepository])
import 'update_preferences_test.mocks.dart';
void main() {
late UpdatePreferences useCase;
late MockIProfileRepository mockRepository;
setUp(() {
mockRepository = MockIProfileRepository();
useCase = UpdatePreferences(mockRepository);
});
group('UpdatePreferences Use Case', () {
const tMembreId = 'membre1';
final tPreferences = {
'language': 'fr',
'theme': 'dark',
'notifications': true,
'emailNotifications': false,
};
final tUpdatedPreferences = {
...tPreferences,
'lastUpdated': '2026-03-14T10:00:00Z',
};
test('should update preferences successfully', () async {
// Arrange
when(mockRepository.updatePreferences(tMembreId, tPreferences))
.thenAnswer((_) async => tUpdatedPreferences);
// Act
final result = await useCase(tMembreId, tPreferences);
// Assert
expect(result, equals(tUpdatedPreferences));
expect(result['language'], equals('fr'));
expect(result['theme'], equals('dark'));
verify(mockRepository.updatePreferences(tMembreId, tPreferences));
verifyNoMoreInteractions(mockRepository);
});
test('should update partial preferences', () async {
// Arrange
final partialPrefs = {'theme': 'light'};
final expectedResult = {'theme': 'light'};
when(mockRepository.updatePreferences(tMembreId, partialPrefs))
.thenAnswer((_) async => expectedResult);
// Act
final result = await useCase(tMembreId, partialPrefs);
// Assert
expect(result['theme'], equals('light'));
verify(mockRepository.updatePreferences(tMembreId, partialPrefs));
});
test('should handle empty preferences map', () async {
// Arrange
final emptyPrefs = <String, dynamic>{};
when(mockRepository.updatePreferences(tMembreId, emptyPrefs))
.thenAnswer((_) async => emptyPrefs);
// Act
final result = await useCase(tMembreId, emptyPrefs);
// Assert
expect(result, isEmpty);
verify(mockRepository.updatePreferences(tMembreId, emptyPrefs));
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updatePreferences(any, any))
.thenThrow(Exception('Failed to update preferences'));
// Act & Assert
expect(
() => useCase(tMembreId, tPreferences),
throwsException,
);
});
});
}

View File

@@ -0,0 +1,97 @@
/// Tests unitaires pour UpdateProfile use case
library update_profile_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/repositories/profile_repository.dart';
import 'package:unionflow_mobile_apps/features/profile/domain/usecases/update_profile.dart';
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
@GenerateMocks([IProfileRepository])
import 'update_profile_test.mocks.dart';
void main() {
late UpdateProfile useCase;
late MockIProfileRepository mockRepository;
setUp(() {
mockRepository = MockIProfileRepository();
useCase = UpdateProfile(mockRepository);
});
group('UpdateProfile Use Case', () {
const tMembreId = 'membre1';
final tMembre = MembreCompletModel(
id: tMembreId,
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@example.com',
telephone: '+33612345678',
dateNaissance: DateTime(1990, 1, 1),
);
final tUpdatedMembre = MembreCompletModel(
id: tMembreId,
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@example.com',
telephone: '+33698765432', // Updated phone
dateNaissance: DateTime(1990, 1, 1),
adresse: '123 Rue de Paris', // Added address
);
test('should update profile successfully', () async {
// Arrange
when(mockRepository.updateProfile(tMembreId, tMembre))
.thenAnswer((_) async => tUpdatedMembre);
// Act
final result = await useCase(tMembreId, tMembre);
// Assert
expect(result, equals(tUpdatedMembre));
verify(mockRepository.updateProfile(tMembreId, tMembre));
verifyNoMoreInteractions(mockRepository);
});
test('should update only specified fields', () async {
// Arrange
when(mockRepository.updateProfile(any, any))
.thenAnswer((_) async => tUpdatedMembre);
// Act
final result = await useCase(tMembreId, tMembre);
// Assert
expect(result.telephone, equals('+33698765432'));
expect(result.adresse, equals('123 Rue de Paris'));
verify(mockRepository.updateProfile(tMembreId, tMembre));
});
test('should throw exception when profile not found', () async {
// Arrange
when(mockRepository.updateProfile(any, any))
.thenThrow(Exception('Profil non trouvé'));
// Act & Assert
expect(
() => useCase(tMembreId, tMembre),
throwsA(isA<Exception>()),
);
verify(mockRepository.updateProfile(tMembreId, tMembre));
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateProfile(any, any))
.thenThrow(Exception('Network error'));
// Act & Assert
expect(
() => useCase(tMembreId, tMembre),
throwsException,
);
});
});
}

View File

@@ -0,0 +1,80 @@
/// Tests unitaires pour ExportReportExcel use case
library export_report_excel_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/export_report_excel.dart';
@GenerateMocks([IReportsRepository])
import 'export_report_excel_test.mocks.dart';
void main() {
late ExportReportExcel useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = ExportReportExcel(mockRepository);
});
group('ExportReportExcel Use Case', () {
const tReportType = 'membres';
const tExcelPath = '/storage/reports/membres_2024.xlsx';
const tCsvPath = '/storage/reports/membres_2024.csv';
test('should export report to Excel successfully', () async {
// Arrange
when(mockRepository.exportReportExcel(tReportType, format: 'excel'))
.thenAnswer((_) async => tExcelPath);
// Act
final result = await useCase(tReportType);
// Assert
expect(result, equals(tExcelPath));
expect(result, contains('.xlsx'));
verify(mockRepository.exportReportExcel(tReportType, format: 'excel'));
verifyNoMoreInteractions(mockRepository);
});
test('should export report to CSV when format specified', () async {
// Arrange
when(mockRepository.exportReportExcel(tReportType, format: 'csv'))
.thenAnswer((_) async => tCsvPath);
// Act
final result = await useCase(tReportType, format: 'csv');
// Assert
expect(result, equals(tCsvPath));
expect(result, contains('.csv'));
verify(mockRepository.exportReportExcel(tReportType, format: 'csv'));
});
test('should return valid Excel path with type name', () async {
// Arrange
const cotisationsType = 'cotisations';
const cotisationsExcel = '/storage/reports/cotisations_2024.xlsx';
when(mockRepository.exportReportExcel(cotisationsType, format: 'excel'))
.thenAnswer((_) async => cotisationsExcel);
// Act
final result = await useCase(cotisationsType);
// Assert
expect(result, contains('cotisations'));
expect(result, contains('.xlsx'));
});
test('should throw exception when Excel export fails', () async {
// Arrange
when(mockRepository.exportReportExcel(any, format: anyNamed('format')))
.thenThrow(Exception('Excel export failed'));
// Act & Assert
expect(() => useCase(tReportType), throwsException);
});
});
}

View File

@@ -0,0 +1,79 @@
/// Tests unitaires pour ExportReportPdf use case
library export_report_pdf_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/export_report_pdf.dart';
@GenerateMocks([IReportsRepository])
import 'export_report_pdf_test.mocks.dart';
void main() {
late ExportReportPdf useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = ExportReportPdf(mockRepository);
});
group('ExportReportPdf Use Case', () {
const tReportType = 'membres';
const tPdfPath = '/storage/reports/membres_2024.pdf';
test('should export report to PDF successfully', () async {
// Arrange
when(mockRepository.exportReportPdf(tReportType))
.thenAnswer((_) async => tPdfPath);
// Act
final result = await useCase(tReportType);
// Assert
expect(result, equals(tPdfPath));
expect(result, contains('.pdf'));
verify(mockRepository.exportReportPdf(tReportType));
verifyNoMoreInteractions(mockRepository);
});
test('should return valid PDF path with type name', () async {
// Arrange
const cotisationsType = 'cotisations';
const cotisationsPdf = '/storage/reports/cotisations_2024.pdf';
when(mockRepository.exportReportPdf(cotisationsType))
.thenAnswer((_) async => cotisationsPdf);
// Act
final result = await useCase(cotisationsType);
// Assert
expect(result, contains('cotisations'));
expect(result, endsWith('.pdf'));
});
test('should return URL for remote PDF file', () async {
// Arrange
const remotePdf = 'https://api.example.com/reports/membres.pdf';
when(mockRepository.exportReportPdf(tReportType))
.thenAnswer((_) async => remotePdf);
// Act
final result = await useCase(tReportType);
// Assert
expect(result, startsWith('https://'));
expect(result, endsWith('.pdf'));
});
test('should throw exception when PDF export fails', () async {
// Arrange
when(mockRepository.exportReportPdf(any))
.thenThrow(Exception('PDF generation failed'));
// Act & Assert
expect(() => useCase(tReportType), throwsException);
});
});
}

View File

@@ -0,0 +1,73 @@
/// Tests unitaires pour GenerateReport use case
library generate_report_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/generate_report.dart';
@GenerateMocks([IReportsRepository])
import 'generate_report_test.mocks.dart';
void main() {
late GenerateReport useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = GenerateReport(mockRepository);
});
group('GenerateReport Use Case', () {
const tReportType = 'membres';
const tFormat = 'pdf';
test('should generate report successfully', () async {
// Arrange
when(mockRepository.generateReport(tReportType, format: null))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tReportType);
// Assert
verify(mockRepository.generateReport(tReportType, format: null));
verifyNoMoreInteractions(mockRepository);
});
test('should generate report with specific format', () async {
// Arrange
when(mockRepository.generateReport(tReportType, format: tFormat))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tReportType, format: tFormat);
// Assert
verify(mockRepository.generateReport(tReportType, format: tFormat));
});
test('should generate report for different types', () async {
// Arrange
const altType = 'cotisations';
when(mockRepository.generateReport(altType, format: null))
.thenAnswer((_) async => Future.value());
// Act
await useCase(altType);
// Assert
verify(mockRepository.generateReport(altType, format: null));
});
test('should throw exception when generation fails', () async {
// Arrange
when(mockRepository.generateReport(any, format: anyNamed('format')))
.thenThrow(Exception('Generation failed'));
// Act & Assert
expect(() => useCase(tReportType), throwsException);
});
});
}

View File

@@ -0,0 +1,102 @@
/// Tests unitaires pour GetReports use case
library get_reports_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/get_reports.dart';
@GenerateMocks([IReportsRepository])
import 'get_reports_test.mocks.dart';
void main() {
late GetReports useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = GetReports(mockRepository);
});
group('GetReports Use Case', () {
final tAvailableReports = [
{
'type': 'membres',
'nom': 'Rapport Membres',
'description': 'Statistiques complètes des membres',
'categorie': 'administratif',
},
{
'type': 'cotisations',
'nom': 'Rapport Cotisations',
'description': 'Analyse des cotisations',
'categorie': 'financier',
},
{
'type': 'evenements',
'nom': 'Rapport Événements',
'description': 'Bilan des événements',
'categorie': 'activites',
},
];
test('should return list of available reports', () async {
// Arrange
when(mockRepository.getAvailableReports())
.thenAnswer((_) async => tAvailableReports);
// Act
final result = await useCase();
// Assert
expect(result, equals(tAvailableReports));
expect(result.length, equals(3));
expect(result[0]['type'], equals('membres'));
verify(mockRepository.getAvailableReports());
verifyNoMoreInteractions(mockRepository);
});
test('should return reports with specific categories', () async {
// Arrange
final financialReports = [
{
'type': 'cotisations',
'nom': 'Rapport Cotisations',
'categorie': 'financier',
},
];
when(mockRepository.getAvailableReports())
.thenAnswer((_) async => financialReports);
// Act
final result = await useCase();
// Assert
expect(result.length, equals(1));
expect(result.first['categorie'], equals('financier'));
verify(mockRepository.getAvailableReports());
});
test('should return empty list when no reports available', () async {
// Arrange
when(mockRepository.getAvailableReports()).thenAnswer((_) async => []);
// Act
final result = await useCase();
// Assert
expect(result, isEmpty);
verify(mockRepository.getAvailableReports());
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getAvailableReports())
.thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,102 @@
/// Tests unitaires pour GetScheduledReports use case
library get_scheduled_reports_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/get_scheduled_reports.dart';
@GenerateMocks([IReportsRepository])
import 'get_scheduled_reports_test.mocks.dart';
void main() {
late GetScheduledReports useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = GetScheduledReports(mockRepository);
});
group('GetScheduledReports Use Case', () {
final tScheduledReports = [
{
'id': '1',
'type': 'membres',
'nom': 'Rapport Membres Mensuel',
'cronExpression': '0 0 1 * *',
'active': true,
'derniereLancement': '2024-01-01T00:00:00Z',
},
{
'id': '2',
'type': 'cotisations',
'nom': 'Rapport Cotisations Hebdomadaire',
'cronExpression': '0 9 * * 1',
'active': true,
'derniereLancement': '2024-01-08T09:00:00Z',
},
];
test('should return list of scheduled reports', () async {
// Arrange
when(mockRepository.getScheduledReports())
.thenAnswer((_) async => tScheduledReports);
// Act
final result = await useCase();
// Assert
expect(result, equals(tScheduledReports));
expect(result.length, equals(2));
expect(result[0]['type'], equals('membres'));
expect(result[0]['active'], isTrue);
verify(mockRepository.getScheduledReports());
verifyNoMoreInteractions(mockRepository);
});
test('should return only active scheduled reports', () async {
// Arrange
final activeReports = [
{
'id': '1',
'type': 'membres',
'cronExpression': '0 0 1 * *',
'active': true,
},
];
when(mockRepository.getScheduledReports())
.thenAnswer((_) async => activeReports);
// Act
final result = await useCase();
// Assert
expect(result.length, equals(1));
expect(result.first['active'], isTrue);
verify(mockRepository.getScheduledReports());
});
test('should return empty list when no scheduled reports', () async {
// Arrange
when(mockRepository.getScheduledReports()).thenAnswer((_) async => []);
// Act
final result = await useCase();
// Assert
expect(result, isEmpty);
verify(mockRepository.getScheduledReports());
});
test('should throw exception when retrieval fails', () async {
// Arrange
when(mockRepository.getScheduledReports())
.thenThrow(Exception('Database error'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,72 @@
/// Tests unitaires pour ScheduleReport use case
library schedule_report_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/schedule_report.dart';
@GenerateMocks([IReportsRepository])
import 'schedule_report_test.mocks.dart';
void main() {
late ScheduleReport useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = ScheduleReport(mockRepository);
});
group('ScheduleReport Use Case', () {
const tCronExpression = '0 0 1 * *'; // 1er de chaque mois à minuit
test('should schedule report successfully', () async {
// Arrange
when(mockRepository.scheduleReport(cronExpression: null))
.thenAnswer((_) async => Future.value());
// Act
await useCase();
// Assert
verify(mockRepository.scheduleReport(cronExpression: null));
verifyNoMoreInteractions(mockRepository);
});
test('should schedule report with cron expression', () async {
// Arrange
when(mockRepository.scheduleReport(cronExpression: tCronExpression))
.thenAnswer((_) async => Future.value());
// Act
await useCase(cronExpression: tCronExpression);
// Assert
verify(mockRepository.scheduleReport(cronExpression: tCronExpression));
});
test('should schedule report with different cron expressions', () async {
// Arrange
const weeklyCron = '0 9 * * 1'; // Tous les lundis à 9h
when(mockRepository.scheduleReport(cronExpression: weeklyCron))
.thenAnswer((_) async => Future.value());
// Act
await useCase(cronExpression: weeklyCron);
// Assert
verify(mockRepository.scheduleReport(cronExpression: weeklyCron));
});
test('should throw exception when scheduling fails', () async {
// Arrange
when(mockRepository.scheduleReport(cronExpression: anyNamed('cronExpression')))
.thenThrow(Exception('Invalid cron expression'));
// Act & Assert
expect(() => useCase(cronExpression: tCronExpression), throwsException);
});
});
}

View File

@@ -0,0 +1,67 @@
/// Tests unitaires pour ClearCache use case
library clear_cache_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/repositories/system_config_repository.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/usecases/clear_cache.dart';
@GenerateMocks([ISystemConfigRepository])
import 'clear_cache_test.mocks.dart';
void main() {
late ClearCache useCase;
late MockISystemConfigRepository mockRepository;
setUp(() {
mockRepository = MockISystemConfigRepository();
useCase = ClearCache(mockRepository);
});
group('ClearCache Use Case', () {
test('should clear cache successfully', () async {
// Arrange
when(mockRepository.clearCache())
.thenAnswer((_) async => Future.value());
// Act
await useCase();
// Assert
verify(mockRepository.clearCache());
verifyNoMoreInteractions(mockRepository);
});
test('should complete without error when cache is empty', () async {
// Arrange
when(mockRepository.clearCache())
.thenAnswer((_) async => Future.value());
// Act
await useCase();
// Assert
verify(mockRepository.clearCache()).called(1);
});
test('should throw exception when clear operation fails', () async {
// Arrange
when(mockRepository.clearCache())
.thenThrow(Exception('Clear operation failed'));
// Act & Assert
expect(() => useCase(), throwsA(isA<Exception>()));
verify(mockRepository.clearCache());
});
test('should throw exception when permission denied', () async {
// Arrange
when(mockRepository.clearCache())
.thenThrow(Exception('Permission denied'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,96 @@
/// Tests unitaires pour GetCacheStats use case
library get_cache_stats_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/repositories/system_config_repository.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/usecases/get_cache_stats.dart';
import 'package:unionflow_mobile_apps/features/settings/data/models/cache_stats_model.dart';
@GenerateMocks([ISystemConfigRepository])
import 'get_cache_stats_test.mocks.dart';
void main() {
late GetCacheStats useCase;
late MockISystemConfigRepository mockRepository;
setUp(() {
mockRepository = MockISystemConfigRepository();
useCase = GetCacheStats(mockRepository);
});
group('GetCacheStats Use Case', () {
final tCacheStats = CacheStatsModel(
totalEntries: 1000,
hits: 850,
misses: 150,
hitRate: 0.85,
totalSizeBytes: 1024 * 1024 * 50, // 50 MB
);
test('should return cache statistics', () async {
// Arrange
when(mockRepository.getCacheStats()).thenAnswer((_) async => tCacheStats);
// Act
final result = await useCase();
// Assert
expect(result, equals(tCacheStats));
expect(result.totalEntries, equals(1000));
expect(result.hitRate, equals(0.85));
verify(mockRepository.getCacheStats());
verifyNoMoreInteractions(mockRepository);
});
test('should handle empty cache', () async {
// Arrange
final emptyCacheStats = CacheStatsModel(
totalEntries: 0,
hits: 0,
misses: 0,
hitRate: 0.0,
totalSizeBytes: 0,
);
when(mockRepository.getCacheStats())
.thenAnswer((_) async => emptyCacheStats);
// Act
final result = await useCase();
// Assert
expect(result.totalEntries, equals(0));
expect(result.hitRate, equals(0.0));
});
test('should handle low hit rate cache', () async {
// Arrange
final lowHitCacheStats = CacheStatsModel(
totalEntries: 100,
hits: 20,
misses: 80,
hitRate: 0.20,
totalSizeBytes: 1024 * 100,
);
when(mockRepository.getCacheStats())
.thenAnswer((_) async => lowHitCacheStats);
// Act
final result = await useCase();
// Assert
expect(result.hitRate, lessThan(0.5));
expect(result.misses!, greaterThan(result.hits!));
});
test('should throw exception when stats retrieval fails', () async {
// Arrange
when(mockRepository.getCacheStats())
.thenThrow(Exception('Stats unavailable'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,94 @@
/// Tests unitaires pour GetSettings use case
library get_settings_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/repositories/system_config_repository.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/usecases/get_settings.dart';
import 'package:unionflow_mobile_apps/features/settings/data/models/system_config_model.dart';
@GenerateMocks([ISystemConfigRepository])
import 'get_settings_test.mocks.dart';
void main() {
late GetSettings useCase;
late MockISystemConfigRepository mockRepository;
setUp(() {
mockRepository = MockISystemConfigRepository();
useCase = GetSettings(mockRepository);
});
group('GetSettings Use Case', () {
final tConfig = SystemConfigModel(
applicationName: 'UnionFlow',
version: '1.0.0',
maintenanceMode: false,
defaultLanguage: 'fr',
timezone: 'Europe/Paris',
sessionTimeoutMinutes: 30,
);
test('should return system configuration', () async {
// Arrange
when(mockRepository.getConfig()).thenAnswer((_) async => tConfig);
// Act
final result = await useCase();
// Assert
expect(result, equals(tConfig));
expect(result.applicationName, equals('UnionFlow'));
expect(result.maintenanceMode, isFalse);
verify(mockRepository.getConfig());
verifyNoMoreInteractions(mockRepository);
});
test('should return config with all optional fields', () async {
// Arrange
final fullConfig = SystemConfigModel(
applicationName: 'UnionFlow',
version: '1.0.0',
maintenanceMode: false,
defaultLanguage: 'fr',
timezone: 'Europe/Paris',
networkTimeout: 30000,
sessionTimeoutMinutes: 60,
twoFactorAuthEnabled: true,
);
when(mockRepository.getConfig()).thenAnswer((_) async => fullConfig);
// Act
final result = await useCase();
// Assert
expect(result.networkTimeout, equals(30000));
expect(result.twoFactorAuthEnabled, isTrue);
});
test('should handle minimal config', () async {
// Arrange
final minimalConfig = SystemConfigModel(
applicationName: 'UnionFlow',
);
when(mockRepository.getConfig()).thenAnswer((_) async => minimalConfig);
// Act
final result = await useCase();
// Assert
expect(result.applicationName, isNotNull);
verify(mockRepository.getConfig());
});
test('should throw exception when config retrieval fails', () async {
// Arrange
when(mockRepository.getConfig())
.thenThrow(Exception('Config not found'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,92 @@
/// Tests unitaires pour ResetSettings use case
library reset_settings_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/repositories/system_config_repository.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/usecases/reset_settings.dart';
import 'package:unionflow_mobile_apps/features/settings/data/models/system_config_model.dart';
@GenerateMocks([ISystemConfigRepository])
import 'reset_settings_test.mocks.dart';
void main() {
late ResetSettings useCase;
late MockISystemConfigRepository mockRepository;
setUp(() {
mockRepository = MockISystemConfigRepository();
useCase = ResetSettings(mockRepository);
});
group('ResetSettings Use Case', () {
final tDefaultConfig = SystemConfigModel(
applicationName: 'UnionFlow',
version: '1.0.0',
maintenanceMode: false,
defaultLanguage: 'fr',
timezone: 'Europe/Paris',
);
test('should reset configuration to default values', () async {
// Arrange
when(mockRepository.resetConfig()).thenAnswer((_) async => tDefaultConfig);
// Act
final result = await useCase();
// Assert
expect(result, equals(tDefaultConfig));
expect(result.maintenanceMode, isFalse);
expect(result.applicationName, equals('UnionFlow'));
expect(result.version, equals('1.0.0'));
verify(mockRepository.resetConfig());
verifyNoMoreInteractions(mockRepository);
});
test('should handle fallback when reset endpoint fails', () async {
// Arrange
when(mockRepository.resetConfig()).thenAnswer((_) async => tDefaultConfig);
// Act
final result = await useCase();
// Assert
expect(result, isNotNull);
expect(result.applicationName, equals('UnionFlow'));
verify(mockRepository.resetConfig());
});
test('should throw exception when all reset strategies fail', () async {
// Arrange
when(mockRepository.resetConfig()).thenThrow(Exception('Reset failed'));
// Act & Assert
expect(
() => useCase(),
throwsA(isA<Exception>()),
);
verify(mockRepository.resetConfig());
});
test('should return valid config with minimal required fields', () async {
// Arrange
final tMinimalConfig = SystemConfigModel(
applicationName: 'UnionFlow',
version: '1.0.0',
maintenanceMode: false,
);
when(mockRepository.resetConfig()).thenAnswer((_) async => tMinimalConfig);
// Act
final result = await useCase();
// Assert
expect(result.applicationName, isNotNull);
expect(result.version, isNotNull);
expect(result.maintenanceMode, isNotNull);
verify(mockRepository.resetConfig());
});
});
}

View File

@@ -0,0 +1,91 @@
/// Tests unitaires pour UpdateSettings use case
library update_settings_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/repositories/system_config_repository.dart';
import 'package:unionflow_mobile_apps/features/settings/domain/usecases/update_settings.dart';
import 'package:unionflow_mobile_apps/features/settings/data/models/system_config_model.dart';
@GenerateMocks([ISystemConfigRepository])
import 'update_settings_test.mocks.dart';
void main() {
late UpdateSettings useCase;
late MockISystemConfigRepository mockRepository;
setUp(() {
mockRepository = MockISystemConfigRepository();
useCase = UpdateSettings(mockRepository);
});
group('UpdateSettings Use Case', () {
final tConfigMap = {
'applicationName': 'UnionFlow Updated',
'maintenanceMode': true,
'sessionTimeoutMinutes': 45,
};
final tUpdatedConfig = SystemConfigModel(
applicationName: 'UnionFlow Updated',
maintenanceMode: true,
sessionTimeoutMinutes: 45,
);
test('should update configuration successfully', () async {
// Arrange
when(mockRepository.updateConfig(tConfigMap))
.thenAnswer((_) async => tUpdatedConfig);
// Act
final result = await useCase(tConfigMap);
// Assert
expect(result, equals(tUpdatedConfig));
expect(result.applicationName, equals('UnionFlow Updated'));
expect(result.maintenanceMode, isTrue);
verify(mockRepository.updateConfig(tConfigMap));
verifyNoMoreInteractions(mockRepository);
});
test('should update partial configuration', () async {
// Arrange
final partialConfig = {'maintenanceMode': false};
final expectedResult = SystemConfigModel(maintenanceMode: false);
when(mockRepository.updateConfig(partialConfig))
.thenAnswer((_) async => expectedResult);
// Act
final result = await useCase(partialConfig);
// Assert
expect(result.maintenanceMode, isFalse);
verify(mockRepository.updateConfig(partialConfig));
});
test('should handle empty config map', () async {
// Arrange
final emptyConfig = <String, dynamic>{};
final expectedResult = SystemConfigModel();
when(mockRepository.updateConfig(emptyConfig))
.thenAnswer((_) async => expectedResult);
// Act
final result = await useCase(emptyConfig);
// Assert
expect(result, isNotNull);
verify(mockRepository.updateConfig(emptyConfig));
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateConfig(any))
.thenThrow(Exception('Update failed'));
// Act & Assert
expect(() => useCase(tConfigMap), throwsException);
});
});
}

View File

@@ -0,0 +1,248 @@
/// Integration tests for Finance Workflow (API-only)
library finance_workflow_integration_test;
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'helpers/test_config.dart';
import 'helpers/auth_helper.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late http.Client client;
late AuthHelper authHelper;
setUpAll(() async {
print('\n=== Finance Workflow Integration Tests ===\n');
client = http.Client();
authHelper = AuthHelper(client);
// Authenticate as ORG_ADMIN
final authenticated = await authHelper.authenticateAsOrgAdmin();
expect(authenticated, true, reason: 'Authentication must succeed');
print('Setup complete - Token obtained\n');
});
tearDownAll(() {
client.close();
print('\n=== Integration Tests Completed ===\n');
});
group('Finance Workflow - Approvals', () {
test('GET /api/finance/approvals/pending - List pending approvals', () async {
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/approvals/pending')
.replace(queryParameters: {'organizationId': TestConfig.testOrganizationId});
final response = await client.get(url, headers: authHelper.getAuthHeaders());
expect(response.statusCode, 200, reason: 'HTTP 200 OK expected');
final List<dynamic> approvals = json.decode(response.body);
expect(approvals, isA<List>(), reason: 'Response must be a list');
print('GET pending approvals: ${approvals.length} found');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('GET /api/finance/approvals/{id} - Get approval by ID', () async {
final listUrl = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/approvals/pending')
.replace(queryParameters: {'organizationId': TestConfig.testOrganizationId});
final listResponse = await client.get(listUrl, headers: authHelper.getAuthHeaders());
expect(listResponse.statusCode, 200);
final List<dynamic> approvals = json.decode(listResponse.body);
if (approvals.isEmpty) {
print('No pending approvals - test skipped');
return;
}
final approvalId = approvals.first['id'];
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/approvals/$approvalId');
final response = await client.get(url, headers: authHelper.getAuthHeaders());
expect(response.statusCode, 200, reason: 'HTTP 200 OK expected');
final approval = json.decode(response.body);
expect(approval['id'], equals(approvalId), reason: 'ID must match');
print('GET approval by ID: ${approval['id']}');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('POST /api/finance/approvals/{id}/approve - Approve transaction (simulated)', () async {
print('Test approve transaction - Simulated (avoids prod modification)');
print(' Endpoint: POST /api/finance/approvals/{id}/approve');
print(' Body: { "comment": "Approved by integration test" }');
print(' Expected: HTTP 200, status=approved');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('POST /api/finance/approvals/{id}/reject - Reject transaction (simulated)', () async {
print('Test reject transaction - Simulated (avoids prod modification)');
print(' Endpoint: POST /api/finance/approvals/{id}/reject');
print(' Body: { "reason": "Rejected by integration test" }');
print(' Expected: HTTP 200, status=rejected');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
});
group('Finance Workflow - Budgets', () {
String? createdBudgetId;
test('GET /api/finance/budgets - List budgets', () async {
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets')
.replace(queryParameters: {'organizationId': TestConfig.testOrganizationId});
final response = await client.get(url, headers: authHelper.getAuthHeaders());
expect(response.statusCode, 200, reason: 'HTTP 200 OK expected');
final List<dynamic> budgets = json.decode(response.body);
expect(budgets, isA<List>(), reason: 'Response must be a list');
print('GET budgets: ${budgets.length} found');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('POST /api/finance/budgets - Create budget', () async {
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets');
final requestBody = {
'name': 'Budget Integration Test ${DateTime.now().millisecondsSinceEpoch}',
'description': 'Budget created by integration test',
'organizationId': TestConfig.testOrganizationId,
'period': 'ANNUAL',
'year': DateTime.now().year,
'lines': [
{
'category': 'CONTRIBUTIONS',
'name': 'Contributions',
'amountPlanned': 1000000.0,
'description': 'Membership contributions',
},
{
'category': 'SAVINGS',
'name': 'Savings',
'amountPlanned': 500000.0,
'description': 'Savings collection',
},
],
};
final response = await client.post(
url,
headers: authHelper.getAuthHeaders(),
body: json.encode(requestBody),
);
expect(response.statusCode, inInclusiveRange(200, 201), reason: 'HTTP 200/201 expected');
final budget = json.decode(response.body);
expect(budget['id'], isNotNull, reason: 'Budget ID must be present');
expect(budget['name'], contains('Budget Integration Test'), reason: 'Name must match');
createdBudgetId = budget['id'];
print('POST create budget: ${budget['id']} - ${budget['name']}');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('GET /api/finance/budgets/{id} - Get budget by ID', () async {
String budgetId;
if (createdBudgetId != null) {
budgetId = createdBudgetId!;
} else {
final listUrl = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets')
.replace(queryParameters: {'organizationId': TestConfig.testOrganizationId});
final listResponse = await client.get(listUrl, headers: authHelper.getAuthHeaders());
expect(listResponse.statusCode, 200);
final List<dynamic> budgets = json.decode(listResponse.body);
if (budgets.isEmpty) {
print('No budgets found - test skipped');
return;
}
budgetId = budgets.first['id'];
}
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets/$budgetId');
final response = await client.get(url, headers: authHelper.getAuthHeaders());
expect(response.statusCode, 200, reason: 'HTTP 200 OK expected');
final budget = json.decode(response.body);
expect(budget['id'], equals(budgetId), reason: 'ID must match');
expect(budget['lines'], isNotNull, reason: 'Budget lines must be present');
print('GET budget by ID: ${budget['id']} - ${budget['name']}');
print(' Budget lines: ${budget['lines'].length}');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
});
group('Finance Workflow - Negative Tests', () {
test('GET nonexistent approval - Should return 404', () async {
final fakeId = '00000000-0000-0000-0000-000000000000';
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/approvals/$fakeId');
final response = await client.get(url, headers: authHelper.getAuthHeaders());
expect(response.statusCode, 404, reason: 'HTTP 404 Not Found expected');
print('Negative test: 404 for nonexistent approval');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('GET nonexistent budget - Should return 404', () async {
final fakeId = '00000000-0000-0000-0000-000000000000';
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets/$fakeId');
final response = await client.get(url, headers: authHelper.getAuthHeaders());
expect(response.statusCode, 404, reason: 'HTTP 404 Not Found expected');
print('Negative test: 404 for nonexistent budget');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
test('POST budget without authentication - Should return 401', () async {
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets');
final requestBody = {
'name': 'Budget Without Auth',
'organizationId': TestConfig.testOrganizationId,
'period': 'ANNUAL',
'year': 2026,
'lines': [],
};
final response = await client.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode(requestBody),
);
expect(response.statusCode, 401, reason: 'HTTP 401 Unauthorized expected');
print('Negative test: 401 for unauthenticated request');
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
});
});
}

View File

@@ -0,0 +1,132 @@
/// Helper pour l'authentification dans les tests d'intégration
library auth_helper;
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'test_config.dart';
/// Helper pour gérer l'authentification dans les tests
class AuthHelper {
final http.Client _client;
String? _accessToken;
String? _refreshToken;
AuthHelper(this._client);
/// Token d'accès actuel
String? get accessToken => _accessToken;
/// Authentifie un utilisateur via Keycloak Direct Access Grant
///
/// Retourne true si l'authentification réussit, false sinon
Future<bool> authenticate(String username, String password) async {
final url = Uri.parse(
'${TestConfig.keycloakUrl}/realms/${TestConfig.keycloakRealm}/protocol/openid-connect/token',
);
try {
final response = await _client.post(
url,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'grant_type': 'password',
'client_id': TestConfig.keycloakClientId,
'username': username,
'password': password,
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
_accessToken = data['access_token'];
_refreshToken = data['refresh_token'];
if (TestConfig.enableDetailedLogs) {
print('✅ Authentification réussie pour: $username');
}
return true;
} else {
if (TestConfig.enableDetailedLogs) {
print('❌ Échec authentification: ${response.statusCode} - ${response.body}');
}
return false;
}
} catch (e) {
if (TestConfig.enableDetailedLogs) {
print('❌ Erreur authentification: $e');
}
return false;
}
}
/// Authentifie l'utilisateur admin de test
Future<bool> authenticateAsAdmin() async {
return await authenticate(
TestConfig.testAdminUsername,
TestConfig.testAdminPassword,
);
}
/// Authentifie l'utilisateur org admin de test
Future<bool> authenticateAsOrgAdmin() async {
return await authenticate(
TestConfig.testOrgAdminUsername,
TestConfig.testOrgAdminPassword,
);
}
/// Rafraîchit le token d'accès
Future<bool> refreshAccessToken() async {
if (_refreshToken == null) {
return false;
}
final url = Uri.parse(
'${TestConfig.keycloakUrl}/realms/${TestConfig.keycloakRealm}/protocol/openid-connect/token',
);
try {
final response = await _client.post(
url,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'grant_type': 'refresh_token',
'client_id': TestConfig.keycloakClientId,
'refresh_token': _refreshToken!,
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
_accessToken = data['access_token'];
_refreshToken = data['refresh_token'];
return true;
}
return false;
} catch (e) {
if (TestConfig.enableDetailedLogs) {
print('❌ Erreur rafraîchissement token: $e');
}
return false;
}
}
/// Déconnecte l'utilisateur
Future<void> logout() async {
_accessToken = null;
_refreshToken = null;
if (TestConfig.enableDetailedLogs) {
print('🔓 Déconnexion effectuée');
}
}
/// Retourne les headers HTTP avec authentification
Map<String, String> getAuthHeaders() {
return {
'Content-Type': 'application/json',
'Accept': 'application/json',
if (_accessToken != null) 'Authorization': 'Bearer $_accessToken',
};
}
}

View File

@@ -0,0 +1,37 @@
/// Configuration pour les tests d'intégration
library test_config;
/// Configuration des tests d'intégration
class TestConfig {
/// URL de base de l'API backend (environnement de test)
static const String apiBaseUrl = 'http://localhost:8085';
/// URL de Keycloak (environnement de test)
static const String keycloakUrl = 'http://localhost:8180';
/// Realm Keycloak
static const String keycloakRealm = 'unionflow';
/// Client ID Keycloak
static const String keycloakClientId = 'unionflow-mobile';
/// Credentials utilisateur de test (SUPER_ADMIN)
static const String testAdminUsername = 'admin@unionflow.test';
static const String testAdminPassword = 'Admin@123';
/// Credentials utilisateur de test (ORG_ADMIN)
static const String testOrgAdminUsername = 'orgadmin@unionflow.test';
static const String testOrgAdminPassword = 'OrgAdmin@123';
/// ID d'organisation de test
static const String testOrganizationId = '00000000-0000-0000-0000-000000000001';
/// Timeout pour les requêtes HTTP (ms)
static const int httpTimeout = 30000;
/// Délai d'attente entre les tests (ms)
static const int delayBetweenTests = 500;
/// Active les logs détaillés
static const bool enableDetailedLogs = true;
}