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:
DahoudG
2025-09-15 01:44:16 +00:00
parent 73459b3092
commit f89f6167cc
290 changed files with 34563 additions and 3528 deletions

View 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');
});
});
}