fix: gespeicherte Fahrzeugauswahl gegen Server-Fahrzeuge prüfen
Die Tagesauswahl liegt lokal pro Fahrer und wurde ungeprüft wiederhergestellt. Durch den alten Session-Fehler hatte 414 auf dem Gerät das Fahrzeug von 423 gespeichert — das erschien weiter, obwohl „Fahrzeug verwalten" (frisch vom Server) leer war. CarSelectBloc lädt beim Wiederherstellen die Fahrzeuge des Accounts vom Server und übernimmt die Auswahl nur, wenn das Fahrzeug dazugehört und aktiv ist; sonst wird sie verworfen und neu abgefragt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
||||||
import 'package:hl_lieferservice/domain/entity/car.dart';
|
import 'package:hl_lieferservice/domain/repository/cars_repository.dart';
|
||||||
|
|
||||||
import 'events.dart';
|
import 'events.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
@ -10,7 +10,12 @@ import 'state.dart';
|
|||||||
class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
||||||
final CarSelectionRepository repository;
|
final CarSelectionRepository repository;
|
||||||
|
|
||||||
CarSelectBloc({required this.repository}) : super(CarSelectInitial()) {
|
/// Fahrzeuge des angemeldeten Accounts vom Server — gegen sie wird eine
|
||||||
|
/// gespeicherte Tagesauswahl geprüft.
|
||||||
|
final CarsRepository carsRepository;
|
||||||
|
|
||||||
|
CarSelectBloc({required this.repository, required this.carsRepository})
|
||||||
|
: super(CarSelectInitial()) {
|
||||||
on<CarSelectLoad>(_load);
|
on<CarSelectLoad>(_load);
|
||||||
on<CarSelectConfirm>(_confirm);
|
on<CarSelectConfirm>(_confirm);
|
||||||
on<CarSelectChange>(_change);
|
on<CarSelectChange>(_change);
|
||||||
@ -36,23 +41,21 @@ class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
|||||||
stored.date.day == today.day;
|
stored.date.day == today.day;
|
||||||
|
|
||||||
if (validForToday) {
|
if (validForToday) {
|
||||||
emit(
|
// Die lokale Auswahl ist nur ein Zeiger. Sie gilt nur, wenn das
|
||||||
CarSelectComplete(
|
// Fahrzeug laut Server (frisch geladen) zu diesem Account gehört
|
||||||
selectedCar: Car(
|
// und aktiv ist — sonst z. B. ein Fahrzeug eines anderen Fahrers,
|
||||||
id: stored.selectedCarId!,
|
// das auf diesem Gerät gewählt wurde.
|
||||||
// accountId/active fließen aus der lokalen Selection
|
final ownCars = await carsRepository.listMine();
|
||||||
// nicht durch — wir persistieren nur (id, plate) als
|
final car = ownCars
|
||||||
// UI-Pointer. Die Tour-Logik holt die vollständigen
|
.where((c) => c.id == stored.selectedCarId && c.active)
|
||||||
// Car-Felder weiterhin aus dem CarsBloc-Listing.
|
.firstOrNull;
|
||||||
accountId: 0,
|
if (car != null) {
|
||||||
plate: stored.selectedCarPlate!,
|
emit(CarSelectComplete(selectedCar: car));
|
||||||
active: true,
|
return;
|
||||||
),
|
}
|
||||||
),
|
await repository.clearSelection(event.userId);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
emit(CarSelectRequired());
|
|
||||||
}
|
}
|
||||||
|
emit(CarSelectRequired());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint('CarSelectBloc._load failed: $e');
|
debugPrint('CarSelectBloc._load failed: $e');
|
||||||
debugPrint('Stacktrace: $st');
|
debugPrint('Stacktrace: $st');
|
||||||
|
|||||||
@ -49,6 +49,15 @@ class CarSelectionRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verwirft die gespeicherte Auswahl (z. B. Fahrzeug gehört nicht mehr
|
||||||
|
/// zum Account) — der Fahrer muss neu wählen.
|
||||||
|
Future<void> clearSelection(String userId) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_keyDate(userId));
|
||||||
|
await prefs.remove(_keyCarId(userId));
|
||||||
|
await prefs.remove(_keyCarPlate(userId));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveSelection(String userId, CarSelection selection) async {
|
Future<void> saveSelection(String userId, CarSelection selection) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
|||||||
@ -94,6 +94,7 @@ class _SessionScopeState extends State<SessionScope> {
|
|||||||
repository: CarSelectionRepository(
|
repository: CarSelectionRepository(
|
||||||
environmentId: widget.environmentId,
|
environmentId: widget.environmentId,
|
||||||
),
|
),
|
||||||
|
carsRepository: CarsRepositoryImpl(locator<HolzleitnerApi>()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
|||||||
74
test/car_selection_bloc_test.dart
Normal file
74
test/car_selection_bloc_test.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/repository/cars_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/events.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
||||||
|
|
||||||
|
/// Fahrzeuge „laut Server" für den angemeldeten Account.
|
||||||
|
class _ServerCars implements CarsRepository {
|
||||||
|
_ServerCars(this.cars);
|
||||||
|
final List<Car> cars;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Car>> listMine({bool includeInactive = false}) async => cars;
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _fremdesAuto = 'b918b721-0000-0000-0000-000000000423';
|
||||||
|
|
||||||
|
Map<String, Object> _gespeichert(String userId, String carId) => {
|
||||||
|
'flutter.car_selection_test_${userId}_date': DateTime.now().toIso8601String(),
|
||||||
|
'flutter.car_selection_test_${userId}_car_id': carId,
|
||||||
|
'flutter.car_selection_test_${userId}_car_plate': 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<CarSelectState> _laden(String userId, List<Car> serverCars) async {
|
||||||
|
final bloc = CarSelectBloc(
|
||||||
|
repository: CarSelectionRepository(environmentId: 'test'),
|
||||||
|
carsRepository: _ServerCars(serverCars),
|
||||||
|
)..add(CarSelectLoad(userId: userId));
|
||||||
|
final state = await bloc.stream.firstWhere(
|
||||||
|
(s) => s is! CarSelectLoading,
|
||||||
|
);
|
||||||
|
await bloc.close();
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('gespeichertes Fahrzeug eines anderen Fahrers wird verworfen', () async {
|
||||||
|
// 414 hat auf diesem Gerät das Auto von 423 als Tagesfahrzeug gespeichert.
|
||||||
|
SharedPreferences.setMockInitialValues(_gespeichert('414', _fremdesAuto));
|
||||||
|
|
||||||
|
final state = await _laden('414', const []);
|
||||||
|
|
||||||
|
expect(state, isA<CarSelectRequired>());
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
expect(prefs.getString('car_selection_test_414_car_id'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eigenes aktives Fahrzeug wird übernommen, mit Serverdaten', () async {
|
||||||
|
SharedPreferences.setMockInitialValues(_gespeichert('423', _fremdesAuto));
|
||||||
|
const auto = Car(id: _fremdesAuto, accountId: 423, plate: 'test', active: true);
|
||||||
|
|
||||||
|
final state = await _laden('423', const [auto]);
|
||||||
|
|
||||||
|
expect(state, isA<CarSelectComplete>());
|
||||||
|
expect((state as CarSelectComplete).selectedCar.accountId, 423);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deaktiviertes Fahrzeug gilt nicht mehr', () async {
|
||||||
|
SharedPreferences.setMockInitialValues(_gespeichert('423', _fremdesAuto));
|
||||||
|
const auto =
|
||||||
|
Car(id: _fremdesAuto, accountId: 423, plate: 'test', active: false);
|
||||||
|
|
||||||
|
expect(await _laden('423', const [auto]), isA<CarSelectRequired>());
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -12,8 +12,10 @@ 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/car_selection/bloc/bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_date_cubit.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/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:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/session_scope.dart';
|
import 'package:hl_lieferservice/widget/session_scope.dart';
|
||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
||||||
|
|
||||||
/// Token-Provider ohne Keycloak: der Test steuert Login/Logout über [emit].
|
/// Token-Provider ohne Keycloak: der Test steuert Login/Logout über [emit].
|
||||||
class _FakeTokenProvider implements KeycloakOidcTokenProvider {
|
class _FakeTokenProvider implements KeycloakOidcTokenProvider {
|
||||||
@ -45,6 +47,14 @@ AuthLoggedIn _login(int personalnummer) =>
|
|||||||
AuthLoggedIn({'personalnummer': personalnummer, 'name': 'Fahrer'});
|
AuthLoggedIn({'personalnummer': personalnummer, 'name': 'Fahrer'});
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// Session-Blocs bauen ihre Repositories gegen den API-Client; im Test
|
||||||
|
// reicht ein unbenutzter Client (es wird nichts geladen).
|
||||||
|
setUpAll(() {
|
||||||
|
if (!locator.isRegistered<HolzleitnerApi>()) {
|
||||||
|
locator.registerSingleton<HolzleitnerApi>(HolzleitnerApi());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
late _FakeTokenProvider provider;
|
late _FakeTokenProvider provider;
|
||||||
late _CountingPaymentMethods payments;
|
late _CountingPaymentMethods payments;
|
||||||
late List<CarSelectBloc> carSelections;
|
late List<CarSelectBloc> carSelections;
|
||||||
|
|||||||
Reference in New Issue
Block a user