Multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
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.
 
 
 
 
 
 

357 lines
12 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 AutoStore\Domain\Specifications\ItemExpirationSpec;
use AutoStore\Domain\Specifications\Specification;
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 ItemExpirationSpec&\PHPUnit\Framework\MockObject\MockObject $expirationSpec;
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->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();
}
}