## 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>
743 lines
24 KiB
Dart
743 lines
24 KiB
Dart
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')),
|
|
);
|
|
});
|
|
});
|
|
}
|