Auth: - profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password Multi-org (Phase 3): - OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry - org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role Navigation: - MorePage: navigation conditionnelle par typeOrganisation - Suppression adaptive_navigation (remplacé par main_navigation_layout) Auth AppAuth: - keycloak_webview_auth_service: fixes AppAuth Android - AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet Onboarding: - Nouveaux états: payment_method_page, onboarding_shared_widgets - SouscriptionStatusModel mis à jour StatutValidationSouscription Android: - build.gradle: ProGuard/R8, network_security_config - Gradle wrapper mis à jour
379 lines
14 KiB
Dart
379 lines
14 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/members/bloc/membres_bloc.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/bloc/membres_event.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/bloc/membres_state.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_members.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_by_id.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/create_member.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/update_member.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/delete_member.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/search_members.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/usecases/get_member_stats.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart';
|
|
import 'package:unionflow_mobile_apps/features/members/data/models/membre_complete_model.dart';
|
|
import 'package:unionflow_mobile_apps/shared/models/membre_search_result.dart';
|
|
import 'package:unionflow_mobile_apps/shared/models/membre_search_criteria.dart';
|
|
|
|
@GenerateMocks([
|
|
GetMembers,
|
|
GetMemberById,
|
|
CreateMember,
|
|
UpdateMember,
|
|
DeleteMember,
|
|
SearchMembers,
|
|
GetMemberStats,
|
|
IMembreRepository,
|
|
])
|
|
import 'membres_bloc_test.mocks.dart';
|
|
|
|
void main() {
|
|
late MembresBloc bloc;
|
|
late MockGetMembers mockGetMembers;
|
|
late MockGetMemberById mockGetMemberById;
|
|
late MockCreateMember mockCreateMember;
|
|
late MockUpdateMember mockUpdateMember;
|
|
late MockDeleteMember mockDeleteMember;
|
|
late MockSearchMembers mockSearchMembers;
|
|
late MockGetMemberStats mockGetMemberStats;
|
|
late MockIMembreRepository mockRepository;
|
|
|
|
const orgId = 'org-123';
|
|
|
|
MembreSearchResult emptyResult() => MembreSearchResult(
|
|
membres: [],
|
|
totalElements: 0,
|
|
totalPages: 0,
|
|
currentPage: 0,
|
|
pageSize: 20,
|
|
numberOfElements: 0,
|
|
hasNext: false,
|
|
hasPrevious: false,
|
|
isFirst: true,
|
|
isLast: true,
|
|
criteria: MembreSearchCriteria(),
|
|
executionTimeMs: 0,
|
|
);
|
|
|
|
setUp(() {
|
|
mockGetMembers = MockGetMembers();
|
|
mockGetMemberById = MockGetMemberById();
|
|
mockCreateMember = MockCreateMember();
|
|
mockUpdateMember = MockUpdateMember();
|
|
mockDeleteMember = MockDeleteMember();
|
|
mockSearchMembers = MockSearchMembers();
|
|
mockGetMemberStats = MockGetMemberStats();
|
|
mockRepository = MockIMembreRepository();
|
|
|
|
bloc = MembresBloc(
|
|
mockGetMembers,
|
|
mockGetMemberById,
|
|
mockCreateMember,
|
|
mockUpdateMember,
|
|
mockDeleteMember,
|
|
mockSearchMembers,
|
|
mockGetMemberStats,
|
|
mockRepository,
|
|
);
|
|
});
|
|
|
|
tearDown(() => bloc.close());
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// SuperAdmin — accès global sans filtre organisation
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
group('LoadMembres — SuperAdmin (sans organisationId)', () {
|
|
blocTest<MembresBloc, MembresState>(
|
|
'appelle GetMembers sans filtre org et émet MembresLoaded sans organisationId',
|
|
build: () {
|
|
when(mockGetMembers(
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
recherche: anyNamed('recherche'),
|
|
)).thenAnswer((_) async => emptyResult());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const LoadMembres()),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MembresLoaded>().having(
|
|
(s) => s.organisationId,
|
|
'organisationId',
|
|
isNull,
|
|
),
|
|
],
|
|
verify: (_) {
|
|
// GetMembers doit être appelé avec les bons paramètres
|
|
final captured = verify(mockGetMembers(
|
|
page: captureAnyNamed('page'),
|
|
size: captureAnyNamed('size'),
|
|
recherche: captureAnyNamed('recherche'),
|
|
)).captured;
|
|
expect(captured[0], equals(0)); // page
|
|
expect(captured[1], equals(20)); // size
|
|
expect(captured[2], isNull); // recherche
|
|
|
|
// SearchMembers ne doit jamais être appelé
|
|
verifyNever(mockSearchMembers(
|
|
criteria: anyNamed('criteria'),
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
));
|
|
},
|
|
);
|
|
|
|
blocTest<MembresBloc, MembresState>(
|
|
'transmet le terme de recherche à GetMembers',
|
|
build: () {
|
|
when(mockGetMembers(
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
recherche: anyNamed('recherche'),
|
|
)).thenAnswer((_) async => emptyResult());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const LoadMembres(recherche: 'Jean')),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MembresLoaded>(),
|
|
],
|
|
verify: (_) {
|
|
final captured = verify(mockGetMembers(
|
|
page: captureAnyNamed('page'),
|
|
size: captureAnyNamed('size'),
|
|
recherche: captureAnyNamed('recherche'),
|
|
)).captured;
|
|
expect(captured[2], equals('Jean')); // recherche
|
|
|
|
verifyNever(mockSearchMembers(
|
|
criteria: anyNamed('criteria'),
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
));
|
|
},
|
|
);
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// OrgAdmin — accès limité à son organisation
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
group('LoadMembres — OrgAdmin (avec organisationId)', () {
|
|
blocTest<MembresBloc, MembresState>(
|
|
'appelle SearchMembers avec organisationIds et émet MembresLoaded avec organisationId',
|
|
build: () {
|
|
when(mockSearchMembers(
|
|
criteria: anyNamed('criteria'),
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
)).thenAnswer((_) async => emptyResult());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const LoadMembres(organisationId: orgId)),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MembresLoaded>().having(
|
|
(s) => s.organisationId,
|
|
'organisationId',
|
|
equals(orgId),
|
|
),
|
|
],
|
|
verify: (_) {
|
|
// GetMembers ne doit JAMAIS être appelé pour un OrgAdmin
|
|
verifyNever(mockGetMembers(
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
recherche: anyNamed('recherche'),
|
|
));
|
|
|
|
// SearchMembers doit être appelé avec l'organisationId dans les critères
|
|
final captured = verify(mockSearchMembers(
|
|
criteria: captureAnyNamed('criteria'),
|
|
page: captureAnyNamed('page'),
|
|
size: captureAnyNamed('size'),
|
|
)).captured;
|
|
|
|
final criteria = captured[0] as MembreSearchCriteria;
|
|
expect(criteria.organisationIds, equals([orgId]));
|
|
expect(criteria.query, isNull);
|
|
expect(captured[1], equals(0)); // page
|
|
expect(captured[2], equals(20)); // size
|
|
},
|
|
);
|
|
|
|
blocTest<MembresBloc, MembresState>(
|
|
'OrgAdmin avec recherche : ajoute query aux critères',
|
|
build: () {
|
|
when(mockSearchMembers(
|
|
criteria: anyNamed('criteria'),
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
)).thenAnswer((_) async => emptyResult());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const LoadMembres(organisationId: orgId, recherche: 'Dupont')),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MembresLoaded>(),
|
|
],
|
|
verify: (_) {
|
|
final captured = verify(mockSearchMembers(
|
|
criteria: captureAnyNamed('criteria'),
|
|
page: captureAnyNamed('page'),
|
|
size: captureAnyNamed('size'),
|
|
)).captured;
|
|
|
|
final criteria = captured[0] as MembreSearchCriteria;
|
|
expect(criteria.organisationIds, equals([orgId]));
|
|
expect(criteria.query, equals('Dupont'));
|
|
},
|
|
);
|
|
|
|
blocTest<MembresBloc, MembresState>(
|
|
'OrgAdmin avec recherche vide : query non transmis',
|
|
build: () {
|
|
when(mockSearchMembers(
|
|
criteria: anyNamed('criteria'),
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
)).thenAnswer((_) async => emptyResult());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const LoadMembres(organisationId: orgId, recherche: '')),
|
|
expect: () => [const MembresLoading(), isA<MembresLoaded>()],
|
|
verify: (_) {
|
|
final captured = verify(mockSearchMembers(
|
|
criteria: captureAnyNamed('criteria'),
|
|
page: captureAnyNamed('page'),
|
|
size: captureAnyNamed('size'),
|
|
)).captured;
|
|
|
|
final criteria = captured[0] as MembreSearchCriteria;
|
|
expect(criteria.organisationIds, equals([orgId]));
|
|
// recherche vide → query null (pas transmis aux critères)
|
|
expect(criteria.query, isNull);
|
|
},
|
|
);
|
|
|
|
blocTest<MembresBloc, MembresState>(
|
|
'OrgAdmin : la pagination conserve l organisationId',
|
|
build: () {
|
|
when(mockSearchMembers(
|
|
criteria: anyNamed('criteria'),
|
|
page: anyNamed('page'),
|
|
size: anyNamed('size'),
|
|
)).thenAnswer((_) async => emptyResult());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const LoadMembres(organisationId: orgId, page: 1)),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MembresLoaded>().having(
|
|
(s) => s.organisationId,
|
|
'organisationId',
|
|
equals(orgId),
|
|
),
|
|
],
|
|
verify: (_) {
|
|
final captured = verify(mockSearchMembers(
|
|
criteria: captureAnyNamed('criteria'),
|
|
page: captureAnyNamed('page'),
|
|
size: captureAnyNamed('size'),
|
|
)).captured;
|
|
expect(captured[1], equals(1)); // page = 1
|
|
},
|
|
);
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// MembresLoaded.copyWith
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// ResetMotDePasse
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
group('ResetMotDePasse', () {
|
|
MembreCompletModel membreAvecPassword() => MembreCompletModel(
|
|
id: 'membre-id-1',
|
|
prenom: 'Jean',
|
|
nom: 'Dupont',
|
|
email: 'jean.dupont@test.com',
|
|
motDePasseTemporaire: 'NewP@ss1234',
|
|
);
|
|
|
|
blocTest<MembresBloc, MembresState>(
|
|
'émet [MembresLoading, MotDePasseReinitialise] en cas de succès',
|
|
build: () {
|
|
when(mockRepository.resetMotDePasse('membre-id-1'))
|
|
.thenAnswer((_) async => membreAvecPassword());
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const ResetMotDePasse('membre-id-1')),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MotDePasseReinitialise>()
|
|
.having((s) => s.membre.id, 'id', 'membre-id-1')
|
|
.having((s) => s.membre.motDePasseTemporaire, 'password', 'NewP@ss1234'),
|
|
],
|
|
verify: (_) {
|
|
verify(mockRepository.resetMotDePasse('membre-id-1')).called(1);
|
|
},
|
|
);
|
|
|
|
blocTest<MembresBloc, MembresState>(
|
|
'émet [MembresLoading, MembresError] si le repository lève une exception',
|
|
build: () {
|
|
when(mockRepository.resetMotDePasse('membre-id-1'))
|
|
.thenThrow(Exception('Membre sans compte Keycloak'));
|
|
return bloc;
|
|
},
|
|
act: (b) => b.add(const ResetMotDePasse('membre-id-1')),
|
|
expect: () => [
|
|
const MembresLoading(),
|
|
isA<MembresError>(),
|
|
],
|
|
);
|
|
});
|
|
|
|
group('MembresLoaded.copyWith', () {
|
|
test('preserve organisationId si non fourni', () {
|
|
const state = MembresLoaded(
|
|
membres: [],
|
|
totalElements: 5,
|
|
totalPages: 1,
|
|
organisationId: orgId,
|
|
);
|
|
final copy = state.copyWith(totalElements: 10);
|
|
expect(copy.organisationId, equals(orgId));
|
|
expect(copy.totalElements, equals(10));
|
|
});
|
|
|
|
test('met à jour organisationId si fourni', () {
|
|
const state = MembresLoaded(
|
|
membres: [],
|
|
totalElements: 5,
|
|
totalPages: 1,
|
|
organisationId: orgId,
|
|
);
|
|
final copy = state.copyWith(organisationId: 'new-org');
|
|
expect(copy.organisationId, equals('new-org'));
|
|
});
|
|
|
|
test('organisationId null par défaut si non défini', () {
|
|
const state = MembresLoaded(
|
|
membres: [],
|
|
totalElements: 0,
|
|
totalPages: 0,
|
|
);
|
|
expect(state.organisationId, isNull);
|
|
final copy = state.copyWith(totalElements: 1, totalPages: 1);
|
|
expect(copy.organisationId, isNull);
|
|
});
|
|
});
|
|
}
|