createMock(AccommodationBookingRepository::class); $repository->method('find')->with(42)->willReturn($this->booking()); $client = $this->createMock(ApiClient::class); $client->method('isConfigured')->willReturn(true); $client->expects(self::once()) ->method('createTaskFromTemplate') ->with('WIEN01 2026-08-20 Anna Müller') ->willReturn('abc123'); $client->expects(self::once()) ->method('updateTaskStatus') ->with('abc123', CreateClickUpBookingTaskHandler::TASK_STATUS); $this->handler($repository, $client)(new CreateClickUpBookingTaskMessage(42)); } public function testAnUnconfiguredClientIsSkippedWithoutTouchingTheDatabase(): void { $repository = $this->createMock(AccommodationBookingRepository::class); $repository->expects(self::never())->method('find'); $client = $this->createMock(ApiClient::class); $client->method('isConfigured')->willReturn(false); $client->expects(self::never())->method('createTaskFromTemplate'); $this->handler($repository, $client)(new CreateClickUpBookingTaskMessage(42)); } public function testAVanishedBookingIsSkipped(): void { $repository = $this->createStub(AccommodationBookingRepository::class); $repository->method('find')->willReturn(null); $client = $this->createMock(ApiClient::class); $client->method('isConfigured')->willReturn(true); $client->expects(self::never())->method('createTaskFromTemplate'); $this->handler($repository, $client)(new CreateClickUpBookingTaskMessage(42)); } #[DataProvider('incompleteBookings')] public function testABookingWithoutTheDataForATaskNameIsSkipped(callable $mutate): void { $booking = $this->booking(); $mutate($booking); $repository = $this->createStub(AccommodationBookingRepository::class); $repository->method('find')->willReturn($booking); $client = $this->createMock(ApiClient::class); $client->method('isConfigured')->willReturn(true); $client->expects(self::never())->method('createTaskFromTemplate'); $this->handler($repository, $client)(new CreateClickUpBookingTaskMessage(42)); } /** * @return iterable */ public static function incompleteBookings(): iterable { yield 'without an accommodation' => [static fn (AccommodationBooking $b) => $b->setAccommodation(null)]; yield 'without a contact name' => [static function (AccommodationBooking $b): void { $b->setFirstName(null); $b->setLastName(null); }]; yield 'with a blank contact name' => [static function (AccommodationBooking $b): void { $b->setFirstName(' '); $b->setLastName(' '); }]; } public function testAFailingStatusUpdateDoesNotRetryTheTaskCreation(): void { $repository = $this->createStub(AccommodationBookingRepository::class); $repository->method('find')->willReturn($this->booking()); $client = $this->createStub(ApiClient::class); $client->method('isConfigured')->willReturn(true); $client->method('createTaskFromTemplate')->willReturn('abc123'); $client->method('updateTaskStatus')->willThrowException(new ClickUpException('boom')); $this->handler($repository, $client)(new CreateClickUpBookingTaskMessage(42)); // Reaching this point is the assertion: the exception must not bubble up into the // transport, because a retry would create a second ClickUp task. self::assertTrue(true); } private function handler( AccommodationBookingRepository $repository, ApiClient $client, ): CreateClickUpBookingTaskHandler { return new CreateClickUpBookingTaskHandler($repository, $client, new NullLogger()); } private function booking(): AccommodationBooking { $accommodation = new Accommodation(); $accommodation->setCalendarCode('WIEN01'); $booking = new AccommodationBooking(); $booking->setAccommodation($accommodation); $booking->setDateFrom(new \DateTimeImmutable('2026-08-20')); $booking->setFirstName('Anna'); $booking->setLastName('Müller'); return $booking; } }