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, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame(BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED, $result->status); $this->assertFalse($result->immutableChangesReverted); } /** * @dataProvider updateFailureProvider */ public function testHandleSubmissionReturnsResultWhenUpdateThrows( \Throwable $exception, string $expectedStatus, ): 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(ApiClient::class); $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willThrowException($exception); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame($expectedStatus, $result->status); $this->assertNull($result->message); $this->assertFalse($result->immutableChangesReverted); } public function testHandleSubmissionReturnsResultWhenUpdateIsUnsuccessful(): 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(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, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame(BookingEditSubmissionResult::STATUS_UNSUCCESSFUL, $result->status); $this->assertSame('BPN-FAIL', $result->message); $this->assertFalse($result->immutableChangesReverted); } public function testHandleSubmissionStoresInfoForNonErrorNotification(): void { $this->assertNotificationResult(new Notification(650, 'Alles gut'), 'info'); } public function testHandleSubmissionStoresErrorForErrorNotification(): void { $this->assertNotificationResult(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(ApiClient::class); $bookingUpdate = new BookingUpdate(); $bookingUpdate->success = true; $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($bookingUpdate); $bookingSessionService = $this->createMock(BookingSessionManager::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, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status); $this->assertFalse($result->immutableChangesReverted); $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(ApiClient::class); $bookingUpdate = new BookingUpdate(); $bookingUpdate->success = true; $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($bookingUpdate); $bookingSessionService = $this->createMock(BookingSessionManager::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, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status); $this->assertTrue($result->immutableChangesReverted); } public function testHandleSubmissionReturnsResultOnTimeout(): void { $this->assertExceptionResult(new TimeoutException('slow'), BookingEditSubmissionResult::STATUS_TIMEOUT); } public function testHandleSubmissionReturnsResultOnApiClientException(): void { $this->assertExceptionResult(new ApiClientException('boom'), BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR); } public static function updateFailureProvider(): array { return [ 'timeout' => [new TimeoutException('slow'), BookingEditSubmissionResult::STATUS_TIMEOUT], 'api-client' => [new ApiClientException('boom'), BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR], ]; } private function assertNotificationResult(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(ApiClient::class); $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willReturn($notification); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame($this->resolveExpectedStatus($expectedType), $result->status); $this->assertSame($notification->message, $result->message); $this->assertFalse($result->immutableChangesReverted); } private function assertExceptionResult(\Throwable $exception, string $expectedStatus): 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(ApiClient::class); $apiClient->expects($this->once()) ->method('updateBooking') ->with($bookingDto, true) ->willThrowException($exception); $service = $this->createService( apiClient: $apiClient, dataLoader: $dataLoader, travelDataService: $travelDataService, submitGuard: $submitGuard, ); $result = $service->handleSubmission($request, $bookingDto, 42, $user); $this->assertSame($expectedStatus, $result->status); $this->assertNull($result->message); $this->assertFalse($result->immutableChangesReverted); } private function createService( ?ApiClient $apiClient = null, ?BookingEditDataLoader $dataLoader = null, ?BookingEditDraftManager $draftService = null, ?TravelDataProvider $travelDataService = null, ?BookingEditSubmitGuard $submitGuard = null, ?BookingSessionManager $bookingSessionService = null, ): BookingEditSubmitter { return new BookingEditSubmitter( $apiClient ?? $this->createMock(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(BookingSessionManager::class), $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 resolveExpectedStatus(string $expectedType): string { return match ($expectedType) { 'error' => BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR, 'info' => BookingEditSubmissionResult::STATUS_NOTIFICATION_INFO, default => $expectedType, }; } }