import 'package:bloc_test/bloc_test.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:unionflow_mobile_apps/core/error/failures.dart'; import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart'; import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/approve_transaction.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/usecases/get_pending_approvals.dart'; import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/reject_transaction.dart'; import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/approval_bloc.dart'; import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/approval_event.dart'; import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/approval_state.dart'; @GenerateMocks([ GetPendingApprovals, GetApprovalById, ApproveTransaction, RejectTransaction, ]) import 'approval_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- TransactionApproval _buildApproval({ String id = 'appr-1', ApprovalStatus status = ApprovalStatus.pending, }) => TransactionApproval( id: id, transactionId: 'tx-1', transactionType: TransactionType.contribution, amount: 50000, requesterId: 'user-1', requesterName: 'Alice Diallo', requiredLevel: ApprovalLevel.level1, status: status, createdAt: DateTime(2026, 4, 1), ); final _pendingApproval = _buildApproval(); final _approvedApproval = _buildApproval(status: ApprovalStatus.approved); final _rejectedApproval = _buildApproval(status: ApprovalStatus.rejected); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { late MockGetPendingApprovals mockGetPendingApprovals; late MockGetApprovalById mockGetApprovalById; late MockApproveTransaction mockApproveTransaction; late MockRejectTransaction mockRejectTransaction; setUp(() { mockGetPendingApprovals = MockGetPendingApprovals(); mockGetApprovalById = MockGetApprovalById(); mockApproveTransaction = MockApproveTransaction(); mockRejectTransaction = MockRejectTransaction(); }); ApprovalBloc buildBloc() => ApprovalBloc( getPendingApprovals: mockGetPendingApprovals, getApprovalById: mockGetApprovalById, approveTransaction: mockApproveTransaction, rejectTransaction: mockRejectTransaction, ); // ─── initial state ─────────────────────────────────────────────────────── test('initial state is ApprovalInitial', () { final bloc = buildBloc(); expect(bloc.state, isA()); bloc.close(); }); // ─── LoadPendingApprovals ──────────────────────────────────────────────── group('LoadPendingApprovals', () { blocTest( 'emits [ApprovalsLoading, ApprovalsLoaded] when approvals exist', build: () { when(mockGetPendingApprovals(organizationId: anyNamed('organizationId'))) .thenAnswer((_) async => Right([_pendingApproval])); return buildBloc(); }, act: (b) => b.add(const LoadPendingApprovals(organizationId: 'org-1')), expect: () => [ const ApprovalsLoading(), ApprovalsLoaded( approvals: [_pendingApproval], pendingCount: 1, ), ], verify: (_) { verify(mockGetPendingApprovals(organizationId: 'org-1')).called(1); }, ); blocTest( 'emits [ApprovalsLoading, ApprovalsLoaded] with correct pendingCount', build: () { when(mockGetPendingApprovals(organizationId: anyNamed('organizationId'))) .thenAnswer((_) async => Right([ _pendingApproval, _buildApproval(id: 'appr-2'), _buildApproval(id: 'appr-3'), ])); return buildBloc(); }, act: (b) => b.add(const LoadPendingApprovals()), expect: () => [ const ApprovalsLoading(), predicate( (s) => s is ApprovalsLoaded && s.pendingCount == 3, 'ApprovalsLoaded with pendingCount == 3', ), ], ); blocTest( 'emits [ApprovalsLoading, ApprovalsEmpty] when list is empty', build: () { when(mockGetPendingApprovals(organizationId: anyNamed('organizationId'))) .thenAnswer((_) async => const Right([])); return buildBloc(); }, act: (b) => b.add(const LoadPendingApprovals(organizationId: 'org-1')), expect: () => [ const ApprovalsLoading(), const ApprovalsEmpty(), ], ); blocTest( 'emits [ApprovalsLoading, ApprovalError] on failure', build: () { when(mockGetPendingApprovals(organizationId: anyNamed('organizationId'))) .thenAnswer((_) async => const Left(ServerFailure('server error'))); return buildBloc(); }, act: (b) => b.add(const LoadPendingApprovals(organizationId: 'org-1')), expect: () => [ const ApprovalsLoading(), const ApprovalError('server error'), ], ); blocTest( 'works without organizationId (null)', build: () { when(mockGetPendingApprovals(organizationId: null)) .thenAnswer((_) async => Right([_pendingApproval])); return buildBloc(); }, act: (b) => b.add(const LoadPendingApprovals()), expect: () => [ const ApprovalsLoading(), isA(), ], ); }); // ─── LoadApprovalById ──────────────────────────────────────────────────── group('LoadApprovalById', () { blocTest( 'emits [ApprovalsLoading, ApprovalDetailLoaded] on success', build: () { when(mockGetApprovalById('appr-1')) .thenAnswer((_) async => Right(_pendingApproval)); return buildBloc(); }, act: (b) => b.add(const LoadApprovalById('appr-1')), expect: () => [ const ApprovalsLoading(), ApprovalDetailLoaded(_pendingApproval), ], verify: (_) { verify(mockGetApprovalById('appr-1')).called(1); }, ); blocTest( 'emits [ApprovalsLoading, ApprovalError] when not found', build: () { when(mockGetApprovalById(any)) .thenAnswer((_) async => const Left(NotFoundFailure('Approbation introuvable'))); return buildBloc(); }, act: (b) => b.add(const LoadApprovalById('unknown-id')), expect: () => [ const ApprovalsLoading(), const ApprovalError('Approbation introuvable'), ], ); blocTest( 'emits [ApprovalsLoading, ApprovalError] on network failure', build: () { when(mockGetApprovalById(any)) .thenAnswer((_) async => const Left(NetworkFailure('net'))); return buildBloc(); }, act: (b) => b.add(const LoadApprovalById('appr-1')), expect: () => [ const ApprovalsLoading(), const ApprovalError('net'), ], ); }); // ─── ApproveTransactionEvent ───────────────────────────────────────────── group('ApproveTransactionEvent', () { blocTest( 'emits [ApprovalActionInProgress(approve), TransactionApproved] on success', build: () { when(mockApproveTransaction( approvalId: anyNamed('approvalId'), comment: anyNamed('comment'), )).thenAnswer((_) async => Right(_approvedApproval)); return buildBloc(); }, act: (b) => b.add( const ApproveTransactionEvent( approvalId: 'appr-1', comment: 'OK pour moi', ), ), expect: () => [ const ApprovalActionInProgress('approve'), TransactionApproved(approval: _approvedApproval), ], verify: (_) { verify(mockApproveTransaction( approvalId: 'appr-1', comment: 'OK pour moi', )).called(1); }, ); blocTest( 'emits [ApprovalActionInProgress(approve), TransactionApproved] without comment', build: () { when(mockApproveTransaction( approvalId: anyNamed('approvalId'), comment: anyNamed('comment'), )).thenAnswer((_) async => Right(_approvedApproval)); return buildBloc(); }, act: (b) => b.add( const ApproveTransactionEvent(approvalId: 'appr-1'), ), expect: () => [ const ApprovalActionInProgress('approve'), isA(), ], ); blocTest( 'emits [ApprovalActionInProgress(approve), ApprovalError] on failure', build: () { when(mockApproveTransaction( approvalId: anyNamed('approvalId'), comment: anyNamed('comment'), )).thenAnswer( (_) async => const Left(ForbiddenFailure('Droits insuffisants'))); return buildBloc(); }, act: (b) => b.add( const ApproveTransactionEvent(approvalId: 'appr-1'), ), expect: () => [ const ApprovalActionInProgress('approve'), const ApprovalError('Droits insuffisants'), ], ); blocTest( 'TransactionApproved has correct default message', build: () { when(mockApproveTransaction( approvalId: anyNamed('approvalId'), comment: anyNamed('comment'), )).thenAnswer((_) async => Right(_approvedApproval)); return buildBloc(); }, act: (b) => b.add(const ApproveTransactionEvent(approvalId: 'appr-1')), expect: () => [ const ApprovalActionInProgress('approve'), predicate( (s) => s is TransactionApproved && s.message == 'Transaction approuvée avec succès', 'TransactionApproved with correct message', ), ], ); }); // ─── RejectTransactionEvent ────────────────────────────────────────────── group('RejectTransactionEvent', () { blocTest( 'emits [ApprovalActionInProgress(reject), TransactionRejected] on success', build: () { when(mockRejectTransaction( approvalId: anyNamed('approvalId'), reason: anyNamed('reason'), )).thenAnswer((_) async => Right(_rejectedApproval)); return buildBloc(); }, act: (b) => b.add( const RejectTransactionEvent( approvalId: 'appr-1', reason: 'Montant incorrect', ), ), expect: () => [ const ApprovalActionInProgress('reject'), TransactionRejected(approval: _rejectedApproval), ], verify: (_) { verify(mockRejectTransaction( approvalId: 'appr-1', reason: 'Montant incorrect', )).called(1); }, ); blocTest( 'emits [ApprovalActionInProgress(reject), ApprovalError] on failure', build: () { when(mockRejectTransaction( approvalId: anyNamed('approvalId'), reason: anyNamed('reason'), )).thenAnswer((_) async => const Left(ValidationFailure('ID approbation requis'))); return buildBloc(); }, act: (b) => b.add( const RejectTransactionEvent( approvalId: 'appr-1', reason: 'bad data', ), ), expect: () => [ const ApprovalActionInProgress('reject'), const ApprovalError('ID approbation requis'), ], ); blocTest( 'TransactionRejected has correct default message', build: () { when(mockRejectTransaction( approvalId: anyNamed('approvalId'), reason: anyNamed('reason'), )).thenAnswer((_) async => Right(_rejectedApproval)); return buildBloc(); }, act: (b) => b.add( const RejectTransactionEvent( approvalId: 'appr-1', reason: 'Raison valide', ), ), expect: () => [ const ApprovalActionInProgress('reject'), predicate( (s) => s is TransactionRejected && s.message == 'Transaction rejetée avec succès', 'TransactionRejected with correct message', ), ], ); }); // ─── RefreshApprovals ──────────────────────────────────────────────────── group('RefreshApprovals', () { blocTest( 'emits ApprovalsLoaded without ApprovalsLoading during refresh', build: () { when(mockGetPendingApprovals( organizationId: anyNamed('organizationId'), )).thenAnswer((_) async => Right([_pendingApproval])); return buildBloc(); }, // No seed — run from ApprovalInitial. RefreshApprovals skips ApprovalsLoading. act: (b) => b.add(const RefreshApprovals(organizationId: 'org-1')), // No ApprovalsLoading emitted — goes straight to result expect: () => [ ApprovalsLoaded( approvals: [_pendingApproval], pendingCount: 1, ), ], ); blocTest( 'emits ApprovalsEmpty when refresh returns empty list', build: () { when(mockGetPendingApprovals( organizationId: anyNamed('organizationId'), )).thenAnswer((_) async => const Right([])); return buildBloc(); }, act: (b) => b.add(const RefreshApprovals()), expect: () => [const ApprovalsEmpty()], ); blocTest( 'emits ApprovalError on refresh failure', build: () { when(mockGetPendingApprovals( organizationId: anyNamed('organizationId'), )).thenAnswer((_) async => const Left(ServerFailure('serveur indisponible'))); return buildBloc(); }, act: (b) => b.add(const RefreshApprovals()), expect: () => [const ApprovalError('serveur indisponible')], ); }); // ─── State equality ────────────────────────────────────────────────────── group('State equality / props', () { test('ApprovalsLoaded equality', () { final s1 = ApprovalsLoaded( approvals: [_pendingApproval], pendingCount: 1, ); final s2 = ApprovalsLoaded( approvals: [_pendingApproval], pendingCount: 1, ); expect(s1, equals(s2)); }); test('ApprovalActionInProgress equality', () { expect( const ApprovalActionInProgress('approve'), equals(const ApprovalActionInProgress('approve')), ); expect( const ApprovalActionInProgress('approve'), isNot(equals(const ApprovalActionInProgress('reject'))), ); }); test('TransactionApproval.isPending is true for pending status', () { expect(_pendingApproval.isPending, isTrue); }); test('TransactionApproval.isCompleted is false for pending', () { expect(_pendingApproval.isCompleted, isFalse); }); test('TransactionApproval.requiredApprovals matches level', () { expect(_pendingApproval.requiredApprovals, equals(1)); // level1 }); test('ApprovalsEmpty has default message', () { const s = ApprovalsEmpty(); expect(s.message, 'Aucune approbation en attente'); }); }); }