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:
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user