itemRepository = $this->createMock(IItemRepository::class); $this->orderService = $this->createMock(IOrderService::class); $this->timeProvider = $this->createMock(ITimeProvider::class); $this->expirationSpec = $this->createMock(ItemExpirationSpec::class); $this->logger = $this->createMock(LoggerInterface::class); $this->fixedCurrentTime = new DateTimeImmutable(self::MOCKED_NOW); $this->timeProvider->method('now')->willReturn($this->fixedCurrentTime); $this->handleExpiredItems = new HandleExpiredItems( $this->itemRepository, $this->orderService, $this->timeProvider, $this->expirationSpec, $this->logger ); } private function createExpiredItem1(): Item { return new Item( self::ITEM_ID_1, self::ITEM_NAME_1, new DateTimeImmutable(self::EXPIRED_DATE), self::ORDER_URL_1, self::USER_ID_1 ); } private function createExpiredItem2(): Item { return new Item( self::ITEM_ID_2, self::ITEM_NAME_2, new DateTimeImmutable(self::EXPIRED_DATE), self::ORDER_URL_2, self::USER_ID_2 ); } public function testWhenNoExpiredItemsExistThenNoOrdersPlaced(): void { // Given $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn(new Specification([])); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn([]); $this->orderService->expects($this->never())->method('orderItem'); $this->itemRepository->expects($this->never())->method('delete'); $this->logger->expects($this->never())->method('error'); // When & Then $this->handleExpiredItems->execute(); } public function testWhenExpiredItemsExistThenOrdersPlacedAndItemsDeleted(): void { // Given $expiredItem1 = $this->createExpiredItem1(); $expiredItem2 = $this->createExpiredItem2(); $expiredItems = [$expiredItem1, $expiredItem2]; $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn($expiredItems); $this->orderService->expects($this->exactly(2)) ->method('orderItem'); $this->itemRepository->expects($this->exactly(2)) ->method('delete'); $this->logger->expects($this->never())->method('error'); // When & Then $this->handleExpiredItems->execute(); } public function testWhenOrderServiceFailsForOneItemThenErrorLoggedAndOtherItemProcessed(): void { // Given $expiredItem1 = $this->createExpiredItem1(); $expiredItem2 = $this->createExpiredItem2(); $expiredItems = [$expiredItem1, $expiredItem2]; $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn($expiredItems); $callCount = 0; $this->orderService->expects($this->exactly(2)) ->method('orderItem') ->willReturnCallback(function () use (&$callCount) { $callCount++; if ($callCount === 1) { throw new \RuntimeException('Order service failed'); } return null; }); $this->itemRepository->expects($this->once()) ->method('delete') ->with(self::ITEM_ID_2); $this->logger->expects($this->once()) ->method('error') ->with('Failed to place order for expired item ' . self::ITEM_ID_1 . ': Order service failed'); // When & Then $this->handleExpiredItems->execute(); } public function testWhenRepositoryFindThrowsDomainExceptionThenApplicationExceptionThrown(): void { // Given $domainException = new DomainException('Repository error'); $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willThrowException($domainException); $this->orderService->expects($this->never())->method('orderItem'); $this->itemRepository->expects($this->never())->method('delete'); // When & Then $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Failed to handle expired items: Repository error'); $this->handleExpiredItems->execute(); } public function testWhenRepositoryDeleteThrowsExceptionThenErrorLogged(): void { // Given $expiredItem = $this->createExpiredItem1(); $expiredItems = [$expiredItem]; $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn($expiredItems); $this->orderService->expects($this->once()) ->method('orderItem') ->with($expiredItem); $this->itemRepository->expects($this->once()) ->method('delete') ->with(self::ITEM_ID_1) ->willThrowException(new \RuntimeException('Delete failed')); $this->logger->expects($this->once()) ->method('error') ->with($this->stringContains('Failed to place order for expired item')); // When & Then $this->handleExpiredItems->execute(); } public function testWhenTimeProviderThrowsDomainExceptionThenApplicationExceptionThrown(): void { // Given $domainException = new DomainException('Time provider error'); $this->timeProvider->method('now') ->willThrowException($domainException); $this->expirationSpec->expects($this->never())->method('getSpec'); $this->itemRepository->expects($this->never())->method('findWhere'); $this->orderService->expects($this->never())->method('orderItem'); $this->itemRepository->expects($this->never())->method('delete'); // When & Then $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Failed to handle expired items: Time provider error'); $this->handleExpiredItems->execute(); } public function testWhenNoItemsFoundThenNoProcessingOccurs(): void { // Given $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn([]); $this->orderService->expects($this->never())->method('orderItem'); $this->itemRepository->expects($this->never())->method('delete'); $this->logger->expects($this->never())->method('error'); // When & Then $this->handleExpiredItems->execute(); } public function testWhenExpiredItemsFoundThenTheyAreProcessed(): void { // Given $expiredItem1 = $this->createExpiredItem1(); $expiredItem2 = $this->createExpiredItem2(); $expiredItems = [$expiredItem1, $expiredItem2]; $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn($expiredItems); $this->orderService->expects($this->exactly(2)) ->method('orderItem'); $this->itemRepository->expects($this->exactly(2)) ->method('delete'); $this->logger->expects($this->never())->method('error'); // When & Then $this->handleExpiredItems->execute(); } public function testWhenAllOrderServicesFailThenAllErrorsLogged(): void { // Given $expiredItem1 = $this->createExpiredItem1(); $expiredItem2 = $this->createExpiredItem2(); $expiredItems = [$expiredItem1, $expiredItem2]; $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn($expiredItems); $this->orderService->expects($this->exactly(2)) ->method('orderItem') ->willThrowException(new \RuntimeException('Order service failed')); $this->itemRepository->expects($this->never())->method('delete'); $this->logger->expects($this->exactly(2)) ->method('error'); // When & Then $this->handleExpiredItems->execute(); } public function testWhenRepositoryFindReturnsEmptyArrayThenNoProcessingOccurs(): void { // Given $spec = new Specification([ 'expirationDate', '<=', '2023-01-01 12:00:00' ]); $this->expirationSpec->expects($this->once()) ->method('getSpec') ->willReturn($spec); $this->itemRepository->expects($this->once()) ->method('findWhere') ->willReturn([]); $this->orderService->expects($this->never())->method('orderItem'); $this->itemRepository->expects($this->never())->method('delete'); $this->logger->expects($this->never())->method('error'); // When & Then $this->handleExpiredItems->execute(); } }