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,119 @@
/// Tests unitaires pour ApproveTransaction use case
library approve_transaction_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/approve_transaction.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'approve_transaction_test.mocks.dart';
void main() {
late ApproveTransaction useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = ApproveTransaction(mockRepository);
});
group('ApproveTransaction Use Case', () {
const tApprovalId = 'approval-123';
const tComment = 'Approuvé - Montant conforme au budget';
final tApprovedTransaction = TransactionApproval(
id: tApprovalId,
transactionId: 'tx-123',
transactionType: TransactionType.withdrawal,
amount: 500000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
requiredLevel: ApprovalLevel.level1,
status: ApprovalStatus.approved,
approvers: [],
createdAt: DateTime(2024, 12, 15),
);
test('should approve transaction successfully', () async {
// Arrange
when(mockRepository.approveTransaction(
approvalId: tApprovalId,
comment: tComment,
)).thenAnswer((_) async => Right(tApprovedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId, comment: tComment);
// Assert
expect(result, Right(tApprovedTransaction));
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.id, equals(tApprovalId));
expect(approval.status, equals(ApprovalStatus.approved));
},
);
verify(mockRepository.approveTransaction(
approvalId: tApprovalId,
comment: tComment,
));
verifyNoMoreInteractions(mockRepository);
});
test('should approve transaction without comment', () async {
// Arrange
when(mockRepository.approveTransaction(
approvalId: tApprovalId,
comment: null,
)).thenAnswer((_) async => Right(tApprovedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approval) => expect(approval.status, equals(ApprovalStatus.approved)),
);
});
test('should return ValidationFailure when approvalId is empty', () async {
// Act
final result = await useCase(approvalId: '');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('ID approbation requis'));
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Transaction déjà approuvée');
when(mockRepository.approveTransaction(
approvalId: anyNamed('approvalId'),
comment: anyNamed('comment'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(approvalId: tApprovalId);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(approval) => fail('Should not return approval'),
);
});
});
}

View File

@@ -0,0 +1,221 @@
/// Tests unitaires pour CreateBudget use case
library create_budget_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/create_budget.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'create_budget_test.mocks.dart';
void main() {
late CreateBudget useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = CreateBudget(mockRepository);
});
group('CreateBudget Use Case', () {
const tName = 'Budget 2025';
const tOrgId = 'org-123';
final tBudgetLines = [
BudgetLine(
id: 'line-1',
category: BudgetCategory.contributions,
name: 'Cotisations mensuelles',
description: 'Revenus des cotisations',
amountPlanned: 3000000.0,
),
BudgetLine(
id: 'line-2',
category: BudgetCategory.savings,
name: 'Dépôts épargne',
description: 'Collecte épargne',
amountPlanned: 2000000.0,
),
BudgetLine(
id: 'line-3',
category: BudgetCategory.solidarity,
name: 'Aide mutuelle',
description: 'Soutien membres',
amountPlanned: 1000000.0,
),
];
final tCreatedBudget = Budget(
id: 'budget-new',
name: tName,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
status: BudgetStatus.draft,
lines: tBudgetLines,
totalPlanned: 6000000.0,
totalRealized: 0.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime.now(),
startDate: DateTime(2025, 1, 1),
endDate: DateTime(2025, 12, 31),
);
test('should create budget successfully', () async {
// Arrange
when(mockRepository.createBudget(
name: tName,
description: anyNamed('description'),
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
month: anyNamed('month'),
lines: tBudgetLines,
)).thenAnswer((_) async => Right(tCreatedBudget));
// Act
final result = await useCase(
name: tName,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
lines: tBudgetLines,
);
// Assert
expect(result, Right(tCreatedBudget));
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.id, equals('budget-new'));
expect(budget.name, equals(tName));
expect(budget.status, equals(BudgetStatus.draft));
},
);
verify(mockRepository.createBudget(
name: tName,
description: null,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
month: null,
lines: tBudgetLines,
));
verifyNoMoreInteractions(mockRepository);
});
test('should create monthly budget with description', () async {
// Arrange
const description = 'Budget opérationnel janvier 2025';
final monthlyLines = [
BudgetLine(
id: 'line-monthly',
category: BudgetCategory.contributions,
name: 'Cotisations janvier',
amountPlanned: 500000.0,
),
];
final monthlyBudget = Budget(
id: 'budget-monthly',
name: 'Budget Janvier 2025',
description: description,
organizationId: tOrgId,
period: BudgetPeriod.monthly,
year: 2025,
month: 1,
status: BudgetStatus.draft,
lines: monthlyLines,
totalPlanned: 500000.0,
totalRealized: 0.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime.now(),
startDate: DateTime(2025, 1, 1),
endDate: DateTime(2025, 1, 31),
);
when(mockRepository.createBudget(
name: 'Budget Janvier 2025',
description: description,
organizationId: tOrgId,
period: BudgetPeriod.monthly,
year: 2025,
month: 1,
lines: monthlyLines,
)).thenAnswer((_) async => Right(monthlyBudget));
// Act
final result = await useCase(
name: 'Budget Janvier 2025',
description: description,
organizationId: tOrgId,
period: BudgetPeriod.monthly,
year: 2025,
month: 1,
lines: monthlyLines,
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.period, equals(BudgetPeriod.monthly));
expect(budget.month, equals(1));
},
);
});
test('should return ValidationFailure when name is empty', () async {
// Act
final result = await useCase(
name: '',
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
lines: [BudgetLine(id: 'test-1', category: BudgetCategory.operational, name: 'Test line', amountPlanned: 100.0)],
);
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(budget) => fail('Should not return budget'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur création budget');
when(mockRepository.createBudget(
name: anyNamed('name'),
description: anyNamed('description'),
organizationId: anyNamed('organizationId'),
period: anyNamed('period'),
year: anyNamed('year'),
month: anyNamed('month'),
lines: anyNamed('lines'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(
name: tName,
organizationId: tOrgId,
period: BudgetPeriod.annual,
year: 2025,
lines: [BudgetLine(id: 'test-1', category: BudgetCategory.operational, name: 'Test line', amountPlanned: 100.0)],
);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(budget) => fail('Should not return budget'),
);
});
});
}

View File

@@ -0,0 +1,112 @@
/// Tests unitaires pour GetApprovalById use case
library get_approval_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_approval_by_id.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_approval_by_id_test.mocks.dart';
void main() {
late GetApprovalById useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetApprovalById(mockRepository);
});
group('GetApprovalById Use Case', () {
const tApprovalId = 'approval-123';
final tApproval = TransactionApproval(
id: tApprovalId,
transactionId: 'tx-456',
transactionType: TransactionType.solidarity,
amount: 350000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
organizationId: 'org-123',
requiredLevel: ApprovalLevel.level2,
status: ApprovalStatus.pending,
approvers: [],
createdAt: DateTime(2024, 12, 15),
);
test('should return approval details by ID', () async {
// Arrange
when(mockRepository.getApprovalById(tApprovalId))
.thenAnswer((_) async => Right(tApproval));
// Act
final result = await useCase(tApprovalId);
// Assert
expect(result, Right(tApproval));
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.id, equals(tApprovalId));
expect(approval.amount, equals(350000.0));
expect(approval.transactionType, equals(TransactionType.solidarity));
},
);
verify(mockRepository.getApprovalById(tApprovalId));
verifyNoMoreInteractions(mockRepository);
});
test('should return approval with level 2 requirement', () async {
// Arrange
when(mockRepository.getApprovalById(tApprovalId))
.thenAnswer((_) async => Right(tApproval));
// Act
final result = await useCase(tApprovalId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.requiredLevel, equals(ApprovalLevel.level2));
},
);
});
test('should return ValidationFailure when approvalId is empty', () async {
// Act
final result = await useCase('');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when approval not found', () async {
// Arrange
final tFailure = ServerFailure('Approbation non trouvée');
when(mockRepository.getApprovalById(any))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(approval) => fail('Should not return approval'),
);
});
});
}

View File

@@ -0,0 +1,116 @@
/// Tests unitaires pour GetBudgetById use case
library get_budget_by_id_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_by_id.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_budget_by_id_test.mocks.dart';
void main() {
late GetBudgetById useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetBudgetById(mockRepository);
});
group('GetBudgetById Use Case', () {
const tBudgetId = 'budget-123';
final tBudget = Budget(
id: tBudgetId,
name: 'Budget Annuel 2024',
description: 'Budget prévisionnel pour l\'année 2024',
organizationId: 'org-123',
period: BudgetPeriod.annual,
year: 2024,
status: BudgetStatus.active,
lines: [],
totalPlanned: 5000000.0,
totalRealized: 3250000.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime(2024, 1, 1),
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 12, 31),
);
test('should return budget details by ID', () async {
// Arrange
when(mockRepository.getBudgetById(tBudgetId))
.thenAnswer((_) async => Right(tBudget));
// Act
final result = await useCase(tBudgetId);
// Assert
expect(result, Right(tBudget));
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.id, equals(tBudgetId));
expect(budget.name, equals('Budget Annuel 2024'));
expect(budget.totalPlanned, equals(5000000.0));
},
);
verify(mockRepository.getBudgetById(tBudgetId));
verifyNoMoreInteractions(mockRepository);
});
test('should return budget with realized amount', () async {
// Arrange
when(mockRepository.getBudgetById(tBudgetId))
.thenAnswer((_) async => Right(tBudget));
// Act
final result = await useCase(tBudgetId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budget) {
expect(budget.totalRealized, equals(3250000.0));
expect(budget.totalRealized, lessThan(budget.totalPlanned));
},
);
});
test('should return ValidationFailure when budgetId is empty', () async {
// Act
final result = await useCase('');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(budget) => fail('Should not return budget'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when budget not found', () async {
// Arrange
final tFailure = ServerFailure('Budget non trouvé');
when(mockRepository.getBudgetById(any))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase('nonexistent');
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(budget) => fail('Should not return budget'),
);
});
});
}

View File

@@ -0,0 +1,114 @@
/// Tests unitaires pour GetBudgetTracking use case
library get_budget_tracking_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_tracking.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_budget_tracking_test.mocks.dart';
void main() {
late GetBudgetTracking useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetBudgetTracking(mockRepository);
});
group('GetBudgetTracking Use Case', () {
const tBudgetId = 'budget-123';
final tTrackingData = {
'budgetId': tBudgetId,
'totalPlanned': 5000000.0,
'totalRealized': 3250000.0,
'remainingAmount': 1750000.0,
'realizationRate': 0.65,
'categories': {
'contributions': {'planned': 2000000.0, 'realized': 1800000.0, 'rate': 0.9},
'savings': {'planned': 1500000.0, 'realized': 950000.0, 'rate': 0.63},
'solidarity': {'planned': 1000000.0, 'realized': 350000.0, 'rate': 0.35},
'events': {'planned': 500000.0, 'realized': 150000.0, 'rate': 0.3},
},
};
test('should return budget tracking data successfully', () async {
// Arrange
when(mockRepository.getBudgetTracking(budgetId: tBudgetId))
.thenAnswer((_) async => Right(tTrackingData));
// Act
final result = await useCase(budgetId: tBudgetId);
// Assert
expect(result, Right(tTrackingData));
result.fold(
(failure) => fail('Should not return failure'),
(tracking) {
expect(tracking['budgetId'], equals(tBudgetId));
expect(tracking['totalPlanned'], equals(5000000.0));
expect(tracking['realizationRate'], equals(0.65));
},
);
verify(mockRepository.getBudgetTracking(budgetId: tBudgetId));
verifyNoMoreInteractions(mockRepository);
});
test('should return tracking with category breakdown', () async {
// Arrange
when(mockRepository.getBudgetTracking(budgetId: tBudgetId))
.thenAnswer((_) async => Right(tTrackingData));
// Act
final result = await useCase(budgetId: tBudgetId);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(tracking) {
final categories = tracking['categories'] as Map<String, dynamic>;
expect(categories.keys, contains('contributions'));
expect(categories.keys, contains('solidarity'));
final contribs = categories['contributions'] as Map<String, dynamic>;
expect(contribs['rate'], equals(0.9));
},
);
});
test('should return ValidationFailure when budgetId is empty', () async {
// Act
final result = await useCase(budgetId: '');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
},
(tracking) => fail('Should not return tracking'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur suivi budget');
when(mockRepository.getBudgetTracking(budgetId: anyNamed('budgetId')))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(budgetId: tBudgetId);
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(tracking) => fail('Should not return tracking'),
);
});
});
}

View File

@@ -0,0 +1,155 @@
/// Tests unitaires pour GetBudgets use case
library get_budgets_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budgets.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_budgets_test.mocks.dart';
void main() {
late GetBudgets useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetBudgets(mockRepository);
});
group('GetBudgets Use Case', () {
final tBudgets = [
Budget(
id: 'budget-1',
name: 'Budget Annuel 2024',
organizationId: 'org-123',
period: BudgetPeriod.annual,
year: 2024,
status: BudgetStatus.active,
lines: [],
totalPlanned: 5000000.0,
totalRealized: 3250000.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime(2024, 1, 1),
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 12, 31),
),
Budget(
id: 'budget-2',
name: 'Budget Q4 2024',
organizationId: 'org-123',
period: BudgetPeriod.quarterly,
year: 2024,
month: 10,
status: BudgetStatus.active,
lines: [],
totalPlanned: 1250000.0,
totalRealized: 850000.0,
currency: 'XOF',
createdBy: 'user-1',
createdAt: DateTime(2024, 10, 1),
startDate: DateTime(2024, 10, 1),
endDate: DateTime(2024, 12, 31),
),
];
test('should return list of budgets successfully', () async {
// Arrange
when(mockRepository.getBudgets(
organizationId: anyNamed('organizationId'),
status: anyNamed('status'),
year: anyNamed('year'),
)).thenAnswer((_) async => Right(tBudgets));
// Act
final result = await useCase(organizationId: 'org-123');
// Assert
expect(result, Right(tBudgets));
result.fold(
(failure) => fail('Should not return failure'),
(budgets) {
expect(budgets.length, equals(2));
expect(budgets[0].name, equals('Budget Annuel 2024'));
expect(budgets[0].totalPlanned, equals(5000000.0));
},
);
verify(mockRepository.getBudgets(
organizationId: 'org-123',
status: null,
year: null,
));
verifyNoMoreInteractions(mockRepository);
});
test('should filter budgets by status and year', () async {
// Arrange
final activeBudgets = [tBudgets[0], tBudgets[1]];
when(mockRepository.getBudgets(
organizationId: 'org-123',
status: BudgetStatus.active,
year: 2024,
)).thenAnswer((_) async => Right(activeBudgets));
// Act
final result = await useCase(
organizationId: 'org-123',
status: BudgetStatus.active,
year: 2024,
);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budgets) {
expect(budgets.every((b) => b.status == BudgetStatus.active), isTrue);
expect(budgets.every((b) => b.year == 2024), isTrue);
},
);
});
test('should return empty list when no budgets exist', () async {
// Arrange
when(mockRepository.getBudgets(
organizationId: anyNamed('organizationId'),
status: anyNamed('status'),
year: anyNamed('year'),
)).thenAnswer((_) async => Right([]));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(budgets) => expect(budgets, isEmpty),
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getBudgets(
organizationId: anyNamed('organizationId'),
status: anyNamed('status'),
year: anyNamed('year'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase();
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(budgets) => fail('Should not return budgets'),
);
});
});
}

View File

@@ -0,0 +1,132 @@
/// Tests unitaires pour GetPendingApprovals use case
library get_pending_approvals_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_pending_approvals.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'get_pending_approvals_test.mocks.dart';
void main() {
late GetPendingApprovals useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = GetPendingApprovals(mockRepository);
});
group('GetPendingApprovals Use Case', () {
final tApprovals = [
TransactionApproval(
id: 'approval-1',
transactionId: 'tx-123',
transactionType: TransactionType.withdrawal,
amount: 500000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
organizationId: 'org-123',
requiredLevel: ApprovalLevel.level2,
status: ApprovalStatus.pending,
approvers: [],
createdAt: DateTime(2024, 12, 15),
),
TransactionApproval(
id: 'approval-2',
transactionId: 'tx-456',
transactionType: TransactionType.solidarity,
amount: 200000.0,
currency: 'XOF',
requesterId: 'user-2',
requesterName: 'Fatou Ndiaye',
requiredLevel: ApprovalLevel.level1,
status: ApprovalStatus.pending,
approvers: [],
createdAt: DateTime(2024, 12, 14),
),
];
test('should return list of pending approvals successfully', () async {
// Arrange
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Right(tApprovals));
// Act
final result = await useCase(organizationId: 'org-123');
// Assert
expect(result, Right(tApprovals));
result.fold(
(failure) => fail('Should not return failure'),
(approvals) {
expect(approvals.length, equals(2));
expect(approvals[0].status, equals(ApprovalStatus.pending));
expect(approvals[0].amount, equals(500000.0));
},
);
verify(mockRepository.getPendingApprovals(organizationId: 'org-123'));
verifyNoMoreInteractions(mockRepository);
});
test('should return approvals with different levels', () async {
// Arrange
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Right(tApprovals));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approvals) {
expect(approvals.any((a) => a.requiredLevel == ApprovalLevel.level2), isTrue);
expect(approvals.any((a) => a.requiredLevel == ApprovalLevel.level1), isTrue);
},
);
});
test('should return empty list when no pending approvals', () async {
// Arrange
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Right([]));
// Act
final result = await useCase();
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approvals) => expect(approvals, isEmpty),
);
});
test('should return ServerFailure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Erreur serveur');
when(mockRepository.getPendingApprovals(
organizationId: anyNamed('organizationId'),
)).thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase();
// Assert
expect(result, Left(tFailure));
result.fold(
(failure) => expect(failure, isA<ServerFailure>()),
(approvals) => fail('Should not return approvals'),
);
});
});
}

View File

@@ -0,0 +1,116 @@
/// Tests unitaires pour RejectTransaction use case
library reject_transaction_test;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:dartz/dartz.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/reject_transaction.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart';
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart';
import 'package:unionflow_mobile_apps/core/error/failures.dart';
@GenerateMocks([FinanceWorkflowRepository])
import 'reject_transaction_test.mocks.dart';
void main() {
late RejectTransaction useCase;
late MockFinanceWorkflowRepository mockRepository;
setUp(() {
mockRepository = MockFinanceWorkflowRepository();
useCase = RejectTransaction(mockRepository);
});
group('RejectTransaction Use Case', () {
const tApprovalId = 'approval-123';
const tReason = 'Montant trop élevé - Budget insuffisant';
final tRejectedTransaction = TransactionApproval(
id: tApprovalId,
transactionId: 'tx-123',
transactionType: TransactionType.withdrawal,
amount: 500000.0,
currency: 'XOF',
requesterId: 'user-1',
requesterName: 'Amadou Diallo',
requiredLevel: ApprovalLevel.level2,
status: ApprovalStatus.rejected,
approvers: [],
createdAt: DateTime(2024, 12, 15),
);
test('should reject transaction successfully', () async {
// Arrange
when(mockRepository.rejectTransaction(
approvalId: tApprovalId,
reason: tReason,
)).thenAnswer((_) async => Right(tRejectedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId, reason: tReason);
// Assert
expect(result, Right(tRejectedTransaction));
result.fold(
(failure) => fail('Should not return failure'),
(approval) {
expect(approval.id, equals(tApprovalId));
expect(approval.status, equals(ApprovalStatus.rejected));
},
);
verify(mockRepository.rejectTransaction(
approvalId: tApprovalId,
reason: tReason,
));
verifyNoMoreInteractions(mockRepository);
});
test('should reject transaction with detailed reason', () async {
// Arrange
const detailedReason = 'Refus: Documentation incomplète + montant non justifié';
when(mockRepository.rejectTransaction(
approvalId: tApprovalId,
reason: detailedReason,
)).thenAnswer((_) async => Right(tRejectedTransaction));
// Act
final result = await useCase(approvalId: tApprovalId, reason: detailedReason);
// Assert
result.fold(
(failure) => fail('Should not return failure'),
(approval) => expect(approval.status, equals(ApprovalStatus.rejected)),
);
});
test('should return ValidationFailure when approvalId is empty', () async {
// Act
final result = await useCase(approvalId: '', reason: tReason);
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('ID approbation requis'));
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
test('should return ValidationFailure when reason is empty', () async {
// Act
final result = await useCase(approvalId: tApprovalId, reason: ' ');
// Assert
result.fold(
(failure) {
expect(failure, isA<ValidationFailure>());
expect((failure as ValidationFailure).message, contains('Raison du rejet requise'));
},
(approval) => fail('Should not return approval'),
);
verifyZeroInteractions(mockRepository);
});
});
}