feat: BLoC tests complets + sécurité production + freerasp 7.5.1 migration
## Tests BLoC (Task P2.4 Mobile) - 25 nouveaux fichiers *_bloc_test.dart + mocks générés (build_runner) - Features couvertes : authentication, admin_users, adhesions, backup, communication/messaging, contributions, dashboard, finance (approval/budget), events, explore/network, feed, logs_monitoring, notifications, onboarding, organizations (switcher/types/CRUD), profile, reports, settings, solidarity - ~380 tests, > 80% coverage BLoCs ## Sécurité Production (Task P2.2) - lib/core/security/app_integrity_service.dart (freerasp 7.5.1) - Migration API breaking changes freerasp 7.5.1 : - onRootDetected → onPrivilegedAccess - onDebuggerDetected → onDebug - onSignatureDetected → onAppIntegrity - onHookDetected → onHooks - onEmulatorDetected → onSimulator - onUntrustedInstallationSourceDetected → onUnofficialStore - onDeviceBindingDetected → onDeviceBinding - onObfuscationIssuesDetected → onObfuscationIssues - Talsec.start() split → start() + attachListener() - const AndroidConfig/IOSConfig → final (constructors call ConfigVerifier) - supportedAlternativeStores → supportedStores ## Pubspec - bloc_test: ^9.1.7 → ^10.0.0 (compat flutter_bloc ^9.0.0) - freerasp 7.5.1 ## Config - android/app/build.gradle : ajustements release - lib/core/config/environment.dart : URLs API actualisées - lib/main.dart + app_router : intégrations sécurité/BLoC ## Cleanup - Suppression docs intermédiaires (TACHES_*.md, TASK_*_COMPLETION_REPORT.md, TESTS_UNITAIRES_PROGRESS.md) - .g.dart régénérés (json_serializable) - .mocks.dart régénérés (mockito) ## Résultat - 142 fichiers, +27 596 insertions - Toutes les tâches P2 mobile complétées Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
483
test/features/finance_workflow/bloc/approval_bloc_test.dart
Normal file
483
test/features/finance_workflow/bloc/approval_bloc_test.dart
Normal file
@@ -0,0 +1,483 @@
|
||||
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<ApprovalInitial>());
|
||||
bloc.close();
|
||||
});
|
||||
|
||||
// ─── LoadPendingApprovals ────────────────────────────────────────────────
|
||||
|
||||
group('LoadPendingApprovals', () {
|
||||
blocTest<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalState>(
|
||||
(s) => s is ApprovalsLoaded && s.pendingCount == 3,
|
||||
'ApprovalsLoaded with pendingCount == 3',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalsLoaded>(),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── LoadApprovalById ────────────────────────────────────────────────────
|
||||
|
||||
group('LoadApprovalById', () {
|
||||
blocTest<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<TransactionApproved>(),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalState>(
|
||||
(s) =>
|
||||
s is TransactionApproved &&
|
||||
s.message == 'Transaction approuvée avec succès',
|
||||
'TransactionApproved with correct message',
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── RejectTransactionEvent ──────────────────────────────────────────────
|
||||
|
||||
group('RejectTransactionEvent', () {
|
||||
blocTest<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalState>(
|
||||
(s) =>
|
||||
s is TransactionRejected &&
|
||||
s.message == 'Transaction rejetée avec succès',
|
||||
'TransactionRejected with correct message',
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── RefreshApprovals ────────────────────────────────────────────────────
|
||||
|
||||
group('RefreshApprovals', () {
|
||||
blocTest<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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<ApprovalBloc, ApprovalState>(
|
||||
'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');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in unionflow_mobile_apps/test/features/finance_workflow/bloc/approval_bloc_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i5;
|
||||
|
||||
import 'package:dartz/dartz.dart' as _i3;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:unionflow_mobile_apps/core/error/failures.dart' as _i6;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/transaction_approval.dart'
|
||||
as _i7;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart'
|
||||
as _i2;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/approve_transaction.dart'
|
||||
as _i9;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_approval_by_id.dart'
|
||||
as _i8;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_pending_approvals.dart'
|
||||
as _i4;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/reject_transaction.dart'
|
||||
as _i10;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeFinanceWorkflowRepository_0 extends _i1.SmartFake
|
||||
implements _i2.FinanceWorkflowRepository {
|
||||
_FakeFinanceWorkflowRepository_0(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeEither_1<L, R> extends _i1.SmartFake implements _i3.Either<L, R> {
|
||||
_FakeEither_1(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [GetPendingApprovals].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGetPendingApprovals extends _i1.Mock
|
||||
implements _i4.GetPendingApprovals {
|
||||
MockGetPendingApprovals() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, List<_i7.TransactionApproval>>> call({
|
||||
String? organizationId,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [], {#organizationId: organizationId}),
|
||||
returnValue:
|
||||
_i5.Future<
|
||||
_i3.Either<_i6.Failure, List<_i7.TransactionApproval>>
|
||||
>.value(
|
||||
_FakeEither_1<_i6.Failure, List<_i7.TransactionApproval>>(
|
||||
this,
|
||||
Invocation.method(#call, [], {
|
||||
#organizationId: organizationId,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<
|
||||
_i3.Either<_i6.Failure, List<_i7.TransactionApproval>>
|
||||
>);
|
||||
}
|
||||
|
||||
/// A class which mocks [GetApprovalById].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGetApprovalById extends _i1.Mock implements _i8.GetApprovalById {
|
||||
MockGetApprovalById() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, _i7.TransactionApproval>> call(
|
||||
String? approvalId,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [approvalId]),
|
||||
returnValue:
|
||||
_i5.Future<
|
||||
_i3.Either<_i6.Failure, _i7.TransactionApproval>
|
||||
>.value(
|
||||
_FakeEither_1<_i6.Failure, _i7.TransactionApproval>(
|
||||
this,
|
||||
Invocation.method(#call, [approvalId]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, _i7.TransactionApproval>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [ApproveTransaction].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockApproveTransaction extends _i1.Mock
|
||||
implements _i9.ApproveTransaction {
|
||||
MockApproveTransaction() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, _i7.TransactionApproval>> call({
|
||||
required String? approvalId,
|
||||
String? comment,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [], {
|
||||
#approvalId: approvalId,
|
||||
#comment: comment,
|
||||
}),
|
||||
returnValue:
|
||||
_i5.Future<
|
||||
_i3.Either<_i6.Failure, _i7.TransactionApproval>
|
||||
>.value(
|
||||
_FakeEither_1<_i6.Failure, _i7.TransactionApproval>(
|
||||
this,
|
||||
Invocation.method(#call, [], {
|
||||
#approvalId: approvalId,
|
||||
#comment: comment,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, _i7.TransactionApproval>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [RejectTransaction].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockRejectTransaction extends _i1.Mock implements _i10.RejectTransaction {
|
||||
MockRejectTransaction() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, _i7.TransactionApproval>> call({
|
||||
required String? approvalId,
|
||||
required String? reason,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [], {
|
||||
#approvalId: approvalId,
|
||||
#reason: reason,
|
||||
}),
|
||||
returnValue:
|
||||
_i5.Future<
|
||||
_i3.Either<_i6.Failure, _i7.TransactionApproval>
|
||||
>.value(
|
||||
_FakeEither_1<_i6.Failure, _i7.TransactionApproval>(
|
||||
this,
|
||||
Invocation.method(#call, [], {
|
||||
#approvalId: approvalId,
|
||||
#reason: reason,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, _i7.TransactionApproval>>);
|
||||
}
|
||||
742
test/features/finance_workflow/bloc/budget_bloc_test.dart
Normal file
742
test/features/finance_workflow/bloc/budget_bloc_test.dart
Normal file
@@ -0,0 +1,742 @@
|
||||
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/budget.dart';
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/create_budget.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/usecases/get_budget_tracking.dart';
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budgets.dart';
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/budget_bloc.dart';
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/budget_event.dart';
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/presentation/bloc/budget_state.dart';
|
||||
|
||||
@GenerateMocks([
|
||||
GetBudgets,
|
||||
GetBudgetById,
|
||||
CreateBudget,
|
||||
GetBudgetTracking,
|
||||
])
|
||||
import 'budget_bloc_test.mocks.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final _now = DateTime(2026, 4, 20);
|
||||
|
||||
Budget _buildBudget({
|
||||
String id = 'budget-1',
|
||||
BudgetStatus status = BudgetStatus.active,
|
||||
String orgId = 'org-1',
|
||||
double totalPlanned = 100000,
|
||||
double totalRealized = 60000,
|
||||
}) =>
|
||||
Budget(
|
||||
id: id,
|
||||
name: 'Budget Avril 2026',
|
||||
organizationId: orgId,
|
||||
period: BudgetPeriod.monthly,
|
||||
year: 2026,
|
||||
month: 4,
|
||||
status: status,
|
||||
totalPlanned: totalPlanned,
|
||||
totalRealized: totalRealized,
|
||||
createdBy: 'user-1',
|
||||
createdAt: _now,
|
||||
startDate: DateTime(2026, 4, 1),
|
||||
endDate: DateTime(2026, 4, 30),
|
||||
);
|
||||
|
||||
final _budget = _buildBudget();
|
||||
|
||||
const _budgetLine = BudgetLine(
|
||||
id: 'line-1',
|
||||
category: BudgetCategory.contributions,
|
||||
name: 'Cotisations membres',
|
||||
amountPlanned: 50000,
|
||||
);
|
||||
|
||||
final _tracking = <String, dynamic>{
|
||||
'totalPlanned': 100000,
|
||||
'totalRealized': 60000,
|
||||
'realizationRate': 60.0,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void main() {
|
||||
late MockGetBudgets mockGetBudgets;
|
||||
late MockGetBudgetById mockGetBudgetById;
|
||||
late MockCreateBudget mockCreateBudget;
|
||||
late MockGetBudgetTracking mockGetBudgetTracking;
|
||||
|
||||
setUp(() {
|
||||
mockGetBudgets = MockGetBudgets();
|
||||
mockGetBudgetById = MockGetBudgetById();
|
||||
mockCreateBudget = MockCreateBudget();
|
||||
mockGetBudgetTracking = MockGetBudgetTracking();
|
||||
});
|
||||
|
||||
BudgetBloc buildBloc() => BudgetBloc(
|
||||
getBudgets: mockGetBudgets,
|
||||
getBudgetById: mockGetBudgetById,
|
||||
createBudget: mockCreateBudget,
|
||||
getBudgetTracking: mockGetBudgetTracking,
|
||||
);
|
||||
|
||||
// ─── initial state ───────────────────────────────────────────────────────
|
||||
|
||||
test('initial state is BudgetInitial', () {
|
||||
final bloc = buildBloc();
|
||||
expect(bloc.state, isA<BudgetInitial>());
|
||||
bloc.close();
|
||||
});
|
||||
|
||||
// ─── LoadBudgets ─────────────────────────────────────────────────────────
|
||||
|
||||
group('LoadBudgets', () {
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetsLoaded] on success',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => Right([_budget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const LoadBudgets(organizationId: 'org-1'),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
BudgetsLoaded(budgets: [_budget]),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(mockGetBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: null,
|
||||
year: null,
|
||||
)).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetsEmpty] when list is empty',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => const Right([]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgets()),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetsEmpty(),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetError] on failure',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async =>
|
||||
const Left(ServerFailure('server error')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgets(organizationId: 'org-1')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetError('server error'),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'passes status and year filters to use case',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => Right([_budget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const LoadBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: BudgetStatus.active,
|
||||
year: 2026,
|
||||
),
|
||||
),
|
||||
verify: (_) {
|
||||
verify(mockGetBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: BudgetStatus.active,
|
||||
year: 2026,
|
||||
)).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'BudgetsLoaded preserves filterStatus and filterYear',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => Right([_budget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const LoadBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: BudgetStatus.draft,
|
||||
year: 2025,
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
BudgetsLoaded(
|
||||
budgets: [_budget],
|
||||
filterStatus: BudgetStatus.draft,
|
||||
filterYear: 2025,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── LoadBudgetById ───────────────────────────────────────────────────────
|
||||
|
||||
group('LoadBudgetById', () {
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetDetailLoaded] on success',
|
||||
build: () {
|
||||
when(mockGetBudgetById('budget-1'))
|
||||
.thenAnswer((_) async => Right(_budget));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgetById('budget-1')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
BudgetDetailLoaded(_budget),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(mockGetBudgetById('budget-1')).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetError] when not found',
|
||||
build: () {
|
||||
when(mockGetBudgetById(any))
|
||||
.thenAnswer((_) async =>
|
||||
const Left(NotFoundFailure('Budget introuvable')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgetById('unknown')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetError('Budget introuvable'),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetError] on network failure',
|
||||
build: () {
|
||||
when(mockGetBudgetById(any))
|
||||
.thenAnswer((_) async => const Left(NetworkFailure('net')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgetById('budget-1')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetError('net'),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── CreateBudgetEvent ────────────────────────────────────────────────────
|
||||
|
||||
group('CreateBudgetEvent', () {
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetActionInProgress(create), BudgetCreated] on success',
|
||||
build: () {
|
||||
when(mockCreateBudget(
|
||||
name: anyNamed('name'),
|
||||
description: anyNamed('description'),
|
||||
organizationId: anyNamed('organizationId'),
|
||||
period: anyNamed('period'),
|
||||
year: anyNamed('year'),
|
||||
month: anyNamed('month'),
|
||||
lines: anyNamed('lines'),
|
||||
)).thenAnswer((_) async => Right(_budget));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const CreateBudgetEvent(
|
||||
name: 'Budget Avril 2026',
|
||||
organizationId: 'org-1',
|
||||
period: BudgetPeriod.monthly,
|
||||
year: 2026,
|
||||
month: 4,
|
||||
lines: [_budgetLine],
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetActionInProgress('create'),
|
||||
BudgetCreated(budget: _budget),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(mockCreateBudget(
|
||||
name: 'Budget Avril 2026',
|
||||
description: null,
|
||||
organizationId: 'org-1',
|
||||
period: BudgetPeriod.monthly,
|
||||
year: 2026,
|
||||
month: 4,
|
||||
lines: const [_budgetLine],
|
||||
)).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'BudgetCreated has correct default message',
|
||||
build: () {
|
||||
when(mockCreateBudget(
|
||||
name: anyNamed('name'),
|
||||
description: anyNamed('description'),
|
||||
organizationId: anyNamed('organizationId'),
|
||||
period: anyNamed('period'),
|
||||
year: anyNamed('year'),
|
||||
month: anyNamed('month'),
|
||||
lines: anyNamed('lines'),
|
||||
)).thenAnswer((_) async => Right(_budget));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const CreateBudgetEvent(
|
||||
name: 'Budget',
|
||||
organizationId: 'org-1',
|
||||
period: BudgetPeriod.annual,
|
||||
year: 2026,
|
||||
lines: [_budgetLine],
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetActionInProgress('create'),
|
||||
predicate<BudgetState>(
|
||||
(s) => s is BudgetCreated && s.message == 'Budget créé avec succès',
|
||||
'BudgetCreated with correct message',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetActionInProgress(create), BudgetError] on validation failure',
|
||||
build: () {
|
||||
when(mockCreateBudget(
|
||||
name: anyNamed('name'),
|
||||
description: anyNamed('description'),
|
||||
organizationId: anyNamed('organizationId'),
|
||||
period: anyNamed('period'),
|
||||
year: anyNamed('year'),
|
||||
month: anyNamed('month'),
|
||||
lines: anyNamed('lines'),
|
||||
)).thenAnswer((_) async =>
|
||||
const Left(ValidationFailure('Nom du budget requis')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const CreateBudgetEvent(
|
||||
name: '',
|
||||
organizationId: 'org-1',
|
||||
period: BudgetPeriod.monthly,
|
||||
year: 2026,
|
||||
month: 4,
|
||||
lines: [_budgetLine],
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetActionInProgress('create'),
|
||||
const BudgetError('Nom du budget requis'),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetActionInProgress(create), BudgetError] on server failure',
|
||||
build: () {
|
||||
when(mockCreateBudget(
|
||||
name: anyNamed('name'),
|
||||
description: anyNamed('description'),
|
||||
organizationId: anyNamed('organizationId'),
|
||||
period: anyNamed('period'),
|
||||
year: anyNamed('year'),
|
||||
month: anyNamed('month'),
|
||||
lines: anyNamed('lines'),
|
||||
)).thenAnswer((_) async =>
|
||||
const Left(ServerFailure('server error')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const CreateBudgetEvent(
|
||||
name: 'Budget valide',
|
||||
organizationId: 'org-1',
|
||||
period: BudgetPeriod.annual,
|
||||
year: 2026,
|
||||
lines: [_budgetLine],
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetActionInProgress('create'),
|
||||
const BudgetError('server error'),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── LoadBudgetTracking ───────────────────────────────────────────────────
|
||||
|
||||
group('LoadBudgetTracking', () {
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetTrackingLoaded] on success',
|
||||
build: () {
|
||||
when(mockGetBudgetById('budget-1'))
|
||||
.thenAnswer((_) async => Right(_budget));
|
||||
when(mockGetBudgetTracking(budgetId: 'budget-1'))
|
||||
.thenAnswer((_) async => Right(_tracking));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgetTracking('budget-1')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
BudgetTrackingLoaded(budget: _budget, tracking: _tracking),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(mockGetBudgetById('budget-1')).called(1);
|
||||
verify(mockGetBudgetTracking(budgetId: 'budget-1')).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetError] when getBudgetById fails',
|
||||
build: () {
|
||||
when(mockGetBudgetById(any))
|
||||
.thenAnswer((_) async =>
|
||||
const Left(NotFoundFailure('Budget introuvable')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgetTracking('unknown')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetError('Budget introuvable'),
|
||||
],
|
||||
verify: (_) {
|
||||
// Tracking should NOT be called if budget fetch fails
|
||||
verifyNever(mockGetBudgetTracking(budgetId: anyNamed('budgetId')));
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetError] when getTracking fails',
|
||||
build: () {
|
||||
when(mockGetBudgetById('budget-1'))
|
||||
.thenAnswer((_) async => Right(_budget));
|
||||
when(mockGetBudgetTracking(budgetId: anyNamed('budgetId')))
|
||||
.thenAnswer((_) async =>
|
||||
const Left(ServerFailure('tracking error')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const LoadBudgetTracking('budget-1')),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetError('tracking error'),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── RefreshBudgets ───────────────────────────────────────────────────────
|
||||
|
||||
group('RefreshBudgets', () {
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits BudgetsLoaded without BudgetsLoading during refresh',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => Right([_budget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) =>
|
||||
b.add(const RefreshBudgets(organizationId: 'org-1')),
|
||||
// RefreshBudgets does NOT emit BudgetsLoading — goes straight to result
|
||||
expect: () => [
|
||||
BudgetsLoaded(budgets: [_budget]),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits BudgetsEmpty on refresh when list is empty',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => const Right([]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const RefreshBudgets()),
|
||||
expect: () => [const BudgetsEmpty()],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits BudgetError on refresh failure',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async =>
|
||||
const Left(ServerFailure('refresh error')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) =>
|
||||
b.add(const RefreshBudgets(organizationId: 'org-1')),
|
||||
expect: () => [const BudgetError('refresh error')],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── FilterBudgets ────────────────────────────────────────────────────────
|
||||
|
||||
group('FilterBudgets', () {
|
||||
final activeBudget =
|
||||
_buildBudget(id: 'budget-active', status: BudgetStatus.active);
|
||||
final draftBudget =
|
||||
_buildBudget(id: 'budget-draft', status: BudgetStatus.draft);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetsLoaded] with filter applied',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: BudgetStatus.active,
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => Right([activeBudget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(
|
||||
const FilterBudgets(status: BudgetStatus.active),
|
||||
),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
BudgetsLoaded(
|
||||
budgets: [activeBudget],
|
||||
filterStatus: BudgetStatus.active,
|
||||
),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(mockGetBudgets(
|
||||
organizationId: null, // no loaded state, so null
|
||||
status: BudgetStatus.active,
|
||||
year: null,
|
||||
)).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'uses organizationId from loaded state for filter',
|
||||
build: () {
|
||||
// First call (LoadBudgets) returns activeBudget with orgId='org-1'
|
||||
when(mockGetBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: null,
|
||||
year: null,
|
||||
)).thenAnswer((_) async => Right([activeBudget]));
|
||||
// Second call (FilterBudgets) returns draftBudget
|
||||
when(mockGetBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: BudgetStatus.draft,
|
||||
year: null,
|
||||
)).thenAnswer((_) async => Right([draftBudget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) async {
|
||||
b.add(const LoadBudgets(organizationId: 'org-1'));
|
||||
// Wait until BudgetsLoaded so FilterBudgets sees org-1 in state
|
||||
await b.stream.firstWhere((s) => s is BudgetsLoaded);
|
||||
b.add(const FilterBudgets(status: BudgetStatus.draft));
|
||||
},
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
BudgetsLoaded(budgets: [activeBudget]),
|
||||
const BudgetsLoading(),
|
||||
BudgetsLoaded(
|
||||
budgets: [draftBudget],
|
||||
filterStatus: BudgetStatus.draft,
|
||||
),
|
||||
],
|
||||
verify: (_) {
|
||||
// organizationId extracted from first budget in loaded state
|
||||
verify(mockGetBudgets(
|
||||
organizationId: 'org-1',
|
||||
status: BudgetStatus.draft,
|
||||
year: null,
|
||||
)).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetsEmpty] when filter returns no results',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => const Right([]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) =>
|
||||
b.add(const FilterBudgets(status: BudgetStatus.cancelled)),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetsEmpty(),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'emits [BudgetsLoading, BudgetError] on filter failure',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async =>
|
||||
const Left(ServerFailure('filter error')));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const FilterBudgets(year: 2025)),
|
||||
expect: () => [
|
||||
const BudgetsLoading(),
|
||||
const BudgetError('filter error'),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<BudgetBloc, BudgetState>(
|
||||
'filters with year only',
|
||||
build: () {
|
||||
when(mockGetBudgets(
|
||||
organizationId: anyNamed('organizationId'),
|
||||
status: anyNamed('status'),
|
||||
year: anyNamed('year'),
|
||||
)).thenAnswer((_) async => Right([_budget]));
|
||||
return buildBloc();
|
||||
},
|
||||
act: (b) => b.add(const FilterBudgets(year: 2026)),
|
||||
verify: (_) {
|
||||
verify(mockGetBudgets(
|
||||
organizationId: null,
|
||||
status: null,
|
||||
year: 2026,
|
||||
)).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ─── State equality / Budget entity ──────────────────────────────────────
|
||||
|
||||
group('Budget entity computations', () {
|
||||
test('realizationRate is correct', () {
|
||||
expect(_budget.realizationRate, closeTo(60.0, 0.01));
|
||||
});
|
||||
|
||||
test('variance is correct', () {
|
||||
expect(_budget.variance, closeTo(-40000, 0.01));
|
||||
});
|
||||
|
||||
test('isOverBudget is false when realized < planned', () {
|
||||
expect(_budget.isOverBudget, isFalse);
|
||||
});
|
||||
|
||||
test('isOverBudget is true when realized > planned', () {
|
||||
final over = _buildBudget(totalPlanned: 100000, totalRealized: 120000);
|
||||
expect(over.isOverBudget, isTrue);
|
||||
});
|
||||
|
||||
test('isActive is true for active budget', () {
|
||||
expect(_budget.isActive, isTrue);
|
||||
});
|
||||
|
||||
test('realizationRate is 0 when totalPlanned is 0', () {
|
||||
final zeroBudget = _buildBudget(totalPlanned: 0, totalRealized: 0);
|
||||
expect(zeroBudget.realizationRate, equals(0.0));
|
||||
});
|
||||
|
||||
test('varianceRate is 0 when totalPlanned is 0', () {
|
||||
final zeroBudget = _buildBudget(totalPlanned: 0, totalRealized: 0);
|
||||
expect(zeroBudget.varianceRate, equals(0.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('BudgetLine entity computations', () {
|
||||
test('realizationRate is correct', () {
|
||||
const line = BudgetLine(
|
||||
id: 'l',
|
||||
category: BudgetCategory.events,
|
||||
name: 'Test',
|
||||
amountPlanned: 10000,
|
||||
amountRealized: 7500,
|
||||
);
|
||||
expect(line.realizationRate, closeTo(75.0, 0.01));
|
||||
});
|
||||
|
||||
test('isOverBudget is false when realized <= planned', () {
|
||||
expect(_budgetLine.isOverBudget, isFalse);
|
||||
});
|
||||
|
||||
test('realizationRate is 0 when amountPlanned is 0', () {
|
||||
const line = BudgetLine(
|
||||
id: 'l',
|
||||
category: BudgetCategory.other,
|
||||
name: 'Zero',
|
||||
amountPlanned: 0,
|
||||
);
|
||||
expect(line.realizationRate, equals(0.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('State equality', () {
|
||||
test('BudgetsLoaded equality', () {
|
||||
final s1 = BudgetsLoaded(budgets: [_budget]);
|
||||
final s2 = BudgetsLoaded(budgets: [_budget]);
|
||||
expect(s1, equals(s2));
|
||||
});
|
||||
|
||||
test('BudgetError equality', () {
|
||||
const e1 = BudgetError('msg');
|
||||
const e2 = BudgetError('msg');
|
||||
expect(e1, equals(e2));
|
||||
});
|
||||
|
||||
test('BudgetsEmpty has default message', () {
|
||||
const s = BudgetsEmpty();
|
||||
expect(s.message, 'Aucun budget trouvé');
|
||||
});
|
||||
|
||||
test('BudgetActionInProgress equality', () {
|
||||
expect(
|
||||
const BudgetActionInProgress('create'),
|
||||
equals(const BudgetActionInProgress('create')),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
220
test/features/finance_workflow/bloc/budget_bloc_test.mocks.dart
Normal file
220
test/features/finance_workflow/bloc/budget_bloc_test.mocks.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in unionflow_mobile_apps/test/features/finance_workflow/bloc/budget_bloc_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i5;
|
||||
|
||||
import 'package:dartz/dartz.dart' as _i3;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:unionflow_mobile_apps/core/error/failures.dart' as _i6;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/entities/budget.dart'
|
||||
as _i7;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/repositories/finance_workflow_repository.dart'
|
||||
as _i2;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/create_budget.dart'
|
||||
as _i9;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_by_id.dart'
|
||||
as _i8;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budget_tracking.dart'
|
||||
as _i10;
|
||||
import 'package:unionflow_mobile_apps/features/finance_workflow/domain/usecases/get_budgets.dart'
|
||||
as _i4;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeFinanceWorkflowRepository_0 extends _i1.SmartFake
|
||||
implements _i2.FinanceWorkflowRepository {
|
||||
_FakeFinanceWorkflowRepository_0(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeEither_1<L, R> extends _i1.SmartFake implements _i3.Either<L, R> {
|
||||
_FakeEither_1(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [GetBudgets].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGetBudgets extends _i1.Mock implements _i4.GetBudgets {
|
||||
MockGetBudgets() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, List<_i7.Budget>>> call({
|
||||
String? organizationId,
|
||||
_i7.BudgetStatus? status,
|
||||
int? year,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [], {
|
||||
#organizationId: organizationId,
|
||||
#status: status,
|
||||
#year: year,
|
||||
}),
|
||||
returnValue:
|
||||
_i5.Future<_i3.Either<_i6.Failure, List<_i7.Budget>>>.value(
|
||||
_FakeEither_1<_i6.Failure, List<_i7.Budget>>(
|
||||
this,
|
||||
Invocation.method(#call, [], {
|
||||
#organizationId: organizationId,
|
||||
#status: status,
|
||||
#year: year,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, List<_i7.Budget>>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [GetBudgetById].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGetBudgetById extends _i1.Mock implements _i8.GetBudgetById {
|
||||
MockGetBudgetById() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, _i7.Budget>> call(String? budgetId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [budgetId]),
|
||||
returnValue: _i5.Future<_i3.Either<_i6.Failure, _i7.Budget>>.value(
|
||||
_FakeEither_1<_i6.Failure, _i7.Budget>(
|
||||
this,
|
||||
Invocation.method(#call, [budgetId]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, _i7.Budget>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [CreateBudget].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockCreateBudget extends _i1.Mock implements _i9.CreateBudget {
|
||||
MockCreateBudget() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, _i7.Budget>> call({
|
||||
required String? name,
|
||||
String? description,
|
||||
required String? organizationId,
|
||||
required _i7.BudgetPeriod? period,
|
||||
required int? year,
|
||||
int? month,
|
||||
required List<_i7.BudgetLine>? lines,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [], {
|
||||
#name: name,
|
||||
#description: description,
|
||||
#organizationId: organizationId,
|
||||
#period: period,
|
||||
#year: year,
|
||||
#month: month,
|
||||
#lines: lines,
|
||||
}),
|
||||
returnValue: _i5.Future<_i3.Either<_i6.Failure, _i7.Budget>>.value(
|
||||
_FakeEither_1<_i6.Failure, _i7.Budget>(
|
||||
this,
|
||||
Invocation.method(#call, [], {
|
||||
#name: name,
|
||||
#description: description,
|
||||
#organizationId: organizationId,
|
||||
#period: period,
|
||||
#year: year,
|
||||
#month: month,
|
||||
#lines: lines,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, _i7.Budget>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [GetBudgetTracking].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockGetBudgetTracking extends _i1.Mock implements _i10.GetBudgetTracking {
|
||||
MockGetBudgetTracking() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.FinanceWorkflowRepository get repository =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#repository),
|
||||
returnValue: _FakeFinanceWorkflowRepository_0(
|
||||
this,
|
||||
Invocation.getter(#repository),
|
||||
),
|
||||
)
|
||||
as _i2.FinanceWorkflowRepository);
|
||||
|
||||
@override
|
||||
_i5.Future<_i3.Either<_i6.Failure, Map<String, dynamic>>> call({
|
||||
required String? budgetId,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#call, [], {#budgetId: budgetId}),
|
||||
returnValue:
|
||||
_i5.Future<_i3.Either<_i6.Failure, Map<String, dynamic>>>.value(
|
||||
_FakeEither_1<_i6.Failure, Map<String, dynamic>>(
|
||||
this,
|
||||
Invocation.method(#call, [], {#budgetId: budgetId}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i5.Future<_i3.Either<_i6.Failure, Map<String, dynamic>>>);
|
||||
}
|
||||
Reference in New Issue
Block a user