## 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>
739 lines
26 KiB
Dart
739 lines
26 KiB
Dart
import 'package:bloc_test/bloc_test.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:mockito/annotations.dart';
|
|
import 'package:mockito/mockito.dart';
|
|
|
|
import 'package:unionflow_mobile_apps/features/onboarding/bloc/onboarding_bloc.dart';
|
|
import 'package:unionflow_mobile_apps/features/onboarding/data/datasources/souscription_datasource.dart';
|
|
import 'package:unionflow_mobile_apps/features/onboarding/data/models/formule_model.dart';
|
|
import 'package:unionflow_mobile_apps/features/onboarding/data/models/souscription_status_model.dart';
|
|
|
|
@GenerateMocks([SouscriptionDatasource])
|
|
import 'onboarding_bloc_test.mocks.dart';
|
|
|
|
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
|
|
List<FormuleModel> _formules() => [
|
|
const FormuleModel(
|
|
code: 'BASIC',
|
|
libelle: 'Basic',
|
|
plage: 'PETITE',
|
|
plageLibelle: '1-20 membres',
|
|
minMembres: 1,
|
|
maxMembres: 20,
|
|
prixMensuel: 5000,
|
|
prixAnnuel: 50000,
|
|
ordreAffichage: 1,
|
|
),
|
|
const FormuleModel(
|
|
code: 'STANDARD',
|
|
libelle: 'Standard',
|
|
plage: 'MOYENNE',
|
|
plageLibelle: '21-100 membres',
|
|
minMembres: 21,
|
|
maxMembres: 100,
|
|
prixMensuel: 15000,
|
|
prixAnnuel: 150000,
|
|
ordreAffichage: 2,
|
|
),
|
|
];
|
|
|
|
SouscriptionStatusModel _souscription({String? waveLaunchUrl}) =>
|
|
SouscriptionStatusModel(
|
|
souscriptionId: 'sosc-1',
|
|
statutValidation: 'EN_ATTENTE_PAIEMENT',
|
|
typeFormule: 'BASIC',
|
|
plageMembres: 'PETITE',
|
|
plageLibelle: '1-20 membres',
|
|
typePeriode: 'MENSUEL',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
montantTotal: 5000,
|
|
waveLaunchUrl: waveLaunchUrl,
|
|
organisationId: 'org-1',
|
|
);
|
|
|
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
|
|
void main() {
|
|
late OnboardingBloc bloc;
|
|
late MockSouscriptionDatasource mockDatasource;
|
|
|
|
setUp(() {
|
|
mockDatasource = MockSouscriptionDatasource();
|
|
bloc = OnboardingBloc(mockDatasource);
|
|
});
|
|
|
|
tearDown(() => bloc.close());
|
|
|
|
// ─── Initial state ──────────────────────────────────────────────────────────
|
|
|
|
test('initial state is OnboardingInitial', () {
|
|
expect(bloc.state, isA<OnboardingInitial>());
|
|
});
|
|
|
|
// ─── OnboardingStarted ──────────────────────────────────────────────────────
|
|
|
|
group('OnboardingStarted — NO_SUBSCRIPTION', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingStepFormule] with loaded formulas',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(
|
|
initialState: 'NO_SUBSCRIPTION',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
)),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepFormule>()
|
|
.having((s) => s.formules.length, 'formules count', 2),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingError] when getFormules throws',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenThrow(Exception('Network error'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(initialState: 'NO_SUBSCRIPTION')),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingError>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('Impossible de charger'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'falls back to OnboardingStepFormule for unknown initialState',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(initialState: 'UNKNOWN_STATE')),
|
|
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepFormule>()],
|
|
);
|
|
});
|
|
|
|
group('OnboardingStarted — AWAITING_PAYMENT', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepSummary when getMaSouscription returns a souscription',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT')),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepSummary>().having(
|
|
(s) => s.souscription.souscriptionId,
|
|
'souscriptionId',
|
|
'sosc-1',
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'falls back to OnboardingStepFormule when getMaSouscription returns null',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => null);
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT')),
|
|
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepFormule>()],
|
|
);
|
|
});
|
|
|
|
group('OnboardingStarted — PAYMENT_INITIATED', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepPaiement when souscription has waveLaunchUrl',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription()).thenAnswer(
|
|
(_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/xyz'),
|
|
);
|
|
return bloc;
|
|
},
|
|
act: (b) =>
|
|
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED')),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepPaiement>()
|
|
.having((s) => s.waveLaunchUrl, 'waveLaunchUrl', 'https://wave.com/pay/xyz'),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepAttente when souscription has no waveLaunchUrl',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription()); // no waveLaunchUrl
|
|
return bloc;
|
|
},
|
|
act: (b) =>
|
|
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED')),
|
|
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepAttente>()],
|
|
);
|
|
});
|
|
|
|
group('OnboardingStarted — AWAITING_VALIDATION', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepAttente with the fetched souscription',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
return bloc;
|
|
},
|
|
act: (b) =>
|
|
b.add(const OnboardingStarted(initialState: 'AWAITING_VALIDATION')),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepAttente>().having(
|
|
(s) => s.souscription?.souscriptionId,
|
|
'souscriptionId',
|
|
'sosc-1',
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
group('OnboardingStarted — VALIDATED', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepAttente for edge case where activation not yet effective',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(initialState: 'VALIDATED')),
|
|
expect: () => [isA<OnboardingLoading>(), isA<OnboardingStepAttente>()],
|
|
);
|
|
});
|
|
|
|
group('OnboardingStarted — REJECTED', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingRejected with commentaire from statutValidation',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription()).thenAnswer((_) async =>
|
|
SouscriptionStatusModel(
|
|
souscriptionId: 'sosc-2',
|
|
statutValidation: 'Documents manquants',
|
|
typeFormule: 'BASIC',
|
|
plageMembres: 'PETITE',
|
|
plageLibelle: '1-20',
|
|
typePeriode: 'MENSUEL',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const OnboardingStarted(initialState: 'REJECTED')),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingRejected>()
|
|
.having((s) => s.commentaire, 'commentaire', 'Documents manquants'),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ─── OnboardingFormuleSelected ─────────────────────────────────────────────
|
|
|
|
group('OnboardingFormuleSelected', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepPeriode with selected formule and plage',
|
|
build: () {
|
|
// Pre-load formulas by starting first
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
return bloc;
|
|
},
|
|
seed: () => OnboardingStepFormule(_formules()),
|
|
act: (b) => b.add(const OnboardingFormuleSelected(
|
|
codeFormule: 'BASIC',
|
|
plage: 'PETITE',
|
|
)),
|
|
expect: () => [
|
|
isA<OnboardingStepPeriode>()
|
|
.having((s) => s.codeFormule, 'codeFormule', 'BASIC')
|
|
.having((s) => s.plage, 'plage', 'PETITE'),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepPeriode with STANDARD/GRANDE',
|
|
build: () => bloc,
|
|
seed: () => OnboardingStepFormule(_formules()),
|
|
act: (b) => b.add(const OnboardingFormuleSelected(
|
|
codeFormule: 'STANDARD',
|
|
plage: 'GRANDE',
|
|
)),
|
|
expect: () => [
|
|
isA<OnboardingStepPeriode>()
|
|
.having((s) => s.codeFormule, 'codeFormule', 'STANDARD')
|
|
.having((s) => s.plage, 'plage', 'GRANDE'),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ─── OnboardingPeriodeSelected ─────────────────────────────────────────────
|
|
|
|
group('OnboardingPeriodeSelected', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'does not emit a new state (only updates internal data)',
|
|
build: () => bloc,
|
|
act: (b) => b.add(const OnboardingPeriodeSelected(
|
|
typePeriode: 'MENSUEL',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
)),
|
|
expect: () => [],
|
|
);
|
|
});
|
|
|
|
// ─── OnboardingDemandeConfirmee ────────────────────────────────────────────
|
|
|
|
group('OnboardingDemandeConfirmee', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingError when required data is missing',
|
|
build: () => bloc,
|
|
// No formule, plage, periode, organisationId set → missing data
|
|
act: (b) => b.add(const OnboardingDemandeConfirmee()),
|
|
expect: () => [
|
|
isA<OnboardingError>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('Données manquantes'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingStepSummary] on successful creation',
|
|
build: () {
|
|
when(mockDatasource.creerDemande(
|
|
typeFormule: anyNamed('typeFormule'),
|
|
plageMembres: anyNamed('plageMembres'),
|
|
typePeriode: anyNamed('typePeriode'),
|
|
typeOrganisation: anyNamed('typeOrganisation'),
|
|
organisationId: anyNamed('organisationId'),
|
|
)).thenAnswer((_) async => _souscription());
|
|
return bloc;
|
|
},
|
|
// Seed internal state: first select formule, then periode
|
|
act: (b) async {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
b.add(const OnboardingStarted(
|
|
initialState: 'NO_SUBSCRIPTION',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingFormuleSelected(
|
|
codeFormule: 'BASIC',
|
|
plage: 'PETITE',
|
|
));
|
|
b.add(const OnboardingPeriodeSelected(
|
|
typePeriode: 'MENSUEL',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
b.add(const OnboardingDemandeConfirmee());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(), // from OnboardingStarted
|
|
isA<OnboardingStepFormule>(), // after getFormules
|
|
isA<OnboardingStepPeriode>(), // after OnboardingFormuleSelected
|
|
// OnboardingPeriodeSelected emits nothing
|
|
isA<OnboardingLoading>(), // from OnboardingDemandeConfirmee
|
|
isA<OnboardingStepSummary>()
|
|
.having((s) => s.souscription.souscriptionId, 'id', 'sosc-1'),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingError] when creerDemande returns null',
|
|
build: () {
|
|
when(mockDatasource.creerDemande(
|
|
typeFormule: anyNamed('typeFormule'),
|
|
plageMembres: anyNamed('plageMembres'),
|
|
typePeriode: anyNamed('typePeriode'),
|
|
typeOrganisation: anyNamed('typeOrganisation'),
|
|
organisationId: anyNamed('organisationId'),
|
|
)).thenAnswer((_) async => null);
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
b.add(const OnboardingStarted(
|
|
initialState: 'NO_SUBSCRIPTION',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingFormuleSelected(
|
|
codeFormule: 'BASIC',
|
|
plage: 'PETITE',
|
|
));
|
|
b.add(const OnboardingPeriodeSelected(
|
|
typePeriode: 'MENSUEL',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
b.add(const OnboardingDemandeConfirmee());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepFormule>(),
|
|
isA<OnboardingStepPeriode>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingError>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('Erreur lors de la création'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingError] when creerDemande throws',
|
|
build: () {
|
|
when(mockDatasource.creerDemande(
|
|
typeFormule: anyNamed('typeFormule'),
|
|
plageMembres: anyNamed('plageMembres'),
|
|
typePeriode: anyNamed('typePeriode'),
|
|
typeOrganisation: anyNamed('typeOrganisation'),
|
|
organisationId: anyNamed('organisationId'),
|
|
)).thenThrow(Exception('API unavailable'));
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
b.add(const OnboardingStarted(
|
|
initialState: 'NO_SUBSCRIPTION',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingFormuleSelected(
|
|
codeFormule: 'BASIC',
|
|
plage: 'PETITE',
|
|
));
|
|
b.add(const OnboardingPeriodeSelected(
|
|
typePeriode: 'MENSUEL',
|
|
typeOrganisation: 'ASSOCIATION',
|
|
organisationId: 'org-1',
|
|
));
|
|
b.add(const OnboardingDemandeConfirmee());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepFormule>(),
|
|
isA<OnboardingStepPeriode>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingError>(),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ─── OnboardingChoixPaiementOuvert ─────────────────────────────────────────
|
|
|
|
group('OnboardingChoixPaiementOuvert', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingStepChoixPaiement when souscription is loaded',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingChoixPaiementOuvert());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepSummary>(),
|
|
isA<OnboardingStepChoixPaiement>().having(
|
|
(s) => s.souscription.souscriptionId,
|
|
'souscriptionId',
|
|
'sosc-1',
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'does nothing when no souscription is loaded',
|
|
build: () => bloc,
|
|
act: (b) => b.add(const OnboardingChoixPaiementOuvert()),
|
|
expect: () => [],
|
|
);
|
|
});
|
|
|
|
// ─── OnboardingPaiementInitie ──────────────────────────────────────────────
|
|
|
|
group('OnboardingPaiementInitie', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits OnboardingError when no souscription loaded',
|
|
build: () => bloc,
|
|
act: (b) => b.add(const OnboardingPaiementInitie()),
|
|
expect: () => [
|
|
isA<OnboardingError>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('Souscription introuvable'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingStepPaiement] when initierPaiement succeeds',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
when(mockDatasource.initierPaiement('sosc-1')).thenAnswer(
|
|
(_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
|
|
);
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingPaiementInitie());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepSummary>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepPaiement>()
|
|
.having((s) => s.waveLaunchUrl, 'waveLaunchUrl', 'https://wave.com/pay/abc'),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingError] when initierPaiement returns null',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
when(mockDatasource.initierPaiement('sosc-1'))
|
|
.thenAnswer((_) async => null);
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingPaiementInitie());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepSummary>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingError>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('paiement Wave'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingError] when initierPaiement throws',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async => _souscription());
|
|
when(mockDatasource.initierPaiement('sosc-1'))
|
|
.thenThrow(Exception('Wave timeout'));
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'AWAITING_PAYMENT'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingPaiementInitie());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepSummary>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingError>(),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ─── OnboardingRetourDepuisWave ────────────────────────────────────────────
|
|
|
|
group('OnboardingRetourDepuisWave', () {
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingError] when no souscription loaded',
|
|
build: () => bloc,
|
|
act: (b) => b.add(const OnboardingRetourDepuisWave()),
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingError>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('Souscription introuvable'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingPaiementConfirme] when confirmerPaiement returns true',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription())
|
|
.thenAnswer((_) async =>
|
|
_souscription(waveLaunchUrl: 'https://wave.com/pay/abc'));
|
|
when(mockDatasource.initierPaiement('sosc-1')).thenAnswer(
|
|
(_) async => _souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
|
|
);
|
|
when(mockDatasource.confirmerPaiement('sosc-1'))
|
|
.thenAnswer((_) async => true);
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingRetourDepuisWave());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepPaiement>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingPaiementConfirme>(),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingPaiementEchoue] when confirmerPaiement returns false',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription()).thenAnswer(
|
|
(_) async =>
|
|
_souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
|
|
);
|
|
when(mockDatasource.confirmerPaiement('sosc-1'))
|
|
.thenAnswer((_) async => false);
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingRetourDepuisWave());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepPaiement>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingPaiementEchoue>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('confirmé'),
|
|
),
|
|
],
|
|
);
|
|
|
|
blocTest<OnboardingBloc, OnboardingState>(
|
|
'emits [OnboardingLoading, OnboardingPaiementEchoue] when confirmerPaiement throws',
|
|
build: () {
|
|
when(mockDatasource.getFormules())
|
|
.thenAnswer((_) async => _formules());
|
|
when(mockDatasource.getMaSouscription()).thenAnswer(
|
|
(_) async =>
|
|
_souscription(waveLaunchUrl: 'https://wave.com/pay/abc'),
|
|
);
|
|
when(mockDatasource.confirmerPaiement('sosc-1'))
|
|
.thenThrow(Exception('Timeout'));
|
|
return bloc;
|
|
},
|
|
act: (b) async {
|
|
b.add(const OnboardingStarted(initialState: 'PAYMENT_INITIATED'));
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
b.add(const OnboardingRetourDepuisWave());
|
|
},
|
|
expect: () => [
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingStepPaiement>(),
|
|
isA<OnboardingLoading>(),
|
|
isA<OnboardingPaiementEchoue>().having(
|
|
(s) => s.message,
|
|
'message',
|
|
contains('Erreur lors de la confirmation'),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
// ─── State equality ────────────────────────────────────────────────────────
|
|
|
|
group('OnboardingState equality', () {
|
|
test('OnboardingError with same message are equal', () {
|
|
const s1 = OnboardingError('err');
|
|
const s2 = OnboardingError('err');
|
|
expect(s1, equals(s2));
|
|
});
|
|
|
|
test('OnboardingStepFormule props contains formulas list', () {
|
|
final formulas = _formules();
|
|
final state = OnboardingStepFormule(formulas);
|
|
expect(state.props, contains(formulas));
|
|
});
|
|
|
|
test('OnboardingStepPeriode props are correct', () {
|
|
final formulas = _formules();
|
|
const state = OnboardingStepPeriode(
|
|
codeFormule: 'BASIC',
|
|
plage: 'PETITE',
|
|
formules: [],
|
|
);
|
|
expect(state.codeFormule, 'BASIC');
|
|
expect(state.plage, 'PETITE');
|
|
});
|
|
|
|
test('OnboardingPaiementEchoue props are correct', () {
|
|
final sosc = _souscription();
|
|
final state = OnboardingPaiementEchoue(
|
|
message: 'Error',
|
|
souscription: sosc,
|
|
waveLaunchUrl: 'https://wave.com',
|
|
);
|
|
expect(state.props,
|
|
containsAll(['Error', sosc, 'https://wave.com']));
|
|
});
|
|
});
|
|
}
|