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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user