createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn(null); $service = $this->createService( dataLoader: $dataLoader, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame( ['Buchungsdaten konnten vor dem Speichern nicht neu geladen werden'], $request->getSession()->getFlashBag()->get('error') ); } /** * @dataProvider updateFailureProvider */ public function testHandleSubmissionReturnsRedirectWhenUpdateThrows( \Throwable $exception, string $expectedFlashType, string $expectedFlashMessage, ): void { $request = $this->createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $freshBookingData = $this->createFreshBooking(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->exactly(1)) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn($freshBookingData); $travelDataService = $this->createMock(TravelDataProvider::class); $travelDataService->expects($this->once()) ->method('getMutabilityData') ->with(1234, true, true) ->willReturn(null); $travelDataService->expects($this->never()) ->method('patchMutability'); $submitGuard = $this->createMock(BookingEditSubmitGuard::class); $submitGuard->expects($this->once()) ->method('reconcileImmutableCategories') ->with($bookingDto, $freshBookingData) ->willReturn(false); $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willThrowException($exception); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame([$expectedFlashMessage], $request->getSession()->getFlashBag()->get($expectedFlashType)); } public function testHandleSubmissionReturnsRedirectWhenUpdateIsUnsuccessful(): void { $request = $this->createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $freshBookingData = $this->createFreshBooking(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn($freshBookingData); $travelDataService = $this->createMock(TravelDataProvider::class); $travelDataService->expects($this->once()) ->method('getMutabilityData') ->with(1234, true, true) ->willReturn(null); $travelDataService->expects($this->never()) ->method('patchMutability'); $submitGuard = $this->createMock(BookingEditSubmitGuard::class); $submitGuard->expects($this->once()) ->method('reconcileImmutableCategories') ->with($bookingDto, $freshBookingData) ->willReturn(false); $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); $bookingUpdate = new BookingUpdate(); $bookingUpdate->success = false; $bookingUpdate->status = 'BPN-FAIL'; $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($bookingUpdate); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame(['BPN-FAIL'], $request->getSession()->getFlashBag()->get('error')); } public function testHandleSubmissionStoresInfoForNonErrorNotification(): void { $this->assertNotificationFlash(new Notification(650, 'Alles gut'), 'info'); } public function testHandleSubmissionStoresErrorForErrorNotification(): void { $this->assertNotificationFlash(new Notification(500, 'Kaputt'), 'error'); } public function testHandleSubmissionClearsSessionAndDraftOnSuccessfulUpdate(): void { $request = $this->createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $freshBookingData = $this->createFreshBooking(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->exactly(2)) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn($freshBookingData); $travelDataService = $this->createMock(TravelDataProvider::class); $travelDataService->expects($this->once()) ->method('getMutabilityData') ->with(1234, true, true) ->willReturn(null); $travelDataService->expects($this->never()) ->method('patchMutability'); $submitGuard = $this->createMock(BookingEditSubmitGuard::class); $submitGuard->expects($this->once()) ->method('reconcileImmutableCategories') ->with($bookingDto, $freshBookingData) ->willReturn(false); $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); $bookingUpdate = new BookingUpdate(); $bookingUpdate->success = true; $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($bookingUpdate); $bookingSessionService = $this->createMock(BookingSessionStore::class); $bookingSessionService->expects($this->once()) ->method('clearBookingDto') ->with($request, BookingDto::MODE_EDIT); $bookingSessionService->expects($this->never()) ->method('saveBookingDto'); $draftService = $this->createMock(BookingEditDraftManager::class); $draftService->expects($this->once()) ->method('deleteDraft') ->with($user, 42); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, draftService: $draftService, travelDataService: $travelDataService, submitGuard: $submitGuard, bookingSessionService: $bookingSessionService, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame(['Buchung erfolgreich aktualisiert'], $request->getSession()->getFlashBag()->get('success')); $this->assertSame($freshBookingData, $bookingDto->booking); } public function testHandleSubmissionPersistsSessionWhenImmutableFieldsWereReverted(): void { $request = $this->createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $freshBookingData = $this->createFreshBooking(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->exactly(2)) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn($freshBookingData); $travelDataService = $this->createMock(TravelDataProvider::class); $travelDataService->expects($this->once()) ->method('getMutabilityData') ->with(1234, true, true) ->willReturn(null); $travelDataService->expects($this->never()) ->method('patchMutability'); $submitGuard = $this->createMock(BookingEditSubmitGuard::class); $submitGuard->expects($this->once()) ->method('reconcileImmutableCategories') ->with($bookingDto, $freshBookingData) ->willReturn(true); $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); $bookingUpdate = new BookingUpdate(); $bookingUpdate->success = true; $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($bookingUpdate); $bookingSessionService = $this->createMock(BookingSessionStore::class); $bookingSessionService->expects($this->once()) ->method('saveBookingDto') ->with($request, $bookingDto, BookingDto::MODE_EDIT); $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); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, draftService: $draftService, travelDataService: $travelDataService, submitGuard: $submitGuard, bookingSessionService: $bookingSessionService, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame(['Einige Änderungen wurden verworfen, da sie aktuell nicht mehr änderbar sind.'], $request->getSession()->getFlashBag()->get('info')); $this->assertSame(['Buchung erfolgreich aktualisiert'], $request->getSession()->getFlashBag()->get('success')); } public function testHandleSubmissionReturnsRedirectOnTimeout(): void { $this->assertExceptionFlash(new TimeoutException('slow'), 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.'); } public function testHandleSubmissionReturnsRedirectOnApiClientException(): void { $this->assertExceptionFlash(new ApiClientException('boom'), 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'); } public static function updateFailureProvider(): array { return [ 'timeout' => [new TimeoutException('slow'), 'error', 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.'], 'api-client' => [new ApiClientException('boom'), 'error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'], ]; } private function assertNotificationFlash(Notification $notification, string $expectedType): void { $request = $this->createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $freshBookingData = $this->createFreshBooking(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn($freshBookingData); $travelDataService = $this->createMock(TravelDataProvider::class); $travelDataService->expects($this->once()) ->method('getMutabilityData') ->with(1234, true, true) ->willReturn(null); $travelDataService->expects($this->never()) ->method('patchMutability'); $submitGuard = $this->createMock(BookingEditSubmitGuard::class); $submitGuard->expects($this->once()) ->method('reconcileImmutableCategories') ->with($bookingDto, $freshBookingData) ->willReturn(false); $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($notification); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame([$notification->message], $request->getSession()->getFlashBag()->get($expectedType)); } private function assertExceptionFlash(\Throwable $exception, string $expectedMessage): void { $request = $this->createRequestWithSession(); $user = $this->createUser(); $bookingDto = $this->createBookingDto(); $freshBookingData = $this->createFreshBooking(); $dataLoader = $this->createMock(BookingEditDataLoader::class); $dataLoader->expects($this->once()) ->method('invalidateBookingCache') ->with(42, $user); $dataLoader->expects($this->once()) ->method('fetchBookingData') ->with(42, $user) ->willReturn($freshBookingData); $travelDataService = $this->createMock(TravelDataProvider::class); $travelDataService->expects($this->once()) ->method('getMutabilityData') ->with(1234, true, true) ->willReturn(null); $travelDataService->expects($this->never()) ->method('patchMutability'); $submitGuard = $this->createMock(BookingEditSubmitGuard::class); $submitGuard->expects($this->once()) ->method('reconcileImmutableCategories') ->with($bookingDto, $freshBookingData) ->willReturn(false); $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willThrowException($exception); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $response = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); $this->assertSame([$expectedMessage], $request->getSession()->getFlashBag()->get('error')); } private function createService( ?\App\BusProNet\ApiClient $apiClient = null, ?BookingEditDataLoader $dataLoader = null, ?BookingEditDraftManager $draftService = null, ?TravelDataProvider $travelDataService = null, ?BookingEditSubmitGuard $submitGuard = null, ?BookingSessionStore $bookingSessionService = null, ): BookingEditSubmitter { return new BookingEditSubmitter( $apiClient ?? $this->createMock(\App\BusProNet\ApiClient::class), $dataLoader ?? $this->createMock(BookingEditDataLoader::class), $draftService ?? $this->createMock(BookingEditDraftManager::class), $travelDataService ?? $this->createMock(TravelDataProvider::class), $submitGuard ?? $this->createMock(BookingEditSubmitGuard::class), $bookingSessionService ?? $this->createMock(BookingSessionStore::class), $this->createUrlGenerator(), $this->createMock(LoggerInterface::class), ); } private function createBookingDto(): BookingDto { return new BookingDto(new Travel(), 1); } private function createFreshBooking(): Booking { $booking = new Booking(); $booking->dateId = 1234; return $booking; } private function createUser(): User { $user = new User('user@example.com'); $user->setPassword('secret'); return $user; } private function createRequestWithSession(): Request { $request = Request::create('/bookings/42/edit', Request::METHOD_POST); $request->setSession(new Session(new MockArraySessionStorage())); return $request; } private function createUrlGenerator(): UrlGeneratorInterface { $urlGenerator = $this->createMock(UrlGeneratorInterface::class); $urlGenerator->method('generate') ->with('app_booking_edit', ['id' => 42]) ->willReturn('/bookings/42/edit'); return $urlGenerator; } }