runSync(['A', 'B'], [ ContingentSyncResult::failed('upstream down'), ContingentSyncResult::failed('upstream down'), ]); self::assertSame(Command::FAILURE, $tester->getStatusCode()); } public function testASingleFailingHotelDoesNotFailTheTask(): void { $tester = $this->runSync(['A', 'B', 'C'], [ ContingentSyncResult::synced(true, 3, 0, 0, 0), ContingentSyncResult::failed('unknown hotel code'), ContingentSyncResult::synced(false, 0, 0, 0, 0), ]); self::assertSame(Command::SUCCESS, $tester->getStatusCode()); self::assertStringContainsString('B', $tester->getDisplay()); self::assertStringContainsString('unknown hotel code', $tester->getDisplay()); } public function testASuccessfulRunSucceeds(): void { $tester = $this->runSync(['A'], [ContingentSyncResult::synced(true, 3, 0, 1, 0)]); self::assertSame(Command::SUCCESS, $tester->getStatusCode()); } public function testARunWithNoHotelsSucceeds(): void { $tester = $this->runSync([], []); self::assertSame(Command::SUCCESS, $tester->getStatusCode()); } public function testSyncsAccommodationAndTypo3CodesAndDropsTheRest(): void { $manager = $this->createMock(ContingentSnapshotManager::class); $synced = []; $manager->expects(self::exactly(2))->method('sync') ->willReturnCallback(static function (string $code) use (&$synced): ContingentSyncResult { $synced[] = $code; return ContingentSyncResult::synced(false, 0, 0, 0, 0); }); $dayRepository = $this->createMock(ContingentDayRepository::class); $dayRepository->expects(self::once())->method('deleteExceptHotelCodes')->with(['DGS', 'DKI']); $syncStateRepository = $this->createMock(ContingentSyncStateRepository::class); $syncStateRepository->expects(self::once())->method('deleteExceptHotelCodes')->with(['DGS', 'DKI']); $this->runSync( ['DGS' => 'accommodation', 'DKI' => 'typo3'], manager: $manager, dayRepository: $dayRepository, syncStateRepository: $syncStateRepository, ); self::assertSame(['DGS', 'DKI'], $synced); } public function testASingleHotelRunAcceptsATypo3OnlyCodeAndDropsNothing(): void { $manager = $this->createMock(ContingentSnapshotManager::class); $manager->expects(self::once())->method('sync') ->with('DKI', self::anything(), self::anything()) ->willReturn(ContingentSyncResult::synced(false, 0, 0, 0, 0)); $dayRepository = $this->createMock(ContingentDayRepository::class); $dayRepository->expects(self::never())->method('deleteExceptHotelCodes'); $syncStateRepository = $this->createMock(ContingentSyncStateRepository::class); $syncStateRepository->expects(self::never())->method('deleteExceptHotelCodes'); $tester = $this->runSync( ['DGS' => 'accommodation', 'DKI' => 'typo3'], manager: $manager, dayRepository: $dayRepository, syncStateRepository: $syncStateRepository, options: ['--hotel' => 'DKI'], ); self::assertSame(Command::SUCCESS, $tester->getStatusCode()); } public function testASingleHotelRunIgnoresAnUnknownCode(): void { $manager = $this->createMock(ContingentSnapshotManager::class); $manager->expects(self::never())->method('sync'); $tester = $this->runSync(['DGS' => 'accommodation'], manager: $manager, options: ['--hotel' => 'NOPE']); self::assertSame(Command::SUCCESS, $tester->getStatusCode()); self::assertStringContainsString('No accommodation calendarCode or TYPO3 calendar hotel matched', $tester->getDisplay()); } /** * @param array $hotels codes, or code => 'accommodation'|'typo3' * @param ContingentSyncResult[] $results * @param array $options */ private function runSync( array $hotels, array $results = [], ?ContingentSnapshotManager $manager = null, ?ContingentDayRepository $dayRepository = null, ?ContingentSyncStateRepository $syncStateRepository = null, array $options = [], ): CommandTester { if (array_is_list($hotels)) { $hotels = array_fill_keys($hotels, 'accommodation'); } $directoryEntries = []; foreach ($hotels as $code => $source) { $code = (string) $code; $directoryEntries[$code] = 'typo3' === $source ? new ContingentHotel($code, null, (new CalendarHotel())->setCode($code)->setName($code)) : new ContingentHotel($code, (new Accommodation())->setCalendarCode($code), null); } $hotelDirectory = $this->createStub(ContingentHotelDirectory::class); $hotelDirectory->method('all')->willReturn($directoryEntries); if (null === $manager) { $manager = $this->createStub(ContingentSnapshotManager::class); if ([] !== $results) { $manager->method('sync')->willReturnOnConsecutiveCalls(...$results); } } $command = new BpnSyncContingentsCommand( $hotelDirectory, $syncStateRepository ?? $this->createStub(ContingentSyncStateRepository::class), $dayRepository ?? $this->createStub(ContingentDayRepository::class), $manager, new NullLogger(), ); $application = new Application(); $application->addCommand($command); $tester = new CommandTester($application->find('app:bpn:sync-contingents')); $tester->execute(['--horizon-months' => '1'] + $options); // LockableTrait's lock lives for the life of the process, so without releasing it every // run after the first would exit early and these assertions would pass for the wrong reason. $release = new \ReflectionMethod($command, 'release'); $release->invoke($command); return $tester; } }