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,80 @@
/// Tests unitaires pour ExportReportExcel use case
library export_report_excel_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/export_report_excel.dart';
@GenerateMocks([IReportsRepository])
import 'export_report_excel_test.mocks.dart';
void main() {
late ExportReportExcel useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = ExportReportExcel(mockRepository);
});
group('ExportReportExcel Use Case', () {
const tReportType = 'membres';
const tExcelPath = '/storage/reports/membres_2024.xlsx';
const tCsvPath = '/storage/reports/membres_2024.csv';
test('should export report to Excel successfully', () async {
// Arrange
when(mockRepository.exportReportExcel(tReportType, format: 'excel'))
.thenAnswer((_) async => tExcelPath);
// Act
final result = await useCase(tReportType);
// Assert
expect(result, equals(tExcelPath));
expect(result, contains('.xlsx'));
verify(mockRepository.exportReportExcel(tReportType, format: 'excel'));
verifyNoMoreInteractions(mockRepository);
});
test('should export report to CSV when format specified', () async {
// Arrange
when(mockRepository.exportReportExcel(tReportType, format: 'csv'))
.thenAnswer((_) async => tCsvPath);
// Act
final result = await useCase(tReportType, format: 'csv');
// Assert
expect(result, equals(tCsvPath));
expect(result, contains('.csv'));
verify(mockRepository.exportReportExcel(tReportType, format: 'csv'));
});
test('should return valid Excel path with type name', () async {
// Arrange
const cotisationsType = 'cotisations';
const cotisationsExcel = '/storage/reports/cotisations_2024.xlsx';
when(mockRepository.exportReportExcel(cotisationsType, format: 'excel'))
.thenAnswer((_) async => cotisationsExcel);
// Act
final result = await useCase(cotisationsType);
// Assert
expect(result, contains('cotisations'));
expect(result, contains('.xlsx'));
});
test('should throw exception when Excel export fails', () async {
// Arrange
when(mockRepository.exportReportExcel(any, format: anyNamed('format')))
.thenThrow(Exception('Excel export failed'));
// Act & Assert
expect(() => useCase(tReportType), throwsException);
});
});
}

View File

@@ -0,0 +1,79 @@
/// Tests unitaires pour ExportReportPdf use case
library export_report_pdf_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/export_report_pdf.dart';
@GenerateMocks([IReportsRepository])
import 'export_report_pdf_test.mocks.dart';
void main() {
late ExportReportPdf useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = ExportReportPdf(mockRepository);
});
group('ExportReportPdf Use Case', () {
const tReportType = 'membres';
const tPdfPath = '/storage/reports/membres_2024.pdf';
test('should export report to PDF successfully', () async {
// Arrange
when(mockRepository.exportReportPdf(tReportType))
.thenAnswer((_) async => tPdfPath);
// Act
final result = await useCase(tReportType);
// Assert
expect(result, equals(tPdfPath));
expect(result, contains('.pdf'));
verify(mockRepository.exportReportPdf(tReportType));
verifyNoMoreInteractions(mockRepository);
});
test('should return valid PDF path with type name', () async {
// Arrange
const cotisationsType = 'cotisations';
const cotisationsPdf = '/storage/reports/cotisations_2024.pdf';
when(mockRepository.exportReportPdf(cotisationsType))
.thenAnswer((_) async => cotisationsPdf);
// Act
final result = await useCase(cotisationsType);
// Assert
expect(result, contains('cotisations'));
expect(result, endsWith('.pdf'));
});
test('should return URL for remote PDF file', () async {
// Arrange
const remotePdf = 'https://api.example.com/reports/membres.pdf';
when(mockRepository.exportReportPdf(tReportType))
.thenAnswer((_) async => remotePdf);
// Act
final result = await useCase(tReportType);
// Assert
expect(result, startsWith('https://'));
expect(result, endsWith('.pdf'));
});
test('should throw exception when PDF export fails', () async {
// Arrange
when(mockRepository.exportReportPdf(any))
.thenThrow(Exception('PDF generation failed'));
// Act & Assert
expect(() => useCase(tReportType), throwsException);
});
});
}

View File

@@ -0,0 +1,73 @@
/// Tests unitaires pour GenerateReport use case
library generate_report_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/generate_report.dart';
@GenerateMocks([IReportsRepository])
import 'generate_report_test.mocks.dart';
void main() {
late GenerateReport useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = GenerateReport(mockRepository);
});
group('GenerateReport Use Case', () {
const tReportType = 'membres';
const tFormat = 'pdf';
test('should generate report successfully', () async {
// Arrange
when(mockRepository.generateReport(tReportType, format: null))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tReportType);
// Assert
verify(mockRepository.generateReport(tReportType, format: null));
verifyNoMoreInteractions(mockRepository);
});
test('should generate report with specific format', () async {
// Arrange
when(mockRepository.generateReport(tReportType, format: tFormat))
.thenAnswer((_) async => Future.value());
// Act
await useCase(tReportType, format: tFormat);
// Assert
verify(mockRepository.generateReport(tReportType, format: tFormat));
});
test('should generate report for different types', () async {
// Arrange
const altType = 'cotisations';
when(mockRepository.generateReport(altType, format: null))
.thenAnswer((_) async => Future.value());
// Act
await useCase(altType);
// Assert
verify(mockRepository.generateReport(altType, format: null));
});
test('should throw exception when generation fails', () async {
// Arrange
when(mockRepository.generateReport(any, format: anyNamed('format')))
.thenThrow(Exception('Generation failed'));
// Act & Assert
expect(() => useCase(tReportType), throwsException);
});
});
}

View File

@@ -0,0 +1,102 @@
/// Tests unitaires pour GetReports use case
library get_reports_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/get_reports.dart';
@GenerateMocks([IReportsRepository])
import 'get_reports_test.mocks.dart';
void main() {
late GetReports useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = GetReports(mockRepository);
});
group('GetReports Use Case', () {
final tAvailableReports = [
{
'type': 'membres',
'nom': 'Rapport Membres',
'description': 'Statistiques complètes des membres',
'categorie': 'administratif',
},
{
'type': 'cotisations',
'nom': 'Rapport Cotisations',
'description': 'Analyse des cotisations',
'categorie': 'financier',
},
{
'type': 'evenements',
'nom': 'Rapport Événements',
'description': 'Bilan des événements',
'categorie': 'activites',
},
];
test('should return list of available reports', () async {
// Arrange
when(mockRepository.getAvailableReports())
.thenAnswer((_) async => tAvailableReports);
// Act
final result = await useCase();
// Assert
expect(result, equals(tAvailableReports));
expect(result.length, equals(3));
expect(result[0]['type'], equals('membres'));
verify(mockRepository.getAvailableReports());
verifyNoMoreInteractions(mockRepository);
});
test('should return reports with specific categories', () async {
// Arrange
final financialReports = [
{
'type': 'cotisations',
'nom': 'Rapport Cotisations',
'categorie': 'financier',
},
];
when(mockRepository.getAvailableReports())
.thenAnswer((_) async => financialReports);
// Act
final result = await useCase();
// Assert
expect(result.length, equals(1));
expect(result.first['categorie'], equals('financier'));
verify(mockRepository.getAvailableReports());
});
test('should return empty list when no reports available', () async {
// Arrange
when(mockRepository.getAvailableReports()).thenAnswer((_) async => []);
// Act
final result = await useCase();
// Assert
expect(result, isEmpty);
verify(mockRepository.getAvailableReports());
});
test('should throw exception when repository fails', () async {
// Arrange
when(mockRepository.getAvailableReports())
.thenThrow(Exception('Network error'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,102 @@
/// Tests unitaires pour GetScheduledReports use case
library get_scheduled_reports_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/get_scheduled_reports.dart';
@GenerateMocks([IReportsRepository])
import 'get_scheduled_reports_test.mocks.dart';
void main() {
late GetScheduledReports useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = GetScheduledReports(mockRepository);
});
group('GetScheduledReports Use Case', () {
final tScheduledReports = [
{
'id': '1',
'type': 'membres',
'nom': 'Rapport Membres Mensuel',
'cronExpression': '0 0 1 * *',
'active': true,
'derniereLancement': '2024-01-01T00:00:00Z',
},
{
'id': '2',
'type': 'cotisations',
'nom': 'Rapport Cotisations Hebdomadaire',
'cronExpression': '0 9 * * 1',
'active': true,
'derniereLancement': '2024-01-08T09:00:00Z',
},
];
test('should return list of scheduled reports', () async {
// Arrange
when(mockRepository.getScheduledReports())
.thenAnswer((_) async => tScheduledReports);
// Act
final result = await useCase();
// Assert
expect(result, equals(tScheduledReports));
expect(result.length, equals(2));
expect(result[0]['type'], equals('membres'));
expect(result[0]['active'], isTrue);
verify(mockRepository.getScheduledReports());
verifyNoMoreInteractions(mockRepository);
});
test('should return only active scheduled reports', () async {
// Arrange
final activeReports = [
{
'id': '1',
'type': 'membres',
'cronExpression': '0 0 1 * *',
'active': true,
},
];
when(mockRepository.getScheduledReports())
.thenAnswer((_) async => activeReports);
// Act
final result = await useCase();
// Assert
expect(result.length, equals(1));
expect(result.first['active'], isTrue);
verify(mockRepository.getScheduledReports());
});
test('should return empty list when no scheduled reports', () async {
// Arrange
when(mockRepository.getScheduledReports()).thenAnswer((_) async => []);
// Act
final result = await useCase();
// Assert
expect(result, isEmpty);
verify(mockRepository.getScheduledReports());
});
test('should throw exception when retrieval fails', () async {
// Arrange
when(mockRepository.getScheduledReports())
.thenThrow(Exception('Database error'));
// Act & Assert
expect(() => useCase(), throwsException);
});
});
}

View File

@@ -0,0 +1,72 @@
/// Tests unitaires pour ScheduleReport use case
library schedule_report_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/repositories/reports_repository.dart';
import 'package:unionflow_mobile_apps/features/reports/domain/usecases/schedule_report.dart';
@GenerateMocks([IReportsRepository])
import 'schedule_report_test.mocks.dart';
void main() {
late ScheduleReport useCase;
late MockIReportsRepository mockRepository;
setUp(() {
mockRepository = MockIReportsRepository();
useCase = ScheduleReport(mockRepository);
});
group('ScheduleReport Use Case', () {
const tCronExpression = '0 0 1 * *'; // 1er de chaque mois à minuit
test('should schedule report successfully', () async {
// Arrange
when(mockRepository.scheduleReport(cronExpression: null))
.thenAnswer((_) async => Future.value());
// Act
await useCase();
// Assert
verify(mockRepository.scheduleReport(cronExpression: null));
verifyNoMoreInteractions(mockRepository);
});
test('should schedule report with cron expression', () async {
// Arrange
when(mockRepository.scheduleReport(cronExpression: tCronExpression))
.thenAnswer((_) async => Future.value());
// Act
await useCase(cronExpression: tCronExpression);
// Assert
verify(mockRepository.scheduleReport(cronExpression: tCronExpression));
});
test('should schedule report with different cron expressions', () async {
// Arrange
const weeklyCron = '0 9 * * 1'; // Tous les lundis à 9h
when(mockRepository.scheduleReport(cronExpression: weeklyCron))
.thenAnswer((_) async => Future.value());
// Act
await useCase(cronExpression: weeklyCron);
// Assert
verify(mockRepository.scheduleReport(cronExpression: weeklyCron));
});
test('should throw exception when scheduling fails', () async {
// Arrange
when(mockRepository.scheduleReport(cronExpression: anyNamed('cronExpression')))
.thenThrow(Exception('Invalid cron expression'));
// Act & Assert
expect(() => useCase(cronExpression: tCronExpression), throwsException);
});
});
}