Browse Source

PHP8 implementation clean-up and tests

php8
chodak166 4 months ago
parent
commit
21c2c2a364
  1. 1
      php8/.devcontainer/devcontainer.json
  2. 28
      php8/src/Application.php
  3. 11
      php8/src/Application/Commands/AddItem.php
  4. 10
      php8/src/Application/Commands/HandleExpiredItems.php
  5. 13
      php8/src/DiContainer.php
  6. 141
      php8/src/Domain/Entities/Item.php
  7. 9
      php8/src/Domain/Policies/ItemExpirationPolicy.php
  8. 2
      php8/src/Infrastructure/Adapters/SystemTimeProvider.php
  9. 2
      php8/src/Infrastructure/Auth/JwtAuthService.php
  10. 2
      php8/src/Infrastructure/Http/HttpOrderService.php
  11. 2
      php8/src/Infrastructure/Http/JwtMiddleware.php
  12. 2
      php8/src/WebApi/Controllers/AuthController.php
  13. 2
      php8/src/WebApi/Controllers/BaseController.php
  14. 2
      php8/src/WebApi/Controllers/StoreController.php
  15. 52
      php8/src/WebApi/Router.php
  16. 510
      php8/tests/Unit/AddItemTest.php

1
php8/.devcontainer/devcontainer.json

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

28
php8/src/Application.php

@ -4,11 +4,8 @@ declare(strict_types=1);
namespace AutoStore; namespace AutoStore;
use AutoStore\Infrastructure\Http\JwtMiddleware; use AutoStore\WebApi\Router;
use AutoStore\WebApi\Controllers\AuthController;
use AutoStore\WebApi\Controllers\StoreController;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
class Application class Application
{ {
@ -33,29 +30,12 @@ class Application
private function setupRoutes(): void private function setupRoutes(): void
{ {
$jwtMiddleware = $this->di->get(JwtMiddleware::class); $router = new Router($this->app, $this->di);
$authController = $this->di->get(AuthController::class); $router->setupRoutes();
$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);
});
} }
public function run(): void public function run(): void
{ {
$this->app->run(); $this->app->run();
} }
}
}

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

@ -34,7 +34,7 @@ class AddItem
$this->logger = $logger; $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 { try {
$item = new Item( $item = new Item(
@ -46,14 +46,13 @@ class AddItem
); );
$currentTime = $this->timeProvider->now(); $currentTime = $this->timeProvider->now();
$this->expirationPolicy->checkExpiration($item, $currentTime); if ($this->expirationPolicy->isExpired($item, $currentTime)) {
if ($item->isExpired()) {
try { try {
$this->orderService->orderItem($item); $this->orderService->orderItem($item);
$item->markAsOrdered(); return null;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Failed to place order for expired item: ' . $e->getMessage()); $this->logger->error('Failed to place order for expired item: ' . $e->getMessage());
return null;
} }
} }
@ -64,4 +63,4 @@ class AddItem
throw new ApplicationException('Failed to add item: ' . $e->getMessage(), 0, $e); throw new ApplicationException('Failed to add item: ' . $e->getMessage(), 0, $e);
} }
} }
} }

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

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

13
php8/src/DiContainer.php

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

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

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

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

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

2
php8/src/Infrastructure/Adapters/SystemTimeProvider.php

@ -13,4 +13,4 @@ class SystemTimeProvider implements ITimeProvider
{ {
return new DateTimeImmutable(); return new DateTimeImmutable();
} }
} }

2
php8/src/Infrastructure/Auth/JwtAuthService.php

@ -85,4 +85,4 @@ class JwtAuthService implements IAuthService
return null; return null;
} }
} }
} }

2
php8/src/Infrastructure/Http/HttpOrderService.php

@ -60,4 +60,4 @@ class HttpOrderService implements IOrderService
throw OrderException::create($item->getOrderUrl(), $e->getMessage()); throw OrderException::create($item->getOrderUrl(), $e->getMessage());
} }
} }
} }

2
php8/src/Infrastructure/Http/JwtMiddleware.php

@ -62,4 +62,4 @@ class JwtMiddleware implements MiddlewareInterface
'message' => $message 'message' => $message
])); ]));
} }
} }

2
php8/src/WebApi/Controllers/AuthController.php

@ -40,4 +40,4 @@ class AuthController extends BaseController
return $this->createErrorResponse($response, 'Internal server error', 500); return $this->createErrorResponse($response, 'Internal server error', 500);
} }
} }
} }

2
php8/src/WebApi/Controllers/BaseController.php

@ -56,4 +56,4 @@ abstract class BaseController
return null; return null;
} }
} }

2
php8/src/WebApi/Controllers/StoreController.php

@ -109,4 +109,4 @@ class StoreController extends BaseController
return $this->createErrorResponse($response, 'Internal server error', 500); return $this->createErrorResponse($response, 'Internal server error', 500);
} }
} }
} }

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);
});
}
}

510
php8/tests/Unit/AddItemTest.php

@ -15,11 +15,21 @@ use Psr\Log\LoggerInterface;
class AddItemTest extends TestCase 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 AddItem $addItem;
private IItemRepository&\PHPUnit\Framework\MockObject\MockObject $itemRepository; private IItemRepository&\PHPUnit\Framework\MockObject\MockObject $itemRepository;
private IOrderService&\PHPUnit\Framework\MockObject\MockObject $orderService; private IOrderService&\PHPUnit\Framework\MockObject\MockObject $orderService;
private ITimeProvider&\PHPUnit\Framework\MockObject\MockObject $timeProvider; private ITimeProvider&\PHPUnit\Framework\MockObject\MockObject $timeProvider;
private LoggerInterface&\PHPUnit\Framework\MockObject\MockObject $logger; private LoggerInterface&\PHPUnit\Framework\MockObject\MockObject $logger;
private DateTimeImmutable $fixedCurrentTime;
protected function setUp(): void protected function setUp(): void
{ {
@ -28,6 +38,9 @@ class AddItemTest extends TestCase
$this->timeProvider = $this->createMock(ITimeProvider::class); $this->timeProvider = $this->createMock(ITimeProvider::class);
$this->logger = $this->createMock(LoggerInterface::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->addItem = new AddItem(
$this->itemRepository, $this->itemRepository,
$this->orderService, $this->orderService,
@ -36,69 +49,104 @@ class AddItemTest extends TestCase
); );
} }
public function testExecuteShouldSaveItemWhenNotExpired(): void private function createTestItem(): array
{ {
$userId = 'test-user-id'; return [
$itemName = 'Test Item'; 'name' => self::ITEM_NAME,
$expirationDate = new DateTimeImmutable('+1 day'); 'expirationDate' => new DateTimeImmutable(self::NOT_EXPIRED_DATE), // 1 day in the future
$orderUrl = 'http://example.com/order'; 'orderUrl' => self::ORDER_URL,
'userId' => self::USER_ID
];
}
$this->timeProvider->method('now') private function createExpiredTestItem(): array
->willReturn(new DateTimeImmutable()); {
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 private function createItemWithExpiration(string $expiration): array
$savedItem = null; {
$this->itemRepository->expects($this->once()) return [
->method('save') 'name' => self::ITEM_NAME,
->with($this->callback(function (Item $item) use ($itemName, $orderUrl, $userId, &$savedItem) { 'expirationDate' => new DateTimeImmutable($expiration),
$savedItem = $item; 'orderUrl' => self::ORDER_URL,
return $item->getName() === $itemName && 'userId' => self::USER_ID
$item->getOrderUrl() === $orderUrl && ];
$item->getUserId() === $userId && }
!$item->isOrdered();
}));
$this->orderService->expects($this->never()) private function getItemMatcher(array $expectedItem): callable
->method('orderItem'); {
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()) $this->itemRepository->expects($this->once())
->method('findById') ->method('save')
->willReturnCallback(function ($id) use (&$savedItem) { ->with($this->callback($this->getItemMatcher($testItem)));
return $savedItem;
}); // 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 $this->orderService->expects($this->never())->method('orderItem');
$result = $this->itemRepository->findById($resultId);
$this->assertSame($itemName, $result->getName()); // When & Then
// Compare DateTime objects without microseconds $this->addItem->execute(
$this->assertEquals($expirationDate->format('Y-m-d H:i:s'), $result->getExpirationDate()->format('Y-m-d H:i:s')); $testItem['name'],
$this->assertSame($orderUrl, $result->getOrderUrl()); $testItem['expirationDate']->format(self::DATE_FORMAT),
$this->assertSame($userId, $result->getUserId()); $testItem['orderUrl'],
$this->assertFalse($result->isOrdered()); $testItem['userId']
);
} }
public function testExecuteShouldPlaceOrderWhenItemIsExpired(): void public function testWhenItemNotExpiredThenNewItemIdIsReturned(): void
{ {
$userId = 'test-user-id'; // Given
$itemName = 'Test Item'; $testItem = $this->createTestItem();
$expirationDate = new DateTimeImmutable('-1 day'); $this->orderService->expects($this->never())->method('orderItem');
$orderUrl = 'http://example.com/order';
// When
$resultId = $this->addItem->execute(
$testItem['name'],
$testItem['expirationDate']->format(self::DATE_FORMAT),
$testItem['orderUrl'],
$testItem['userId']
);
$this->timeProvider->method('now') // Then
->willReturn(new DateTimeImmutable()); $this->assertNotNull($resultId);
$this->assertNotEmpty($resultId);
}
public function testWhenItemIsExpiredThenOrderPlaced(): void
{
// Given
$testItem = $this->createExpiredTestItem();
$savedItem = null;
$orderedItem = null; $orderedItem = null;
$this->itemRepository->expects($this->once()) $this->itemRepository->expects($this->never())->method('save');
->method('save')
->with($this->callback(function (Item $item) use (&$savedItem) {
$savedItem = $item;
return true;
}));
$this->orderService->expects($this->once()) $this->orderService->expects($this->once())
->method('orderItem') ->method('orderItem')
@ -107,106 +155,340 @@ class AddItemTest extends TestCase
return true; 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 // When
$result = $this->itemRepository->findById($resultId); $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'; // Given
$itemName = ''; $testItem = $this->createTestItem();
$expirationDate = new DateTimeImmutable('+1 day'); $testItem['name'] = '';
$orderUrl = 'http://example.com/order';
$this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class); $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'; // Given
$itemName = 'Test Item'; $testItem = $this->createTestItem();
$expirationDate = new DateTimeImmutable('+1 day'); $testItem['orderUrl'] = '';
$orderUrl = '';
$this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class); $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 = ''; // Given
$itemName = 'Test Item'; $testItem = $this->createTestItem();
$expirationDate = new DateTimeImmutable('+1 day'); $testItem['userId'] = '';
$orderUrl = 'http://example.com/order';
$this->expectException(\AutoStore\Application\Exceptions\ApplicationException::class); $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'; $userId = self::USER_ID;
$itemName = 'Test Item'; $itemName = self::ITEM_NAME;
$expirationDate = new DateTimeImmutable('-1 day'); $expirationDate = new DateTimeImmutable(self::EXPIRED_DATE);
$orderUrl = 'http://example.com/order'; $orderUrl = self::ORDER_URL;
$this->timeProvider->method('now')
->willReturn(new DateTimeImmutable());
// Mock the repository to return a saved item // Mock the repository to return a saved item
$savedItem = null; $this->itemRepository->expects($this->never())->method('save');
$this->itemRepository->expects($this->once())
->method('save')
->with($this->callback(function (Item $item) use (&$savedItem) {
$savedItem = $item;
return true;
}));
// Mock the order service to throw an exception // Mock the order service to throw an exception
$this->orderService->expects($this->once()) $this->orderService->expects($this->once())
->method('orderItem') ->method('orderItem')
->willThrowException(new \RuntimeException('Order service failed')); ->willThrowException(new \RuntimeException('Order service failed'));
$this->logger->expects($this->once()) $this->logger->expects($this->once())->method('error');
->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;
});
// The handler should not throw an exception when the order service fails // The handler should not throw an exception when the order service fails
// It should log the error and continue // 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 $this->orderService->expects($this->once())->method('orderItem');
$result = $this->itemRepository->findById($resultId);
$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