Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
265
test_integration/finance_workflow_api_test.dart
Normal file
265
test_integration/finance_workflow_api_test.dart
Normal file
@@ -0,0 +1,265 @@
|
||||
/// Integration tests for Finance Workflow API (Pure Dart - no Flutter binding)
|
||||
library finance_workflow_api_test;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'helpers/test_config.dart';
|
||||
import 'helpers/auth_helper.dart';
|
||||
|
||||
void main() {
|
||||
late http.Client client;
|
||||
late AuthHelper authHelper;
|
||||
|
||||
setUpAll(() async {
|
||||
print('\n╔═══════════════════════════════════════════════╗');
|
||||
print('║ Finance Workflow Integration Tests (API) ║');
|
||||
print('╚═══════════════════════════════════════════════╝\n');
|
||||
|
||||
client = http.Client();
|
||||
authHelper = AuthHelper(client);
|
||||
|
||||
print('→ Authenticating as ${TestConfig.testOrgAdminUsername}...');
|
||||
final authenticated = await authHelper.authenticateAsOrgAdmin();
|
||||
|
||||
if (!authenticated) {
|
||||
print('✗ Authentication FAILED');
|
||||
throw Exception('Authentication failed - check Keycloak and credentials');
|
||||
}
|
||||
|
||||
print('✓ Authentication successful\n');
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
client.close();
|
||||
print('\n╔═══════════════════════════════════════════════╗');
|
||||
print('║ Integration Tests Completed ║');
|
||||
print('╚═══════════════════════════════════════════════╝\n');
|
||||
});
|
||||
|
||||
group('Finance Workflow - Approvals API', () {
|
||||
test('GET /api/finance/approvals/pending - List pending approvals', () async {
|
||||
print('\n[TEST] GET pending approvals...');
|
||||
|
||||
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, equals(200), reason: 'HTTP 200 OK expected');
|
||||
|
||||
final List<dynamic> approvals = json.decode(response.body);
|
||||
expect(approvals, isA<List>());
|
||||
|
||||
print(' ✓ Status: ${response.statusCode}');
|
||||
print(' ✓ Approvals found: ${approvals.length}');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
|
||||
test('GET /api/finance/approvals/{id} - Get approval by ID', () async {
|
||||
print('\n[TEST] GET approval by ID...');
|
||||
|
||||
// First, get list of approvals
|
||||
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, equals(200));
|
||||
|
||||
final List<dynamic> approvals = json.decode(listResponse.body);
|
||||
|
||||
if (approvals.isEmpty) {
|
||||
print(' ⚠ No pending approvals - test skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
final approvalId = approvals.first['id'];
|
||||
|
||||
// Get specific approval
|
||||
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/approvals/$approvalId');
|
||||
final response = await client.get(url, headers: authHelper.getAuthHeaders());
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
|
||||
final approval = json.decode(response.body);
|
||||
expect(approval['id'], equals(approvalId));
|
||||
|
||||
print(' ✓ Status: ${response.statusCode}');
|
||||
print(' ✓ Approval ID: ${approval['id']}');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
});
|
||||
|
||||
group('Finance Workflow - Budgets API', () {
|
||||
String? createdBudgetId;
|
||||
|
||||
test('GET /api/finance/budgets - List budgets', () async {
|
||||
print('\n[TEST] GET budgets list...');
|
||||
|
||||
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, equals(200));
|
||||
|
||||
final List<dynamic> budgets = json.decode(response.body);
|
||||
expect(budgets, isA<List>());
|
||||
|
||||
print(' ✓ Status: ${response.statusCode}');
|
||||
print(' ✓ Budgets found: ${budgets.length}');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
|
||||
test('POST /api/finance/budgets - Create budget', () async {
|
||||
print('\n[TEST] POST create budget...');
|
||||
|
||||
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets');
|
||||
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final requestBody = {
|
||||
'name': 'Budget Integration Test $timestamp',
|
||||
'description': 'Budget created by automated integration test',
|
||||
'organizationId': TestConfig.testOrganizationId,
|
||||
'period': 'ANNUAL',
|
||||
'year': DateTime.now().year,
|
||||
'lines': [
|
||||
{
|
||||
'category': 'CONTRIBUTIONS',
|
||||
'name': 'Member Contributions',
|
||||
'amountPlanned': 1000000.0,
|
||||
'description': 'Monthly member contributions',
|
||||
},
|
||||
{
|
||||
'category': 'SAVINGS',
|
||||
'name': 'Savings Deposits',
|
||||
'amountPlanned': 500000.0,
|
||||
'description': 'Member savings deposits',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
final response = await client.post(
|
||||
url,
|
||||
headers: authHelper.getAuthHeaders(),
|
||||
body: json.encode(requestBody),
|
||||
);
|
||||
|
||||
expect(response.statusCode, inInclusiveRange(200, 201));
|
||||
|
||||
final budget = json.decode(response.body);
|
||||
expect(budget['id'], isNotNull);
|
||||
expect(budget['name'], contains('Budget Integration Test'));
|
||||
|
||||
createdBudgetId = budget['id'];
|
||||
|
||||
print(' ✓ Status: ${response.statusCode}');
|
||||
print(' ✓ Budget created: ${budget['id']}');
|
||||
print(' ✓ Budget name: ${budget['name']}');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
|
||||
test('GET /api/finance/budgets/{id} - Get budget by ID', () async {
|
||||
print('\n[TEST] GET budget by ID...');
|
||||
|
||||
String budgetId;
|
||||
|
||||
if (createdBudgetId != null) {
|
||||
budgetId = createdBudgetId!;
|
||||
} else {
|
||||
// Fetch any existing budget
|
||||
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, equals(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, equals(200));
|
||||
|
||||
final budget = json.decode(response.body);
|
||||
expect(budget['id'], equals(budgetId));
|
||||
expect(budget['lines'], isNotNull);
|
||||
|
||||
print(' ✓ Status: ${response.statusCode}');
|
||||
print(' ✓ Budget ID: ${budget['id']}');
|
||||
print(' ✓ Budget name: ${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 {
|
||||
print('\n[TEST] GET nonexistent approval (expect 404)...');
|
||||
|
||||
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, equals(404));
|
||||
|
||||
print(' ✓ Status: ${response.statusCode} (404 as expected)');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
|
||||
test('GET nonexistent budget - Should return 404', () async {
|
||||
print('\n[TEST] GET nonexistent budget (expect 404)...');
|
||||
|
||||
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, equals(404));
|
||||
|
||||
print(' ✓ Status: ${response.statusCode} (404 as expected)');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
|
||||
test('POST budget without auth - Should return 401', () async {
|
||||
print('\n[TEST] POST budget without authentication (expect 401)...');
|
||||
|
||||
final url = Uri.parse('${TestConfig.apiBaseUrl}/api/finance/budgets');
|
||||
final requestBody = {
|
||||
'name': 'Unauthorized Budget',
|
||||
'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, equals(401));
|
||||
|
||||
print(' ✓ Status: ${response.statusCode} (401 as expected)');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: TestConfig.delayBetweenTests));
|
||||
});
|
||||
});
|
||||
}
|
||||
132
test_integration/helpers/auth_helper.dart
Normal file
132
test_integration/helpers/auth_helper.dart
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
37
test_integration/helpers/test_config.dart
Normal file
37
test_integration/helpers/test_config.dart
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user