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