feat(mobile): Implement Keycloak WebView authentication with HTTP callback
- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
This commit is contained in:
222
unionflow-mobile-apps/test/error_handling_test.dart
Normal file
222
unionflow-mobile-apps/test/error_handling_test.dart
Normal file
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../lib/core/error/error_handler.dart';
|
||||
import '../lib/core/validation/form_validator.dart';
|
||||
import '../lib/core/failures/failures.dart';
|
||||
|
||||
void main() {
|
||||
group('FormValidator Tests', () {
|
||||
test('should validate required fields correctly', () {
|
||||
// Test champ requis vide
|
||||
expect(FormValidator.required(''), 'Ce champ est requis');
|
||||
expect(FormValidator.required(' '), 'Ce champ est requis');
|
||||
expect(FormValidator.required(null), 'Ce champ est requis');
|
||||
|
||||
// Test champ requis valide
|
||||
expect(FormValidator.required('valeur'), null);
|
||||
expect(FormValidator.required(' valeur '), null);
|
||||
});
|
||||
|
||||
test('should validate email correctly', () {
|
||||
// Test emails invalides
|
||||
expect(FormValidator.email(''), 'L\'email est requis');
|
||||
expect(FormValidator.email('invalid'), 'Format d\'email invalide');
|
||||
expect(FormValidator.email('test@'), 'Format d\'email invalide');
|
||||
expect(FormValidator.email('@domain.com'), 'Format d\'email invalide');
|
||||
expect(FormValidator.email('test.domain.com'), 'Format d\'email invalide');
|
||||
|
||||
// Test emails valides
|
||||
expect(FormValidator.email('test@domain.com'), null);
|
||||
expect(FormValidator.email('user.name@example.org'), null);
|
||||
expect(FormValidator.email('test123@sub.domain.co.uk'), null);
|
||||
});
|
||||
|
||||
test('should validate phone numbers correctly', () {
|
||||
// Test téléphones invalides
|
||||
expect(FormValidator.phone(''), 'Le numéro de téléphone est requis');
|
||||
expect(FormValidator.phone('123'), 'Format de téléphone invalide (ex: +225XXXXXXXX)');
|
||||
expect(FormValidator.phone('abcdefgh'), 'Format de téléphone invalide (ex: +225XXXXXXXX)');
|
||||
|
||||
// Test téléphones valides
|
||||
expect(FormValidator.phone('12345678'), null);
|
||||
expect(FormValidator.phone('+22512345678'), null);
|
||||
expect(FormValidator.phone('1234567890'), null);
|
||||
expect(FormValidator.phone('+225 12 34 56 78'), null); // Avec espaces
|
||||
});
|
||||
|
||||
test('should validate names correctly', () {
|
||||
// Test noms invalides
|
||||
expect(FormValidator.name(''), 'Ce champ est requis');
|
||||
expect(FormValidator.name('A'), 'Ce champ doit contenir au moins 2 caractères');
|
||||
expect(FormValidator.name('123'), 'Ce champ ne peut contenir que des lettres');
|
||||
expect(FormValidator.name('Name@123'), 'Ce champ ne peut contenir que des lettres');
|
||||
|
||||
// Test noms valides
|
||||
expect(FormValidator.name('Jean'), null);
|
||||
expect(FormValidator.name('Marie-Claire'), null);
|
||||
expect(FormValidator.name('Jean-Baptiste'), null);
|
||||
expect(FormValidator.name('O\'Connor'), null);
|
||||
expect(FormValidator.name('José'), null);
|
||||
expect(FormValidator.name('François'), null);
|
||||
});
|
||||
|
||||
test('should validate birth dates correctly', () {
|
||||
final now = DateTime.now();
|
||||
final validDate = DateTime(now.year - 25, now.month, now.day);
|
||||
final futureDate = DateTime(now.year + 1, now.month, now.day);
|
||||
final tooYoungDate = DateTime(now.year - 10, now.month, now.day);
|
||||
final tooOldDate = DateTime(now.year - 150, now.month, now.day);
|
||||
|
||||
// Test dates invalides
|
||||
expect(FormValidator.birthDate(null), 'La date de naissance est requise');
|
||||
expect(FormValidator.birthDate(futureDate), 'La date de naissance ne peut pas être dans le futur');
|
||||
expect(FormValidator.birthDate(tooYoungDate, minAge: 16), 'L\'âge minimum requis est de 16 ans');
|
||||
expect(FormValidator.birthDate(tooOldDate, maxAge: 120), 'L\'âge maximum autorisé est de 120 ans');
|
||||
|
||||
// Test date valide
|
||||
expect(FormValidator.birthDate(validDate), null);
|
||||
});
|
||||
|
||||
test('should validate member numbers correctly', () {
|
||||
// Test numéros invalides
|
||||
expect(FormValidator.memberNumber(''), 'Le numéro de membre est requis');
|
||||
expect(FormValidator.memberNumber('123'), 'Format invalide (ex: MBR001)');
|
||||
expect(FormValidator.memberNumber('MBR'), 'Format invalide (ex: MBR001)');
|
||||
expect(FormValidator.memberNumber('MBR12'), 'Format invalide (ex: MBR001)');
|
||||
|
||||
// Test numéros valides
|
||||
expect(FormValidator.memberNumber('MBR001'), null);
|
||||
expect(FormValidator.memberNumber('MBR123456'), null);
|
||||
});
|
||||
|
||||
test('should combine validators correctly', () {
|
||||
final combinedValidator = FormValidator.combine([
|
||||
(value) => FormValidator.required(value),
|
||||
(value) => FormValidator.minLength(value, 3),
|
||||
(value) => FormValidator.maxLength(value, 10),
|
||||
]);
|
||||
|
||||
// Test avec erreurs
|
||||
expect(combinedValidator(''), 'Ce champ est requis');
|
||||
expect(combinedValidator('ab'), 'Ce champ doit contenir au moins 3 caractères');
|
||||
expect(combinedValidator('12345678901'), 'Ce champ ne peut pas dépasser 10 caractères');
|
||||
|
||||
// Test valide
|
||||
expect(combinedValidator('valide'), null);
|
||||
});
|
||||
|
||||
test('should validate complete member data', () {
|
||||
final validMemberData = {
|
||||
'prenom': 'Jean',
|
||||
'nom': 'Dupont',
|
||||
'email': 'jean.dupont@email.com',
|
||||
'telephone': '+22512345678',
|
||||
'dateNaissance': DateTime(1990, 1, 1),
|
||||
'adresse': '123 Rue de la Paix',
|
||||
'profession': 'Ingénieur',
|
||||
};
|
||||
|
||||
final invalidMemberData = {
|
||||
'prenom': '',
|
||||
'nom': 'D',
|
||||
'email': 'invalid-email',
|
||||
'telephone': '123',
|
||||
'dateNaissance': DateTime.now().add(const Duration(days: 1)),
|
||||
'adresse': '',
|
||||
'profession': '',
|
||||
};
|
||||
|
||||
// Test données valides
|
||||
final validErrors = FormValidator.validateMember(validMemberData);
|
||||
expect(validErrors.isEmpty, true);
|
||||
|
||||
// Test données invalides
|
||||
final invalidErrors = FormValidator.validateMember(invalidMemberData);
|
||||
expect(invalidErrors.isNotEmpty, true);
|
||||
expect(invalidErrors.containsKey('prenom'), true);
|
||||
expect(invalidErrors.containsKey('nom'), true);
|
||||
expect(invalidErrors.containsKey('email'), true);
|
||||
expect(invalidErrors.containsKey('telephone'), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('ErrorHandler Tests', () {
|
||||
test('should analyze DioException correctly', () {
|
||||
// Test DioException de type connectTimeout
|
||||
final timeoutException = DioException(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
message: 'Connection timeout',
|
||||
);
|
||||
|
||||
// Nous ne pouvons pas tester directement _analyzeError car elle est privée
|
||||
// Mais nous pouvons tester que la classe ErrorHandler existe et compile
|
||||
expect(ErrorHandler, isNotNull);
|
||||
});
|
||||
|
||||
test('should create appropriate failure types', () {
|
||||
// Test NetworkFailure
|
||||
final networkFailure = NetworkFailure.noConnection();
|
||||
expect(networkFailure.message, 'Aucune connexion internet disponible');
|
||||
expect(networkFailure.code, 'NO_CONNECTION');
|
||||
|
||||
// Test ServerFailure
|
||||
final serverFailure = ServerFailure.internalError();
|
||||
expect(serverFailure.message, 'Erreur interne du serveur');
|
||||
expect(serverFailure.statusCode, 500);
|
||||
|
||||
// Test ValidationFailure
|
||||
final validationFailure = ValidationFailure.requiredField('email');
|
||||
expect(validationFailure.message, 'Champ requis manquant');
|
||||
expect(validationFailure.fieldErrors?['email']?.first, 'Ce champ est requis');
|
||||
|
||||
// Test AuthFailure
|
||||
final authFailure = AuthFailure.tokenExpired();
|
||||
expect(authFailure.message, 'Session expirée, veuillez vous reconnecter');
|
||||
expect(authFailure.code, 'TOKEN_EXPIRED');
|
||||
});
|
||||
|
||||
test('should handle failure equality correctly', () {
|
||||
final failure1 = NetworkFailure.noConnection();
|
||||
final failure2 = NetworkFailure.noConnection();
|
||||
final failure3 = NetworkFailure.timeout();
|
||||
|
||||
expect(failure1 == failure2, true);
|
||||
expect(failure1 == failure3, false);
|
||||
expect(failure1.hashCode == failure2.hashCode, true);
|
||||
});
|
||||
});
|
||||
|
||||
group('Failure Classes Tests', () {
|
||||
test('should create DataFailure correctly', () {
|
||||
final notFoundFailure = DataFailure.notFound('Membre');
|
||||
expect(notFoundFailure.message, 'Membre non trouvé(e)');
|
||||
expect(notFoundFailure.code, 'NOT_FOUND');
|
||||
expect(notFoundFailure.details?['resource'], 'Membre');
|
||||
|
||||
final conflictFailure = DataFailure.conflict('Email déjà utilisé');
|
||||
expect(conflictFailure.message, 'Conflit de données : Email déjà utilisé');
|
||||
expect(conflictFailure.code, 'CONFLICT');
|
||||
});
|
||||
|
||||
test('should create FileFailure correctly', () {
|
||||
final fileNotFound = FileFailure.notFound('/path/to/file.txt');
|
||||
expect(fileNotFound.message, 'Fichier non trouvé');
|
||||
expect(fileNotFound.details?['filePath'], '/path/to/file.txt');
|
||||
|
||||
final invalidFormat = FileFailure.invalidFormat('PDF');
|
||||
expect(invalidFormat.message, 'Format de fichier invalide');
|
||||
expect(invalidFormat.details?['expectedFormat'], 'PDF');
|
||||
});
|
||||
|
||||
test('should create UnknownFailure from exception', () {
|
||||
final exception = Exception('Test exception');
|
||||
final unknownFailure = UnknownFailure.fromException(exception);
|
||||
|
||||
expect(unknownFailure.message.contains('Test exception'), true);
|
||||
expect(unknownFailure.code, 'UNKNOWN_ERROR');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user