You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
313 lines
10 KiB
313 lines
10 KiB
<?php |
|
|
|
declare(strict_types=1); |
|
|
|
namespace AutoStore\Tests\Unit; |
|
|
|
use AutoStore\Application\Commands\HandleExpiredItems; |
|
use AutoStore\Application\Interfaces\IItemRepository; |
|
use AutoStore\Application\Interfaces\IOrderService; |
|
use AutoStore\Application\Interfaces\ITimeProvider; |
|
use AutoStore\Application\Exceptions\ApplicationException; |
|
use AutoStore\Domain\Entities\Item; |
|
use AutoStore\Domain\Exceptions\DomainException; |
|
use DateTimeImmutable; |
|
use PHPUnit\Framework\TestCase; |
|
use Psr\Log\LoggerInterface; |
|
|
|
class HandleExpiredItemsTest extends TestCase |
|
{ |
|
private const MOCKED_NOW = '2023-01-01 12:00:00'; |
|
private const EXPIRED_DATE = '2022-12-31 12:00:00'; |
|
private const NOT_EXPIRED_DATE = '2023-01-02 12:00:00'; |
|
private const ITEM_ID_1 = 'expired-item-id-1'; |
|
private const ITEM_ID_2 = 'expired-item-id-2'; |
|
private const ITEM_NAME_1 = 'Expired Item 1'; |
|
private const ITEM_NAME_2 = 'Expired Item 2'; |
|
private const ORDER_URL_1 = 'http://example.com/order1'; |
|
private const ORDER_URL_2 = 'http://example.com/order2'; |
|
private const USER_ID_1 = 'test-user-id-1'; |
|
private const USER_ID_2 = 'test-user-id-2'; |
|
|
|
private HandleExpiredItems $handleExpiredItems; |
|
private IItemRepository&\PHPUnit\Framework\MockObject\MockObject $itemRepository; |
|
private IOrderService&\PHPUnit\Framework\MockObject\MockObject $orderService; |
|
private ITimeProvider&\PHPUnit\Framework\MockObject\MockObject $timeProvider; |
|
private LoggerInterface&\PHPUnit\Framework\MockObject\MockObject $logger; |
|
private DateTimeImmutable $fixedCurrentTime; |
|
|
|
protected function setUp(): void |
|
{ |
|
$this->itemRepository = $this->createMock(IItemRepository::class); |
|
$this->orderService = $this->createMock(IOrderService::class); |
|
$this->timeProvider = $this->createMock(ITimeProvider::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->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->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]; |
|
|
|
$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]; |
|
|
|
$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'); |
|
|
|
$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]; |
|
|
|
$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->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 testWhenItemIsNotExpiredThenOrderNotPlacedAndItemNotDeleted(): void |
|
{ |
|
// Given |
|
// Create an item that's not actually expired despite being returned by the repository |
|
// This tests the double-check with isExpired() |
|
$notExpiredItem = new Item( |
|
self::ITEM_ID_1, |
|
self::ITEM_NAME_1, |
|
new DateTimeImmutable(self::NOT_EXPIRED_DATE), |
|
self::ORDER_URL_1, |
|
self::USER_ID_1 |
|
); |
|
|
|
$items = [$notExpiredItem]; |
|
|
|
$this->itemRepository->expects($this->once()) |
|
->method('findWhere') |
|
->willReturn($items); |
|
|
|
$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 testWhenMixedExpiredAndNonExpiredItemsThenOnlyExpiredItemsProcessed(): void |
|
{ |
|
// Given |
|
$expiredItem = $this->createExpiredItem1(); |
|
$notExpiredItem = new Item( |
|
self::ITEM_ID_2, |
|
self::ITEM_NAME_2, |
|
new DateTimeImmutable(self::NOT_EXPIRED_DATE), |
|
self::ORDER_URL_2, |
|
self::USER_ID_2 |
|
); |
|
|
|
$items = [$expiredItem, $notExpiredItem]; |
|
|
|
$this->itemRepository->expects($this->once()) |
|
->method('findWhere') |
|
->willReturn($items); |
|
|
|
$this->orderService->expects($this->once()) |
|
->method('orderItem') |
|
->with($expiredItem); |
|
|
|
$this->itemRepository->expects($this->once()) |
|
->method('delete') |
|
->with(self::ITEM_ID_1); |
|
|
|
$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]; |
|
|
|
$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 |
|
$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(); |
|
} |
|
} |
|
|
|
|