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:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -0,0 +1,103 @@
/// Tests unitaires pour CreateOrganization use case
library create_organization_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/create_organization.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'create_organization_test.mocks.dart';
void main() {
late CreateOrganization useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = CreateOrganization(mockRepository);
});
group('CreateOrganization Use Case', () {
final tOrganization = OrganizationModel(
nom: 'Nouvelle Organisation',
nomCourt: 'NO',
email: 'contact@nouvelle.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.enCreation,
);
final tCreatedOrganization = OrganizationModel(
id: 'org123',
nom: 'Nouvelle Organisation',
nomCourt: 'NO',
email: 'contact@nouvelle.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should create organization successfully', () async {
// Arrange
when(mockRepository.createOrganization(tOrganization))
.thenAnswer((_) async => tCreatedOrganization);
// Act
final result = await useCase(tOrganization);
// Assert
expect(result, equals(tCreatedOrganization));
expect(result.id, isNotNull);
expect(result.nom, equals('Nouvelle Organisation'));
verify(mockRepository.createOrganization(tOrganization));
verifyNoMoreInteractions(mockRepository);
});
test('should create organization with minimal required fields', () async {
// Arrange
final minimalOrg = OrganizationModel(
nom: 'Minimal Org',
nomCourt: 'MO',
email: 'minimal@org.com',
typeOrganisation: TypeOrganization.cooperative,
statut: StatutOrganization.enCreation,
);
final createdMinimal = OrganizationModel(
id: 'org456',
nom: 'Minimal Org',
nomCourt: 'MO',
email: 'minimal@org.com',
typeOrganisation: TypeOrganization.cooperative,
statut: StatutOrganization.active,
);
when(mockRepository.createOrganization(minimalOrg))
.thenAnswer((_) async => createdMinimal);
// Act
final result = await useCase(minimalOrg);
// Assert
expect(result.id, isNotNull);
expect(result.nom, equals('Minimal Org'));
});
test('should throw exception when email already exists', () async {
// Arrange
when(mockRepository.createOrganization(any))
.thenThrow(Exception('Email déjà utilisé'));
// Act & Assert
expect(() => useCase(tOrganization), throwsA(isA<Exception>()));
});
test('should throw exception when validation fails', () async {
// Arrange
when(mockRepository.createOrganization(any))
.thenThrow(Exception('Données invalides'));
// Act & Assert
expect(() => useCase(tOrganization), throwsException);
});
});
}

View File

@@ -0,0 +1,72 @@
/// Tests unitaires pour DeleteOrganization use case
library delete_organization_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/delete_organization.dart';
@GenerateMocks([IOrganizationRepository])
import 'delete_organization_test.mocks.dart';
void main() {
late DeleteOrganization useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = DeleteOrganization(mockRepository);
});
group('DeleteOrganization Use Case', () {
const tOrganizationId = 'org1';
test('should delete organization successfully', () async {
// Arrange
when(mockRepository.deleteOrganization(tOrganizationId))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tOrganizationId);
// Assert
verify(mockRepository.deleteOrganization(tOrganizationId));
verifyNoMoreInteractions(mockRepository);
});
test('should throw exception when organization not found', () async {
// Arrange
when(mockRepository.deleteOrganization(any))
.thenThrow(Exception('Organisation non trouvée'));
// Act & Assert
expect(
() => useCase(tOrganizationId),
throwsA(isA<Exception>()),
);
verify(mockRepository.deleteOrganization(tOrganizationId));
});
test('should throw exception when organization has members', () async {
// Arrange
when(mockRepository.deleteOrganization(any))
.thenThrow(Exception('Organisation contient des membres'));
// Act & Assert
expect(
() => useCase(tOrganizationId),
throwsA(isA<Exception>()),
);
});
test('should throw exception when deletion fails', () async {
// Arrange
when(mockRepository.deleteOrganization(any))
.thenThrow(Exception('Suppression échouée'));
// Act & Assert
expect(() => useCase(tOrganizationId), throwsException);
});
});
}

View File

@@ -0,0 +1,87 @@
/// Tests unitaires pour GetOrganizationById use case
library get_organization_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_by_id.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'get_organization_by_id_test.mocks.dart';
void main() {
late GetOrganizationById useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = GetOrganizationById(mockRepository);
});
group('GetOrganizationById Use Case', () {
const tOrganizationId = 'org1';
final tOrganization = OrganizationModel(
id: tOrganizationId,
nom: 'Organisation Alpha',
nomCourt: 'OA',
email: 'contact@alpha.org',
telephone: '+33123456789',
adresse: '123 Rue de Paris',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should return organization by id', () async {
// Arrange
when(mockRepository.getOrganizationById(tOrganizationId))
.thenAnswer((_) async => tOrganization);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result, equals(tOrganization));
expect(result?.id, equals(tOrganizationId));
verify(mockRepository.getOrganizationById(tOrganizationId));
verifyNoMoreInteractions(mockRepository);
});
test('should return null when organization not found', () async {
// Arrange
when(mockRepository.getOrganizationById(any))
.thenAnswer((_) async => null);
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, isNull);
verify(mockRepository.getOrganizationById('nonexistent'));
});
test('should return organization with all fields populated', () async {
// Arrange
when(mockRepository.getOrganizationById(tOrganizationId))
.thenAnswer((_) async => tOrganization);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result?.nom, isNotNull);
expect(result?.email, isNotNull);
expect(result?.statut, equals(StatutOrganization.active));
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getOrganizationById(any))
.thenThrow(Exception('Database error'));
// Act & Assert
expect(() => useCase(tOrganizationId), throwsException);
});
});
}

View File

@@ -0,0 +1,88 @@
/// Tests unitaires pour GetOrganizationMembers use case
library get_organization_members_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_members.dart';
@GenerateMocks([IOrganizationRepository])
import 'get_organization_members_test.mocks.dart';
void main() {
late GetOrganizationMembers useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = GetOrganizationMembers(mockRepository);
});
group('GetOrganizationMembers Use Case', () {
const tOrganizationId = 'org1';
final tMembersList = [
{
'id': 'membre1',
'nom': 'Dupont',
'prenom': 'Jean',
'email': 'jean.dupont@example.com',
},
{
'id': 'membre2',
'nom': 'Martin',
'prenom': 'Marie',
'email': 'marie.martin@example.com',
},
];
test('should return list of organization members', () async {
// Arrange
when(mockRepository.getOrganizationMembers(tOrganizationId))
.thenAnswer((_) async => tMembersList);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result, equals(tMembersList));
expect(result.length, equals(2));
verify(mockRepository.getOrganizationMembers(tOrganizationId));
verifyNoMoreInteractions(mockRepository);
});
test('should return empty list when organization has no members', () async {
// Arrange
when(mockRepository.getOrganizationMembers(tOrganizationId))
.thenAnswer((_) async => []);
// Act
final result = await useCase(tOrganizationId);
// Assert
expect(result, isEmpty);
verify(mockRepository.getOrganizationMembers(tOrganizationId));
});
test('should throw exception when organization not found', () async {
// Arrange
when(mockRepository.getOrganizationMembers(any))
.thenThrow(Exception('Organisation non trouvée'));
// Act & Assert
expect(
() => useCase(tOrganizationId),
throwsA(isA<Exception>()),
);
});
test('should throw exception when retrieval fails', () async {
// Arrange
when(mockRepository.getOrganizationMembers(any))
.thenThrow(Exception('Erreur de récupération'));
// Act & Assert
expect(() => useCase(tOrganizationId), throwsException);
});
});
}

View File

@@ -0,0 +1,106 @@
/// Tests unitaires pour GetOrganizations use case
library get_organizations_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organizations.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'get_organizations_test.mocks.dart';
void main() {
late GetOrganizations useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = GetOrganizations(mockRepository);
});
group('GetOrganizations Use Case', () {
final tOrganizationList = [
OrganizationModel(
id: 'org1',
nom: 'Organisation Alpha',
nomCourt: 'OA',
email: 'contact@alpha.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
),
OrganizationModel(
id: 'org2',
nom: 'Organisation Beta',
nomCourt: 'OB',
email: 'contact@beta.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
),
];
test('should return list of organizations', () async {
// Arrange
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => tOrganizationList);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result, equals(tOrganizationList));
expect(result.length, equals(2));
verify(mockRepository.getOrganizations(page: 0, size: 20));
verifyNoMoreInteractions(mockRepository);
});
test('should filter organizations by search query', () async {
// Arrange
final filteredList = [tOrganizationList[0]];
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: 'Alpha',
)).thenAnswer((_) async => filteredList);
// Act
final result = await useCase(page: 0, size: 20, recherche: 'Alpha');
// Assert
expect(result.length, equals(1));
expect(result.first.nom, contains('Alpha'));
verify(mockRepository.getOrganizations(page: 0, size: 20, recherche: 'Alpha'));
});
test('should return empty list when no organizations exist', () async {
// Arrange
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenAnswer((_) async => []);
// Act
final result = await useCase(page: 0, size: 20);
// Assert
expect(result, isEmpty);
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getOrganizations(
page: anyNamed('page'),
size: anyNamed('size'),
recherche: anyNamed('recherche'),
)).thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(page: 0, size: 20), throwsException);
});
});
}

View File

@@ -0,0 +1,91 @@
/// Tests unitaires pour UpdateOrganizationConfig use case
library update_organization_config_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization_config.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'update_organization_config_test.mocks.dart';
void main() {
late UpdateOrganizationConfig useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = UpdateOrganizationConfig(mockRepository);
});
group('UpdateOrganizationConfig Use Case', () {
const tOrganizationId = 'org1';
final tConfig = {
'theme': 'dark',
'language': 'fr',
'notifications': true,
'cotisationMensuelle': 5000.0,
};
final tUpdatedOrganization = OrganizationModel(
id: tOrganizationId,
nom: 'Organisation Alpha',
nomCourt: 'OA',
email: 'contact@alpha.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should update organization configuration successfully', () async {
// Arrange
when(mockRepository.updateOrganizationConfig(tOrganizationId, tConfig))
.thenAnswer((_) async => tUpdatedOrganization);
// Act
final result = await useCase(tOrganizationId, tConfig);
// Assert
expect(result, equals(tUpdatedOrganization));
verify(mockRepository.updateOrganizationConfig(tOrganizationId, tConfig));
verifyNoMoreInteractions(mockRepository);
});
test('should update partial configuration', () async {
// Arrange
final partialConfig = {'theme': 'light'};
when(mockRepository.updateOrganizationConfig(tOrganizationId, partialConfig))
.thenAnswer((_) async => tUpdatedOrganization);
// Act
final result = await useCase(tOrganizationId, partialConfig);
// Assert
expect(result, isNotNull);
verify(mockRepository.updateOrganizationConfig(tOrganizationId, partialConfig));
});
test('should handle empty configuration map', () async {
// Arrange
final emptyConfig = <String, dynamic>{};
when(mockRepository.updateOrganizationConfig(tOrganizationId, emptyConfig))
.thenAnswer((_) async => tUpdatedOrganization);
// Act
final result = await useCase(tOrganizationId, emptyConfig);
// Assert
expect(result, isNotNull);
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateOrganizationConfig(any, any))
.thenThrow(Exception('Configuration update failed'));
// Act & Assert
expect(() => useCase(tOrganizationId, tConfig), throwsException);
});
});
}

View File

@@ -0,0 +1,91 @@
/// Tests unitaires pour UpdateOrganization use case
library update_organization_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart';
import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization.dart';
import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart';
@GenerateMocks([IOrganizationRepository])
import 'update_organization_test.mocks.dart';
void main() {
late UpdateOrganization useCase;
late MockIOrganizationRepository mockRepository;
setUp(() {
mockRepository = MockIOrganizationRepository();
useCase = UpdateOrganization(mockRepository);
});
group('UpdateOrganization Use Case', () {
const tOrganizationId = 'org1';
final tOrganization = OrganizationModel(
id: tOrganizationId,
nom: 'Organisation Mise à Jour',
nomCourt: 'OMA',
email: 'updated@org.com',
telephone: '+33987654321',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
test('should update organization successfully', () async {
// Arrange
when(mockRepository.updateOrganization(tOrganizationId, tOrganization))
.thenAnswer((_) async => tOrganization);
// Act
final result = await useCase(tOrganizationId, tOrganization);
// Assert
expect(result, equals(tOrganization));
expect(result.nom, equals('Organisation Mise à Jour'));
verify(mockRepository.updateOrganization(tOrganizationId, tOrganization));
verifyNoMoreInteractions(mockRepository);
});
test('should update partial organization fields', () async {
// Arrange
final partialUpdate = OrganizationModel(
id: tOrganizationId,
nom: 'Nom Modifié',
nomCourt: 'OA',
email: 'contact@alpha.org',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
);
when(mockRepository.updateOrganization(tOrganizationId, partialUpdate))
.thenAnswer((_) async => partialUpdate);
// Act
final result = await useCase(tOrganizationId, partialUpdate);
// Assert
expect(result.nom, equals('Nom Modifié'));
});
test('should throw exception when organization not found', () async {
// Arrange
when(mockRepository.updateOrganization(any, any))
.thenThrow(Exception('Organisation non trouvée'));
// Act & Assert
expect(
() => useCase(tOrganizationId, tOrganization),
throwsA(isA<Exception>()),
);
});
test('should throw exception when update fails', () async {
// Arrange
when(mockRepository.updateOrganization(any, any))
.thenThrow(Exception('Mise à jour échouée'));
// Act & Assert
expect(() => useCase(tOrganizationId, tOrganization), throwsException);
});
});
}