itemRepository = $this->createMock(IItemRepository::class); $this->orderService = $this->createMock(IOrderService::class); $this->timeProvider = $this->createMock(ITimeProvider::class); $this->expirationPolicy = $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->addItem = new AddItem( $this->itemRepository, $this->orderService, $this->timeProvider, $this->expirationPolicy, $this->logger ); } private function createTestItem(): array { return [ 'name' => self::ITEM_NAME, 'expirationDate' => new DateTimeImmutable(self::NOT_EXPIRED_DATE), // 1 day in the future 'orderUrl' => self::ORDER_URL, 'userId' => self::USER_ID ]; } private function createExpiredTestItem(): array { return [ 'name' => self::ITEM_NAME, 'expirationDate' => new DateTimeImmutable(self::EXPIRED_DATE), // 1 day in the past 'orderUrl' => self::ORDER_URL, 'userId' => self::USER_ID ]; } private function createItemWithExpiration(string $expiration): array { return [ 'name' => self::ITEM_NAME, 'expirationDate' => new DateTimeImmutable($expiration), 'orderUrl' => self::ORDER_URL, 'userId' => self::USER_ID ]; } private function getItemMatcher(array $expectedItem): callable { return function (Item $item) use ($expectedItem) { return $item->getName() === $expectedItem['name'] && $item->getOrderUrl() === $expectedItem['orderUrl'] && $item->getUserId() === $expectedItem['userId']; }; } public function testWhenItemNotExpiredThenItemSaved(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $this->itemRepository->expects($this->once()) ->method('save') ->with($this->callback($this->getItemMatcher($testItem))); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenItemNotExpiredThenOrderIsNotPlaced(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $this->orderService->expects($this->never())->method('orderItem'); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenItemNotExpiredThenNewItemIdIsReturned(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $this->orderService->expects($this->never())->method('orderItem'); // When $resultId = $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); // Then $this->assertNotNull($resultId); $this->assertNotEmpty($resultId); } public function testWhenItemIsExpiredThenOrderPlaced(): void { // Given $testItem = $this->createExpiredTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(true); $orderedItem = null; $this->itemRepository->expects($this->never())->method('save'); $this->orderService->expects($this->once()) ->method('orderItem') ->with($this->callback(function (Item $item) use (&$orderedItem) { $orderedItem = $item; return true; })); // When $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); // Then $this->assertNotNull($orderedItem); $this->assertEquals($testItem['name'], $orderedItem?->getName()); } public function testWhenItemNameIsEmptyThenExceptionThrown(): void { // Given $testItem = $this->createTestItem(); $testItem['name'] = ''; $this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenOrderUrlIsEmptyThenExceptionThrown(): void { // Given $testItem = $this->createTestItem(); $testItem['orderUrl'] = ''; $this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenUserIdIsEmptyThenExceptionThrown(): void { // Given $testItem = $this->createTestItem(); $testItem['userId'] = ''; $this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenOrderServiceFailsThenErrorLogged(): void { $userId = self::USER_ID; $itemName = self::ITEM_NAME; $expirationDate = new DateTimeImmutable(self::EXPIRED_DATE); $orderUrl = self::ORDER_URL; // Given: Item is expired $this->expirationPolicy->method('isExpired')->willReturn(true); // Mock the repository to return a saved item $this->itemRepository->expects($this->never())->method('save'); // Mock the order service to throw an exception $this->orderService->expects($this->once()) ->method('orderItem') ->willThrowException(new \RuntimeException('Order service failed')); $this->logger->expects($this->once())->method('error'); // The handler should not throw an exception when the order service fails // It should log the error and continue $this->addItem->execute($itemName, $expirationDate->format(self::DATE_FORMAT), $orderUrl, $userId); } public function testWhenItemIsExpiredThenOrderIsPlaced(): void { // Given $testItem = $this->createExpiredTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(true); $this->orderService->expects($this->once())->method('orderItem'); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenItemIsExpiredThenItemIsNotSaved(): void { // Given $testItem = $this->createExpiredTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(true); $this->itemRepository->expects($this->never())->method('save'); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenItemIsExpiredThenNullIdIsReturned(): void { // Given $testItem = $this->createExpiredTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(true); // When $resultId = $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); // Then $this->assertNull($resultId); } public function testWhenItemExpirationDateIsExactlyCurrentTimeThenItemIsSaved(): void { // Given $testItem = $this->createItemWithExpiration($this->fixedCurrentTime->format(self::DATE_FORMAT)); $this->expirationPolicy->method('isExpired')->willReturn(true); $this->itemRepository->expects($this->never())->method('save'); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenItemExpirationDateIsExactlyCurrentTimeThenOrderIsPlaced(): void { // Given $testItem = $this->createItemWithExpiration($this->fixedCurrentTime->format(self::DATE_FORMAT)); $this->expirationPolicy->method('isExpired')->willReturn(true); $this->orderService->expects($this->once())->method('orderItem'); // When & Then $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenItemExpirationDateIsExactlyCurrentTimeThenNullIdIsReturned(): void { // Given $testItem = $this->createItemWithExpiration($this->fixedCurrentTime->format(self::DATE_FORMAT)); $this->expirationPolicy->method('isExpired')->willReturn(true); // When $resultId = $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); // Then $this->assertNull($resultId); } public function testWhenItemExpirationDateIsInFutureThenItemSaved(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $this->itemRepository->expects($this->once())->method('save'); // When $resultId = $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); // Then $this->assertNotEmpty($resultId); } public function testWhenRepositorySaveThrowsExceptionThenRuntimeExceptionThrown(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $expectedException = new \RuntimeException('Repository error'); $this->itemRepository->expects($this->once()) ->method('save') ->willThrowException($expectedException); // When & Then $this->expectException(\RuntimeException::class); $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenRepositorySaveThrowsExceptionThenOrderIsNotPlaced(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $expectedException = new \RuntimeException('Repository error'); $this->itemRepository->expects($this->once()) ->method('save') ->willThrowException($expectedException); $this->orderService->expects($this->never())->method('orderItem'); // When & Then $this->expectException(\RuntimeException::class); $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenOrderServiceThrowsExceptionThenRuntimeExceptionThrown(): void { // Given $testItem = $this->createExpiredTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(true); $expectedException = new \RuntimeException('Order service error'); $this->itemRepository->expects($this->never())->method('save'); $this->orderService->expects($this->once()) ->method('orderItem') ->willThrowException($expectedException); // When & Then // The implementation logs the exception and does not throw, so we just call execute $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenClockThrowsExceptionThenRuntimeExceptionThrown(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $expectedException = new \RuntimeException('Clock error'); $this->timeProvider->method('now') ->willThrowException($expectedException); $this->itemRepository->expects($this->never())->method('save'); $this->orderService->expects($this->never())->method('orderItem'); // When & Then $this->expectException(\RuntimeException::class); $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenClockThrowsExceptionThenItemIsNotSaved(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $expectedException = new \RuntimeException('Clock error'); $this->timeProvider->method('now') ->willThrowException($expectedException); $this->itemRepository->expects($this->never())->method('save'); // When & Then $this->expectException(\RuntimeException::class); $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } public function testWhenClockThrowsExceptionThenOrderIsNotPlaced(): void { // Given $testItem = $this->createTestItem(); $this->expirationPolicy->method('isExpired')->willReturn(false); $expectedException = new \RuntimeException('Clock error'); $this->timeProvider->method('now') ->willThrowException($expectedException); $this->orderService->expects($this->never())->method('orderItem'); // When & Then $this->expectException(\RuntimeException::class); $this->addItem->execute( $testItem['name'], $testItem['expirationDate']->format(self::DATE_FORMAT), $testItem['orderUrl'], $testItem['userId'] ); } }