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.
 
 
 
 
 
 

512 lines
17 KiB

<?php
declare(strict_types=1);
namespace AutoStore\Tests\Unit;
use AutoStore\Application\Interfaces\IItemRepository;
use AutoStore\Application\Interfaces\IOrderService;
use AutoStore\Application\Interfaces\ITimeProvider;
use AutoStore\Application\Commands\AddItem;
use AutoStore\Domain\Entities\Item;
use AutoStore\Domain\Specifications\ItemExpirationSpec;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class AddItemTest extends TestCase
{
private const MOCKED_NOW = '2023-01-01 12:00:00';
private const NOT_EXPIRED_DATE = '2023-01-02 12:00:00';
private const EXPIRED_DATE = '2022-12-31 12:00:00';
private const ITEM_NAME = 'Test Item';
private const ORDER_URL = 'http://example.com/order';
private const USER_ID = 'test-user-id';
private const DATE_FORMAT = 'Y-m-d H:i:s';
private AddItem $addItem;
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 $expirationPolicy;
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->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']
);
}
}