fix: fahrerbezogene Daten bei jeder Anmeldung neu laden
Tour, Datum, Fahrzeuge, Fahrzeugauswahl und Phase lebten bisher pro Server-Umgebung, nicht pro Anmeldung. Nach Abmelden von 423 und Anmelden von 414 lieferte der gecachte CarsBloc weiter die Fahrzeuge von 423. Neu: SessionScope unter dem AuthBloc legt diese Blocs bei jeder Anmeldung (neuer Fahrer oder Re-Login) komplett neu an; alles wird frisch vom Server geholt. Token-Refresh desselben Fahrers zählt nicht als neue Anmeldung. Zahlungsmethoden werden ebenfalls nach jeder Anmeldung neu geladen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -1,32 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hl_lieferservice/bloc/app_bloc.dart';
|
import 'package:hl_lieferservice/bloc/app_bloc.dart';
|
||||||
import 'package:hl_lieferservice/data/cache/attachment_cache.dart';
|
|
||||||
import 'package:hl_lieferservice/data/network/keycloak_oidc_token_provider.dart';
|
import 'package:hl_lieferservice/data/network/keycloak_oidc_token_provider.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/presentation/login_enforcer.dart';
|
import 'package:hl_lieferservice/feature/authentication/presentation/login_enforcer.dart';
|
||||||
import 'package:hl_lieferservice/main.dart' show locator;
|
import 'package:hl_lieferservice/main.dart' show locator;
|
||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/car_selection/presentation/car_selection_enforcer.dart';
|
import 'package:hl_lieferservice/feature/car_selection/presentation/car_selection_enforcer.dart';
|
||||||
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
|
||||||
import 'package:hl_lieferservice/data/repository/cars_repository_impl.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart';
|
import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart';
|
||||||
import 'package:hl_lieferservice/data/repository/payment_methods_repository_impl.dart';
|
import 'package:hl_lieferservice/data/repository/payment_methods_repository_impl.dart';
|
||||||
import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart';
|
import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart';
|
||||||
import 'package:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
import 'package:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
||||||
import 'package:hl_lieferservice/data/repository/tour_repository_impl.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_date_cubit.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart';
|
import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart';
|
||||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart';
|
import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart';
|
||||||
|
import 'package:hl_lieferservice/widget/session_scope.dart';
|
||||||
|
|
||||||
import 'package:hl_lieferservice/bloc/app_states.dart';
|
import 'package:hl_lieferservice/bloc/app_states.dart';
|
||||||
import 'home/presentation/home.dart';
|
import 'home/presentation/home.dart';
|
||||||
@ -67,62 +56,6 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
// und ggf. direkt einloggen.
|
// und ggf. direkt einloggen.
|
||||||
..add(const RestoreSessionRequested()),
|
..add(const RestoreSessionRequested()),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
|
||||||
// Phase-C+D-2-Migration: produktive TourRepository-Impl
|
|
||||||
// gegen das generierte Rust-Backend-API. Account-Filter
|
|
||||||
// serverseitig aus dem JWT, deshalb braucht der Bloc
|
|
||||||
// keinen AuthBloc-Bezug mehr.
|
|
||||||
create: (context) => TourBloc(
|
|
||||||
tourRepository: TourRepositoryImpl(locator<HolzleitnerApi>()),
|
|
||||||
opBloc: context.read<OperationBloc>(),
|
|
||||||
attachmentCache: locator<AttachmentCache>(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
// Hält das im Header gewählte Tour-Datum (null = heute).
|
|
||||||
// Steuert, welchen Tag der TourBloc lädt.
|
|
||||||
create: (context) => TourDateCubit(),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
CarSelectBloc(
|
|
||||||
repository: CarSelectionRepository(
|
|
||||||
environmentId: state.active.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
// Phase-D-Migration: produktive CarsRepository-Impl
|
|
||||||
// gegen das generierte Rust-Backend-API. Account-Filter
|
|
||||||
// serverseitig aus dem JWT, deshalb braucht der Bloc
|
|
||||||
// keinen AuthBloc-Bezug mehr.
|
|
||||||
create: (context) => CarsBloc(
|
|
||||||
repository: CarsRepositoryImpl(locator<HolzleitnerApi>()),
|
|
||||||
opBloc: context.read<OperationBloc>(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
// PhaseBloc liest die Team-Fahrzeug-Anzahl jetzt direkt
|
|
||||||
// aus dem CarsBloc — der ist die alleinige Quelle der
|
|
||||||
// Fahrzeug-Stammdaten. Beim ersten Load eines Fahrzeugs
|
|
||||||
// bestimmt das die Eintrittsphase (Auswählen vs. Sortieren).
|
|
||||||
create: (context) => PhaseBloc(
|
|
||||||
carCountResolver: () {
|
|
||||||
final carsState = context.read<CarsBloc>().state;
|
|
||||||
return carsState is CarsLoaded
|
|
||||||
? carsState.cars.length
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
// Bindet die persistierten Phasen-Häkchen an die aktuelle
|
|
||||||
// Tour-Version (Tour.syncedAt). Ein erneuter Sync/Seed
|
|
||||||
// schreibt eine neue syncedAt → neuer Token → frische
|
|
||||||
// Phasen, ohne dass alte lokale Häkchen hängen bleiben.
|
|
||||||
// Ableitung zentral in `TourStatePhaseToken` (tour_state.dart),
|
|
||||||
// dieselbe Quelle wie `Home._ensurePhaseLoaded`.
|
|
||||||
tourTokenResolver: () =>
|
|
||||||
context.read<TourBloc>().state.phaseToken,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
// Zahlungsmethoden sind firmenweite Stammdaten — wir laden
|
// Zahlungsmethoden sind firmenweite Stammdaten — wir laden
|
||||||
// sie einmal beim App-Start und cachen sie im Cubit. Der
|
// sie einmal beim App-Start und cachen sie im Cubit. Der
|
||||||
@ -134,46 +67,42 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
)..load(),
|
)..load(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
// Fahrerbezogene Blocs (Tour, Fahrzeuge, Auswahl, Phase) je
|
||||||
title: 'Holzleitner Auslieferung',
|
// Anmeldung neu — siehe SessionScope.
|
||||||
debugShowCheckedModeBanner: false,
|
child: SessionScope(
|
||||||
// Wrap the Navigator (not just the home route) so the loading
|
environmentId: state.active.id,
|
||||||
// overlay covers every pushed route — DeliveryDetail, Cars,
|
child: MaterialApp(
|
||||||
// dialogs, etc. — not only the initial home tree.
|
title: 'Holzleitner Auslieferung',
|
||||||
builder: (context, child) =>
|
debugShowCheckedModeBanner: false,
|
||||||
OperationViewEnforcer(child: child ?? const SizedBox.shrink()),
|
// Wrap the Navigator (not just the home route) so the loading
|
||||||
home: BlocBuilder<AppBloc, AppState>(
|
// overlay covers every pushed route — DeliveryDetail, Cars,
|
||||||
builder: (context, state) {
|
// dialogs, etc. — not only the initial home tree.
|
||||||
if (state is AppConfigLoading) {
|
builder: (context, child) =>
|
||||||
return Scaffold(
|
OperationViewEnforcer(child: child ?? const SizedBox.shrink()),
|
||||||
body: Center(child: CircularProgressIndicator()),
|
home: BlocBuilder<AppBloc, AppState>(
|
||||||
);
|
builder: (context, state) {
|
||||||
}
|
if (state is AppConfigLoading) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (state is AppConfigLoadingFailed) {
|
if (state is AppConfigLoadingFailed) {
|
||||||
return Scaffold(body: Center(child: Text(state.message)));
|
return Scaffold(body: Center(child: Text(state.message)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is AppConfigLoaded) {
|
if (state is AppConfigLoaded) {
|
||||||
// Stammdaten nach JEDEM erfolgreichen (Re-)Login neu laden.
|
// Zahlungsmethoden lädt SessionScope nach jeder Anmeldung neu.
|
||||||
// Der Cubit lädt zwar beim App-Start — aber ohne Session
|
return LoginEnforcer(
|
||||||
// endet das in 401 („Sitzung abgelaufen") und blieb bisher
|
|
||||||
// für immer im Fehlerzustand hängen, auch nach dem Login.
|
|
||||||
return BlocListener<AuthBloc, AuthState>(
|
|
||||||
listenWhen: (prev, curr) =>
|
|
||||||
curr is Authenticated && prev is! Authenticated,
|
|
||||||
listener: (context, _) =>
|
|
||||||
context.read<PaymentMethodsCubit>().load(),
|
|
||||||
child: LoginEnforcer(
|
|
||||||
child: CarSelectionEnforcer(child: Home()),
|
child: CarSelectionEnforcer(child: Home()),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Container();
|
return Container();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
routes: {"/cars": (context) => CarManagementPage()},
|
||||||
),
|
),
|
||||||
routes: {"/cars": (context) => CarManagementPage()},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
123
lib/widget/session_scope.dart
Normal file
123
lib/widget/session_scope.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/data/cache/attachment_cache.dart';
|
||||||
|
import 'package:hl_lieferservice/data/repository/cars_repository_impl.dart';
|
||||||
|
import 'package:hl_lieferservice/data/repository/tour_repository_impl.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/delivery/bloc/phase_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_date_cubit.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart';
|
||||||
|
import 'package:hl_lieferservice/main.dart' show locator;
|
||||||
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
||||||
|
|
||||||
|
/// Fahrerbezogene Blocs — pro Anmeldung frisch.
|
||||||
|
///
|
||||||
|
/// Tour, gewähltes Datum, Fahrzeuge, Fahrzeugauswahl und Phase gehören zum
|
||||||
|
/// angemeldeten Fahrer. Sie leben unterhalb des [AuthBloc] und werden bei
|
||||||
|
/// JEDER Anmeldung (neuer Fahrer oder derselbe nach Abmelden/Session-Ablauf)
|
||||||
|
/// komplett neu angelegt: Nichts vom vorherigen Fahrer bleibt im Speicher,
|
||||||
|
/// alles wird neu vom Server geholt. Ein bloßer Token-Refresh
|
||||||
|
/// (Authenticated → Authenticated, gleicher Fahrer) zählt nicht als neue
|
||||||
|
/// Anmeldung.
|
||||||
|
///
|
||||||
|
/// [child] ist die `MaterialApp`, damit auch gepushte Routen (Lieferdetails,
|
||||||
|
/// Fahrzeugverwaltung) die Session-Blocs sehen. Eine neue Sitzung baut sie
|
||||||
|
/// deshalb mit neu auf — der Navigator startet wieder bei Home.
|
||||||
|
class SessionScope extends StatefulWidget {
|
||||||
|
const SessionScope({
|
||||||
|
super.key,
|
||||||
|
required this.environmentId,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Aktive Server-Umgebung; die Fahrzeugauswahl wird je Umgebung gespeichert.
|
||||||
|
final String environmentId;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SessionScope> createState() => _SessionScopeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SessionScopeState extends State<SessionScope> {
|
||||||
|
/// Zählt Anmeldungen; Teil des Keys, damit jede Anmeldung einen neuen
|
||||||
|
/// Bloc-Baum erzeugt.
|
||||||
|
int _session = 0;
|
||||||
|
int? _personalnummer;
|
||||||
|
|
||||||
|
bool _startsNewSession(AuthState previous, AuthState current) {
|
||||||
|
if (current is! Authenticated) return false;
|
||||||
|
return previous is! Authenticated ||
|
||||||
|
previous.personalnummer != current.personalnummer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAuthChanged(BuildContext context, AuthState state) {
|
||||||
|
final authenticated = state as Authenticated;
|
||||||
|
setState(() {
|
||||||
|
_session++;
|
||||||
|
_personalnummer = authenticated.personalnummer;
|
||||||
|
});
|
||||||
|
// Firmenweite Stammdaten liegen eine Ebene höher; ohne Session endet
|
||||||
|
// deren Start-Load in 401 — daher nach jeder Anmeldung neu laden.
|
||||||
|
context.read<PaymentMethodsCubit>().load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<AuthBloc, AuthState>(
|
||||||
|
listenWhen: _startsNewSession,
|
||||||
|
listener: _onAuthChanged,
|
||||||
|
child: MultiBlocProvider(
|
||||||
|
key: ValueKey('session-$_session-$_personalnummer'),
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
// Account-Filter serverseitig aus dem JWT.
|
||||||
|
create: (context) => TourBloc(
|
||||||
|
tourRepository: TourRepositoryImpl(locator<HolzleitnerApi>()),
|
||||||
|
opBloc: context.read<OperationBloc>(),
|
||||||
|
attachmentCache: locator<AttachmentCache>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
// Im Header gewähltes Tour-Datum (null = heute).
|
||||||
|
create: (context) => TourDateCubit(),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => CarSelectBloc(
|
||||||
|
repository: CarSelectionRepository(
|
||||||
|
environmentId: widget.environmentId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
// Fahrzeuge des angemeldeten Accounts (aus dem JWT).
|
||||||
|
create: (context) => CarsBloc(
|
||||||
|
repository: CarsRepositoryImpl(locator<HolzleitnerApi>()),
|
||||||
|
opBloc: context.read<OperationBloc>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
// Eintrittsphase hängt an der Fahrzeug-Anzahl (CarsBloc) und an
|
||||||
|
// der Tour-Version (Tour.syncedAt, siehe `TourStatePhaseToken`).
|
||||||
|
create: (context) => PhaseBloc(
|
||||||
|
carCountResolver: () {
|
||||||
|
final carsState = context.read<CarsBloc>().state;
|
||||||
|
return carsState is CarsLoaded ? carsState.cars.length : null;
|
||||||
|
},
|
||||||
|
tourTokenResolver: () =>
|
||||||
|
context.read<TourBloc>().state.phaseToken,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
test/session_scope_test.dart
Normal file
126
test/session_scope_test.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/data/network/auth_session_event.dart';
|
||||||
|
import 'package:hl_lieferservice/data/network/keycloak_oidc_token_provider.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/payment_method.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/repository/payment_methods_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_date_cubit.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart';
|
||||||
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/widget/session_scope.dart';
|
||||||
|
|
||||||
|
/// Token-Provider ohne Keycloak: der Test steuert Login/Logout über [emit].
|
||||||
|
class _FakeTokenProvider implements KeycloakOidcTokenProvider {
|
||||||
|
final _events = StreamController<AuthSessionEvent>.broadcast();
|
||||||
|
|
||||||
|
void emit(AuthSessionEvent event) => _events.add(event);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<AuthSessionEvent> get events => _events.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CountingPaymentMethods implements PaymentMethodsRepository {
|
||||||
|
int loads = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PaymentMethod>> list({bool includeInactive = false}) async {
|
||||||
|
loads++;
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthLoggedIn _login(int personalnummer) =>
|
||||||
|
AuthLoggedIn({'personalnummer': personalnummer, 'name': 'Fahrer'});
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late _FakeTokenProvider provider;
|
||||||
|
late _CountingPaymentMethods payments;
|
||||||
|
late List<CarSelectBloc> carSelections;
|
||||||
|
late List<TourDateCubit> tourDates;
|
||||||
|
|
||||||
|
Future<void> pumpApp(WidgetTester tester) async {
|
||||||
|
provider = _FakeTokenProvider();
|
||||||
|
payments = _CountingPaymentMethods();
|
||||||
|
carSelections = [];
|
||||||
|
tourDates = [];
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(create: (_) => OperationBloc()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => AuthBloc(
|
||||||
|
tokenProvider: provider,
|
||||||
|
operationBloc: context.read<OperationBloc>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) => PaymentMethodsCubit(repository: payments),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: SessionScope(
|
||||||
|
environmentId: 'test',
|
||||||
|
child: MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
carSelections.add(context.read<CarSelectBloc>());
|
||||||
|
tourDates.add(context.read<TourDateCubit>());
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> send(WidgetTester tester, AuthSessionEvent event) async {
|
||||||
|
provider.emit(event);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('jede Anmeldung bekommt frische Fahrer-Blocs', (tester) async {
|
||||||
|
await pumpApp(tester);
|
||||||
|
|
||||||
|
await send(tester, _login(423));
|
||||||
|
final fuer423 = carSelections.last;
|
||||||
|
expect(payments.loads, 1);
|
||||||
|
|
||||||
|
// Abmelden und anderer Fahrer: nichts von 423 darf übrig bleiben.
|
||||||
|
await send(tester, const AuthLoggedOut());
|
||||||
|
await send(tester, _login(414));
|
||||||
|
expect(carSelections.last, isNot(same(fuer423)));
|
||||||
|
expect(tourDates.last, isNot(same(tourDates.first)));
|
||||||
|
expect(payments.loads, 2);
|
||||||
|
|
||||||
|
// Auch derselbe Fahrer nach erneutem Login lädt neu.
|
||||||
|
final fuer414 = carSelections.last;
|
||||||
|
await send(tester, const AuthLoggedOut());
|
||||||
|
await send(tester, _login(414));
|
||||||
|
expect(carSelections.last, isNot(same(fuer414)));
|
||||||
|
expect(payments.loads, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Token-Refresh desselben Fahrers behält die Blocs',
|
||||||
|
(tester) async {
|
||||||
|
await pumpApp(tester);
|
||||||
|
await send(tester, _login(423));
|
||||||
|
final vorher = carSelections.last;
|
||||||
|
|
||||||
|
await send(tester, _login(423));
|
||||||
|
|
||||||
|
expect(carSelections.last, same(vorher));
|
||||||
|
expect(payments.loads, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user