From 0ea43d0c6b05a0f8ff43d05bec1b8758d1e233e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 24 Jun 2026 19:02:08 +0200 Subject: [PATCH] fix: correct edit reload dirty-state --- .../Booking/Edit/IndexController.php | 12 +- .../Booking/Edit/IndexControllerTest.php | 224 +++++++++++++++++- 2 files changed, 231 insertions(+), 5 deletions(-) diff --git a/src/Controller/Booking/Edit/IndexController.php b/src/Controller/Booking/Edit/IndexController.php index be0e109..27e9299 100644 --- a/src/Controller/Booking/Edit/IndexController.php +++ b/src/Controller/Booking/Edit/IndexController.php @@ -94,7 +94,13 @@ class IndexController extends AbstractController return $this->redirectToRoute('app_bookings'); } + $wasDirtyBeforePreselection = $this->fingerprintService->isDirty($bookingDto); + $this->bookingConfigurator->preselectDefaultServices($bookingDto); + if (false === $wasDirtyBeforePreselection) { + $bookingDto->originalFingerprint = $this->fingerprintService->generateFingerprint($bookingDto); + } + $this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT); // Show flash message if draft was restored @@ -142,10 +148,13 @@ class IndexController extends AbstractController return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]); } + $isDirty = $wasDirtyBeforePreselection + || (true === $isOverviewSubmitted && $this->fingerprintService->isDirty($bookingDto)); + $bookingEditContext = $this->editContextFactory->createOverviewContext( $bookingDto, $bookingData, - $this->fingerprintService->isDirty($bookingDto), + $isDirty, $isOverviewSubmitted, $hasBlockingValidationErrors, $missingValueLabelsByParticipantIndex, @@ -215,6 +224,7 @@ class IndexController extends AbstractController /** @var User $user */ $user = $this->getUser(); $this->draftService->deleteDraft($user, $bookingId); + $this->dataLoader->invalidateBookingCache($bookingId, $user); $this->addFlash('success', 'Änderungen verworfen, Daten neu geladen'); diff --git a/tests/Controller/Booking/Edit/IndexControllerTest.php b/tests/Controller/Booking/Edit/IndexControllerTest.php index 8cc02ff..a7efbd8 100644 --- a/tests/Controller/Booking/Edit/IndexControllerTest.php +++ b/tests/Controller/Booking/Edit/IndexControllerTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Tests\Controller\Booking\Edit; use App\BusProNet\Model\Booking; +use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; use App\BusProNet\XmlLoader\AgencyLoader; use App\Controller\Booking\Edit\IndexController; @@ -68,6 +69,8 @@ class IndexControllerTest extends TestCase ->method('isDirty') ->with($bookingDto) ->willReturn(true); + $fingerprintService->expects($this->never()) + ->method('generateFingerprint'); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) @@ -151,6 +154,10 @@ class IndexControllerTest extends TestCase ->method('isDirty') ->with($bookingDto) ->willReturn(false); + $fingerprintService->expects($this->once()) + ->method('generateFingerprint') + ->with($bookingDto) + ->willReturn('normalized-fingerprint'); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) @@ -225,8 +232,12 @@ class IndexControllerTest extends TestCase ->willReturn(false); $fingerprintService = $this->createMock(BookingChangeTracker::class); + $fingerprintService->expects($this->once()) + ->method('isDirty') + ->with($bookingDto) + ->willReturn(true); $fingerprintService->expects($this->never()) - ->method('isDirty'); + ->method('generateFingerprint'); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) @@ -310,10 +321,14 @@ class IndexControllerTest extends TestCase ->willReturn(false); $fingerprintService = $this->createMock(BookingChangeTracker::class); - $fingerprintService->expects($this->once()) + $fingerprintService->expects($this->exactly(2)) ->method('isDirty') ->with($bookingDto) - ->willReturn(false); + ->willReturnOnConsecutiveCalls(false, false); + $fingerprintService->expects($this->once()) + ->method('generateFingerprint') + ->with($bookingDto) + ->willReturn('normalized-fingerprint'); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) @@ -365,6 +380,184 @@ class IndexControllerTest extends TestCase $this->assertSame([0 => ['E-Mail']], $controller->renderParameters['bookingEditContext']->missingValueLabelsByParticipantIndex); } + public function testIndexTreatsDefaultPreselectionOnCleanDtoAsBaselineState(): void + { + $request = Request::create('/bookings/42/edit', 'GET'); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $bookingDto->originalFingerprint = 'api-fingerprint'; + $bookingDto->participants = [$this->createParticipant()]; + $bookingData = $this->createBooking(); + + $form = $this->createMock(FormInterface::class); + $form->method('isSubmitted')->willReturn(false); + + $fingerprintService = $this->createMock(BookingChangeTracker::class); + $fingerprintService->expects($this->once()) + ->method('isDirty') + ->with($bookingDto) + ->willReturn(false); + $fingerprintService->expects($this->once()) + ->method('generateFingerprint') + ->with($bookingDto) + ->willReturn('normalized-fingerprint'); + + $dataLoader = $this->createMock(BookingEditDataLoader::class); + $dataLoader->expects($this->once()) + ->method('loadFormData') + ->with($request, 42, $user) + ->willReturn($bookingDto); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($bookingData); + $dataLoader->expects($this->once()) + ->method('isDraftRestored') + ->willReturn(false); + + $preselectedService = $this->createService(1001); + $bookingConfigurator = $this->createMock(BookingConfigurator::class); + $bookingConfigurator->expects($this->once()) + ->method('preselectDefaultServices') + ->with($bookingDto) + ->willReturnCallback(static function (BookingDto $dto) use ($preselectedService): void { + $dto->participants[0]->additionalServices[] = $preselectedService; + }); + + $contextFactory = $this->createMock(BookingEditContextFactory::class); + $contextFactory->expects($this->once()) + ->method('createOverviewContext') + ->with($bookingDto, $bookingData, false, false, false, []) + ->willReturn($this->createOverviewContext($bookingDto, $bookingData, false, false, false)); + + $controller = new TestableIndexController( + $dataLoader, + $this->createMock(BookingEditDraftManager::class), + $this->createMock(BookingSessionManager::class), + $fingerprintService, + $bookingConfigurator, + $contextFactory, + $this->createPreflightChecker($bookingDto), + $this->createMock(BookingEditSubmitter::class), + $user, + $form, + ); + + $response = $controller->index(42, $request); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame('normalized-fingerprint', $bookingDto->originalFingerprint); + $this->assertFalse($controller->renderParameters['bookingEditContext']->isDirty); + } + + public function testIndexKeepsDirtyStateWhenDtoWasDirtyBeforeDefaultPreselection(): void + { + $request = Request::create('/bookings/42/edit', 'GET'); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $bookingDto->originalFingerprint = 'api-fingerprint'; + $bookingDto->participants = [$this->createParticipant()]; + $bookingData = $this->createBooking(); + + $form = $this->createMock(FormInterface::class); + $form->method('isSubmitted')->willReturn(false); + + $fingerprintService = $this->createMock(BookingChangeTracker::class); + $fingerprintService->expects($this->once()) + ->method('isDirty') + ->with($bookingDto) + ->willReturn(true); + $fingerprintService->expects($this->never()) + ->method('generateFingerprint'); + + $dataLoader = $this->createMock(BookingEditDataLoader::class); + $dataLoader->expects($this->once()) + ->method('loadFormData') + ->with($request, 42, $user) + ->willReturn($bookingDto); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($bookingData); + $dataLoader->expects($this->once()) + ->method('isDraftRestored') + ->willReturn(false); + + $preselectedService = $this->createService(1001); + $bookingConfigurator = $this->createMock(BookingConfigurator::class); + $bookingConfigurator->expects($this->once()) + ->method('preselectDefaultServices') + ->with($bookingDto) + ->willReturnCallback(static function (BookingDto $dto) use ($preselectedService): void { + $dto->participants[0]->additionalServices[] = $preselectedService; + }); + + $contextFactory = $this->createMock(BookingEditContextFactory::class); + $contextFactory->expects($this->once()) + ->method('createOverviewContext') + ->with($bookingDto, $bookingData, true, false, false, []) + ->willReturn($this->createOverviewContext($bookingDto, $bookingData, true, false, false)); + + $controller = new TestableIndexController( + $dataLoader, + $this->createMock(BookingEditDraftManager::class), + $this->createMock(BookingSessionManager::class), + $fingerprintService, + $bookingConfigurator, + $contextFactory, + $this->createPreflightChecker($bookingDto), + $this->createMock(BookingEditSubmitter::class), + $user, + $form, + ); + + $response = $controller->index(42, $request); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame('api-fingerprint', $bookingDto->originalFingerprint); + $this->assertTrue($controller->renderParameters['bookingEditContext']->isDirty); + } + + public function testReloadFromApiClearsSessionDeletesDraftInvalidatesCacheAndRedirectsToEdit(): void + { + $request = Request::create('/bookings/42/edit/reload', 'POST'); + $user = $this->createUser(); + + $bookingSessionService = $this->createMock(BookingSessionManager::class); + $bookingSessionService->expects($this->once()) + ->method('clearBookingDto') + ->with($request, BookingDto::MODE_EDIT); + + $draftService = $this->createMock(BookingEditDraftManager::class); + $draftService->expects($this->once()) + ->method('deleteDraft') + ->with($user, 42); + + $dataLoader = $this->createMock(BookingEditDataLoader::class); + $dataLoader->expects($this->once()) + ->method('invalidateBookingCache') + ->with(42, $user); + + $controller = new TestableIndexController( + $dataLoader, + $draftService, + $bookingSessionService, + $this->createMock(BookingChangeTracker::class), + $this->createMock(BookingConfigurator::class), + $this->createMock(BookingEditContextFactory::class), + $this->createMock(BookingEditPreFlightChecker::class), + $this->createMock(BookingEditSubmitter::class), + $user, + $this->createMock(FormInterface::class), + ); + + $response = $controller->reloadFromApi(42, $request); + + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); + $this->assertSame([['success', 'Änderungen verworfen, Daten neu geladen']], $controller->flashes); + } + /** * @dataProvider submissionResultProvider * @@ -448,6 +641,25 @@ class IndexControllerTest extends TestCase return new \App\Form\Model\ParticipantDto(); } + private function createService(int $id): Service + { + $service = new Service(); + $service->id = $id; + + return $service; + } + + private function createPreflightChecker(BookingDto $bookingDto): BookingEditPreFlightChecker + { + $preflightChecker = $this->createMock(BookingEditPreFlightChecker::class); + $preflightChecker->expects($this->once()) + ->method('findMissingValueLabelsByParticipantIndex') + ->with($bookingDto) + ->willReturn([]); + + return $preflightChecker; + } + private function createOverviewContext( BookingDto $bookingDto, Booking $bookingData, @@ -499,8 +711,12 @@ class IndexControllerTest extends TestCase $bookingData = $this->createBooking(); $form = $this->createMock(FormInterface::class); $fingerprintService = $this->createMock(BookingChangeTracker::class); + $fingerprintService->expects($this->once()) + ->method('isDirty') + ->with($bookingDto) + ->willReturn(true); $fingerprintService->expects($this->never()) - ->method('isDirty'); + ->method('generateFingerprint'); $form->expects($this->once()) ->method('handleRequest')