Browse Source

PHP8 implementation clean-up and tests

php8
chodak166 4 months ago
parent
commit
21c2c2a364
  1. 1
      php8/.devcontainer/devcontainer.json
  2. 26
      php8/src/Application.php
  3. 9
      php8/src/Application/Commands/AddItem.php
  4. 8
      php8/src/Application/Commands/HandleExpiredItems.php
  5. 11
      php8/src/DiContainer.php
  6. 139
      php8/src/Domain/Entities/Item.php
  7. 7
      php8/src/Domain/Policies/ItemExpirationPolicy.php
  8. 52
      php8/src/WebApi/Router.php
  9. 508
      php8/tests/Unit/AddItemTest.php

1
php8/.devcontainer/devcontainer.json

@ -12,6 +12,7 @@
},
"extensions": [
"xdebug.php-debug",
"devsense.phptools-vscode",
"bmewburn.vscode-intelephense-client",
"ms-vscode.vscode-json",
"mrmlnc.vscode-json5",

26
php8/src/Application.php

@ -4,11 +4,8 @@ declare(strict_types=1);
namespace AutoStore;
use AutoStore\Infrastructure\Http\JwtMiddleware;
use AutoStore\WebApi\Controllers\AuthController;
use AutoStore\WebApi\Controllers\StoreController;
use AutoStore\WebApi\Router;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
class Application
{
@ -33,29 +30,12 @@ class Application
private function setupRoutes(): void
{
$jwtMiddleware = $this->di->get(JwtMiddleware::class);
$authController = $this->di->get(AuthController::class);
$storeController = $this->di->get(StoreController::class);
// Public routes
$this->app->group('/api/v1', function (RouteCollectorProxy $group) use ($authController) {
$group->post('/login', [$authController, 'login']);
});
// Protected routes
$this->app->group('/api/v1', function (RouteCollectorProxy $group) use ($storeController, $jwtMiddleware) {
$group->group('/items', function (RouteCollectorProxy $itemGroup) use ($storeController) {
$itemGroup->get('', [$storeController, 'listItems']);
$itemGroup->post('', [$storeController, 'addItem']);
$itemGroup->get('/{id}', [$storeController, 'getItem']);
$itemGroup->delete('/{id}', [$storeController, 'deleteItem']);
})->add($jwtMiddleware);
});
$router = new Router($this->app, $this->di);
$router->setupRoutes();
}
public function run(): void
{
$this->app->run();
}
}

9
php8/src/Application/Commands/AddItem.php

@ -34,7 +34,7 @@ class AddItem
$this->logger = $logger;
}
public function execute(string $name, string $expirationDate, string $orderUrl, string $userId): string
public function execute(string $name, string $expirationDate, string $orderUrl, string $userId): ?string
{
try {
$item = new Item(
@ -46,14 +46,13 @@ class AddItem
);
$currentTime = $this->timeProvider->now();
$this->expirationPolicy->checkExpiration($item, $currentTime);
if ($item->isExpired()) {
if ($this->expirationPolicy->isExpired($item, $currentTime)) {
try {
$this->orderService->orderItem($item);
$item->markAsOrdered();
return null;
} catch (\Exception $e) {
$this->logger->error('Failed to place order for expired item: ' . $e->getMessage());
return null;
}
}

8
php8/src/Application/Commands/HandleExpiredItems.php

@ -40,13 +40,11 @@ class HandleExpiredItems
$items = $this->itemRepository->findExpired();
foreach ($items as $item) {
$this->expirationPolicy->checkExpiration($item, $currentTime);
if ($item->isExpired() && !$item->isOrdered()) {
$isExpired = $this->expirationPolicy->isExpired($item, $currentTime);
if ($isExpired) {
try {
$this->orderService->orderItem($item);
$item->markAsOrdered();
$this->itemRepository->save($item);
$this->itemRepository->delete($item);
} catch (\Exception $e) {
$this->logger->error('Failed to place order for expired item ' . $item->getId() . ': ' . $e->getMessage());
}

11
php8/src/DiContainer.php

@ -10,12 +10,10 @@ use AutoStore\Application\Interfaces\IItemRepository;
use AutoStore\Application\Interfaces\IOrderService;
use AutoStore\Application\Interfaces\ITimeProvider;
use AutoStore\Application\Interfaces\IUserRepository;
use AutoStore\Application\Services\TaskScheduler;
use AutoStore\Application\Commands\AddItem;
use AutoStore\Application\Commands\DeleteItem;
use AutoStore\Application\Commands\HandleExpiredItems;
use AutoStore\Application\Commands\LoginUser;
use AutoStore\Application\Commands\UpdateItem;
use AutoStore\Application\Queries\GetItem;
use AutoStore\Application\Queries\ListItems;
use AutoStore\Infrastructure\Adapters\SystemTimeProvider;
@ -43,7 +41,7 @@ class DiContainer
// Simplified app config, for real app use settings repository (env variables, database, etc.)
$configJson = file_get_contents(self::ROOT_DIR . '/configuration.json');
$config = json_decode($configJson, true);
$this->storagePath = $config['storage_directory'] ?? self::ROOT_DIR. '/storage';
$this->storagePath = $config['storage_directory'] ?? self::ROOT_DIR . '/storage';
$this->jwtSecret = $config['jwt_secret'] ?? 'secret-key';
$this->diContainer = new Container();
@ -147,13 +145,6 @@ class DiContainer
->addArgument(IItemRepository::class)
->setShared(true);
$di->add(UpdateItem::class)
->addArgument(IItemRepository::class)
->addArgument(IOrderService::class)
->addArgument(ITimeProvider::class)
->addArgument(LoggerInterface::class)
->setShared(true);
$di->add(DeleteItem::class)
->addArgument(IItemRepository::class)
->setShared(true);

139
php8/src/Domain/Entities/Item.php

@ -15,43 +15,37 @@ class Item
private DateTimeImmutable $expirationDate;
private string $orderUrl;
private string $userId;
private bool $expired;
private bool $ordered;
private DateTimeImmutable $createdAt;
private ?DateTimeImmutable $updatedAt;
public function __construct(
string $id,
string $name,
DateTimeImmutable $expirationDate,
string $orderUrl,
string $userId
) {
if (empty($id)) {
throw InvalidItemDataException::create('Item ID cannot be empty');
}
public function __construct(
string $id,
string $name,
DateTimeImmutable $expirationDate,
string $orderUrl,
string $userId
) {
if (empty($id)) {
throw InvalidItemDataException::create('Item ID cannot be empty');
}
if (empty($name)) {
throw InvalidItemDataException::create('Item name cannot be empty');
}
if (empty($name)) {
throw InvalidItemDataException::create('Item name cannot be empty');
}
if (empty($orderUrl)) {
throw InvalidItemDataException::create('Order URL cannot be empty');
}
if (empty($orderUrl)) {
throw InvalidItemDataException::create('Order URL cannot be empty');
}
if (empty($userId)) {
throw InvalidItemDataException::create('User ID cannot be empty');
}
if (empty($userId)) {
throw InvalidItemDataException::create('User ID cannot be empty');
}
$this->id = $id;
$this->name = $name;
$this->expirationDate = $expirationDate;
$this->orderUrl = $orderUrl;
$this->userId = $userId;
$this->expired = false;
$this->ordered = false;
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = null;
}
$this->id = $id;
$this->name = $name;
$this->expirationDate = $expirationDate;
$this->orderUrl = $orderUrl;
$this->userId = $userId;
$this->createdAt = new DateTimeImmutable();
}
public function getId(): string
{
@ -78,75 +72,11 @@ public function __construct(
return $this->userId;
}
public function isExpired(): bool
{
return $this->expired;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): ?DateTimeImmutable
{
return $this->updatedAt;
}
public function markAsExpired(): void
{
if ($this->expired) {
return;
}
$this->expired = true;
$this->updatedAt = new DateTimeImmutable();
}
public function isOrdered(): bool
{
return $this->ordered;
}
public function markAsOrdered(): void
{
if ($this->ordered) {
return;
}
$this->ordered = true;
$this->updatedAt = new DateTimeImmutable();
}
public function updateName(string $name): void
{
if ($this->name === $name) {
return;
}
$this->name = $name;
$this->updatedAt = new DateTimeImmutable();
}
public function updateExpirationDate(DateTimeImmutable $expirationDate): void
{
if ($this->expirationDate == $expirationDate) {
return;
}
$this->expirationDate = $expirationDate;
$this->updatedAt = new DateTimeImmutable();
}
public function updateOrderUrl(string $orderUrl): void
{
if ($this->orderUrl === $orderUrl) {
return;
}
$this->orderUrl = $orderUrl;
$this->updatedAt = new DateTimeImmutable();
}
public function toArray(): array
{
@ -156,10 +86,7 @@ public function __construct(
'expirationDate' => $this->expirationDate->format('Y-m-d\TH:i:s.uP'),
'orderUrl' => $this->orderUrl,
'userId' => $this->userId,
'expired' => $this->expired,
'is_ordered' => $this->ordered,
'createdAt' => $this->createdAt->format('Y-m-d\TH:i:s.uP'),
'updatedAt' => $this->updatedAt?->format('Y-m-d\TH:i:s.uP'),
'createdAt' => $this->createdAt->format('Y-m-d\TH:i:s.uP')
];
}
@ -177,18 +104,6 @@ public function __construct(
$data['userId']
);
if (isset($data['expired']) && is_bool($data['expired'])) {
if ($data['expired']) {
$item->markAsExpired();
}
}
if (isset($data['is_ordered']) && is_bool($data['is_ordered'])) {
if ($data['is_ordered']) {
$item->markAsOrdered();
}
}
return $item;
}
}

7
php8/src/Domain/Policies/ItemExpirationPolicy.php

@ -13,11 +13,4 @@ class ItemExpirationPolicy
{
return $item->getExpirationDate() <= $currentTime;
}
public function checkExpiration(Item $item, DateTimeImmutable $currentTime): void
{
if ($this->isExpired($item, $currentTime) && !$item->isExpired()) {
$item->markAsExpired();
}
}
}

52
php8/src/WebApi/Router.php

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace AutoStore\WebApi;
use AutoStore\DiContainer;
use AutoStore\Infrastructure\Http\JwtMiddleware;
use AutoStore\WebApi\Controllers\AuthController;
use AutoStore\WebApi\Controllers\StoreController;
use Slim\App;
use Slim\Routing\RouteCollectorProxy;
class Router
{
private App $app;
private DiContainer $container;
public function __construct(App $app, DiContainer $container)
{
$this->app = $app;
$this->container = $container;
}
public function setupRoutes(): void
{
$jwtMiddleware = $this->container->get(JwtMiddleware::class);
$authController = $this->container->get(AuthController::class);
$storeController = $this->container->get(StoreController::class);
$this->setupPublicRoutes($authController);
$this->setupProtectedRoutes($storeController, $jwtMiddleware);
}
private function setupPublicRoutes(AuthController $authController): void
{
$this->app->group('/api/v1', function (RouteCollectorProxy $group) use ($authController) {
$group->post('/login', [$authController, 'login']);
});
}
private function setupProtectedRoutes(StoreController $storeController, JwtMiddleware $jwtMiddleware): void
{
$this->app->group('/api/v1', function (RouteCollectorProxy $group) use ($storeController, $jwtMiddleware) {
$group->group('/items', function (RouteCollectorProxy $itemGroup) use ($storeController) {
$itemGroup->get('', [$storeController, 'listItems']);
$itemGroup->post('', [$storeController, 'addItem']);
$itemGroup->get('/{id}', [$storeController, 'getItem']);
$itemGroup->delete('/{id}', [$storeController, 'deleteItem']);
})->add($jwtMiddleware);
});
}
}

508
php8/tests/Unit/AddItemTest.php

@ -15,11 +15,21 @@ 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_ID = 'test-item-id';
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 LoggerInterface&\PHPUnit\Framework\MockObject\MockObject $logger;
private DateTimeImmutable $fixedCurrentTime;
protected function setUp(): void
{
@ -28,6 +38,9 @@ class AddItemTest extends TestCase
$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->addItem = new AddItem(
$this->itemRepository,
$this->orderService,
@ -36,69 +49,104 @@ class AddItemTest extends TestCase
);
}
public function testExecuteShouldSaveItemWhenNotExpired(): void
private function createTestItem(): array
{
$userId = 'test-user-id';
$itemName = 'Test Item';
$expirationDate = new DateTimeImmutable('+1 day');
$orderUrl = 'http://example.com/order';
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
];
}
$this->timeProvider->method('now')
->willReturn(new DateTimeImmutable());
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
];
}
// Capture the saved item
$savedItem = null;
$this->itemRepository->expects($this->once())
->method('save')
->with($this->callback(function (Item $item) use ($itemName, $orderUrl, $userId, &$savedItem) {
$savedItem = $item;
return $item->getName() === $itemName &&
$item->getOrderUrl() === $orderUrl &&
$item->getUserId() === $userId &&
!$item->isOrdered();
}));
private function createItemWithExpiration(string $expiration): array
{
return [
'name' => self::ITEM_NAME,
'expirationDate' => new DateTimeImmutable($expiration),
'orderUrl' => self::ORDER_URL,
'userId' => self::USER_ID
];
}
$this->orderService->expects($this->never())
->method('orderItem');
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'];
};
}
// Mock findById to return the saved item
public function testWhenItemNotExpiredThenItemSaved(): void
{
// Given
$testItem = $this->createTestItem();
$this->itemRepository->expects($this->once())
->method('findById')
->willReturnCallback(function ($id) use (&$savedItem) {
return $savedItem;
});
->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']
);
}
$resultId = $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
public function testWhenItemNotExpiredThenOrderIsNotPlaced(): void
{
// Given
$testItem = $this->createTestItem();
// Retrieve the saved item to verify its properties
$result = $this->itemRepository->findById($resultId);
$this->orderService->expects($this->never())->method('orderItem');
$this->assertSame($itemName, $result->getName());
// Compare DateTime objects without microseconds
$this->assertEquals($expirationDate->format('Y-m-d H:i:s'), $result->getExpirationDate()->format('Y-m-d H:i:s'));
$this->assertSame($orderUrl, $result->getOrderUrl());
$this->assertSame($userId, $result->getUserId());
$this->assertFalse($result->isOrdered());
// When & Then
$this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
}
public function testExecuteShouldPlaceOrderWhenItemIsExpired(): void
public function testWhenItemNotExpiredThenNewItemIdIsReturned(): void
{
$userId = 'test-user-id';
$itemName = 'Test Item';
$expirationDate = new DateTimeImmutable('-1 day');
$orderUrl = 'http://example.com/order';
// Given
$testItem = $this->createTestItem();
$this->orderService->expects($this->never())->method('orderItem');
// When
$resultId = $this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
$this->timeProvider->method('now')
->willReturn(new DateTimeImmutable());
// Then
$this->assertNotNull($resultId);
$this->assertNotEmpty($resultId);
}
public function testWhenItemIsExpiredThenOrderPlaced(): void
{
// Given
$testItem = $this->createExpiredTestItem();
$savedItem = null;
$orderedItem = null;
$this->itemRepository->expects($this->once())
->method('save')
->with($this->callback(function (Item $item) use (&$savedItem) {
$savedItem = $item;
return true;
}));
$this->itemRepository->expects($this->never())->method('save');
$this->orderService->expects($this->once())
->method('orderItem')
@ -107,106 +155,340 @@ class AddItemTest extends TestCase
return true;
}));
// Mock findById to return the ordered item
$this->itemRepository->expects($this->once())
->method('findById')
->willReturnCallback(function ($id) use (&$orderedItem) {
// Mark the item as ordered before returning it
if ($orderedItem) {
$orderedItem->markAsOrdered();
}
return $orderedItem;
});
$resultId = $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
// Retrieve the saved item to verify its properties
$result = $this->itemRepository->findById($resultId);
// When
$this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
$this->assertTrue($result->isOrdered());
// Then
$this->assertNotNull($orderedItem);
$this->assertEquals($testItem['name'], $orderedItem?->getName());
}
public function testExecuteShouldThrowExceptionWhenItemNameIsEmpty(): void
public function testWhenItemNameIsEmptyThenExceptionThrown(): void
{
$userId = 'test-user-id';
$itemName = '';
$expirationDate = new DateTimeImmutable('+1 day');
$orderUrl = 'http://example.com/order';
// Given
$testItem = $this->createTestItem();
$testItem['name'] = '';
$this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class);
$this->expectExceptionMessage('Failed to add item: Item name cannot be empty');
$this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
// When & Then
$this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
}
public function testExecuteShouldThrowExceptionWhenOrderUrlIsEmpty(): void
public function testWhenOrderUrlIsEmptyThenExceptionThrown(): void
{
$userId = 'test-user-id';
$itemName = 'Test Item';
$expirationDate = new DateTimeImmutable('+1 day');
$orderUrl = '';
// Given
$testItem = $this->createTestItem();
$testItem['orderUrl'] = '';
$this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class);
$this->expectExceptionMessage('Failed to add item: Order URL cannot be empty');
$this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
// When & Then
$this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
}
public function testExecuteShouldThrowExceptionWhenUserIdIsEmpty(): void
public function testWhenUserIdIsEmptyThenExceptionThrown(): void
{
$userId = '';
$itemName = 'Test Item';
$expirationDate = new DateTimeImmutable('+1 day');
$orderUrl = 'http://example.com/order';
// Given
$testItem = $this->createTestItem();
$testItem['userId'] = '';
$this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class);
$this->expectExceptionMessage('Failed to add item: User ID cannot be empty');
$this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
// When & Then
$this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
}
public function testExecuteShouldLogErrorWhenOrderServiceFails(): void
public function testWhenOrderServiceFailsThenErrorLogged(): void
{
$userId = 'test-user-id';
$itemName = 'Test Item';
$expirationDate = new DateTimeImmutable('-1 day');
$orderUrl = 'http://example.com/order';
$this->timeProvider->method('now')
->willReturn(new DateTimeImmutable());
$userId = self::USER_ID;
$itemName = self::ITEM_NAME;
$expirationDate = new DateTimeImmutable(self::EXPIRED_DATE);
$orderUrl = self::ORDER_URL;
// Mock the repository to return a saved item
$savedItem = null;
$this->itemRepository->expects($this->once())
->method('save')
->with($this->callback(function (Item $item) use (&$savedItem) {
$savedItem = $item;
return true;
}));
$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')
->with($this->stringContains('Failed to place order for expired item'));
// Mock findById to return the saved item
$this->itemRepository->expects($this->once())
->method('findById')
->willReturnCallback(function ($id) use (&$savedItem) {
return $savedItem;
});
$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
$resultId = $this->addItem->execute($itemName, $expirationDate->format('Y-m-d H:i:s'), $orderUrl, $userId);
$this->addItem->execute($itemName, $expirationDate->format(self::DATE_FORMAT), $orderUrl, $userId);
}
public function testWhenItemIsExpiredThenOrderIsPlaced(): void
{
// Given
$testItem = $this->createExpiredTestItem();
$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->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();
// 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->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));
// Retrieve the saved item to verify its properties
$result = $this->itemRepository->findById($resultId);
$this->orderService->expects($this->once())->method('orderItem');
$this->assertFalse($result->isOrdered());
// 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));
// 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->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();
$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();
$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();
$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();
$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();
$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();
$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']
);
}
}
Loading…
Cancel
Save