Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

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;
}