13 changed files with 1001 additions and 104 deletions
@ -0,0 +1,19 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace AutoStore\Domain\Filters; |
||||
|
||||
class FilterCondition extends FilterExpr |
||||
{ |
||||
public string $field; |
||||
public string $operator; |
||||
public $value; |
||||
|
||||
public function __construct(string $field, string $operator, $value = null) |
||||
{ |
||||
$this->field = $field; |
||||
$this->operator = $operator; |
||||
$this->value = $value; |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace AutoStore\Domain\Filters; |
||||
|
||||
abstract class FilterExpr |
||||
{ |
||||
} |
||||
@ -0,0 +1,22 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace AutoStore\Domain\Filters; |
||||
|
||||
class FilterGroup extends FilterExpr |
||||
{ |
||||
public string $type; |
||||
public array $children = []; |
||||
|
||||
public function __construct(string $type = 'AND') |
||||
{ |
||||
$this->type = strtoupper($type); |
||||
} |
||||
|
||||
public function add(FilterExpr $expr): self |
||||
{ |
||||
$this->children[] = $expr; |
||||
return $this; |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace AutoStore\Domain\Filters; |
||||
|
||||
class FilterOperator |
||||
{ |
||||
public const EQUALS = 'equals'; |
||||
public const NOT_EQUALS = 'not_equals'; |
||||
public const GREATER_THAN = 'greater_than'; |
||||
public const LESS_THAN = 'less_than'; |
||||
public const GREATER_EQ = 'greater_eq'; |
||||
public const LESS_EQ = 'less_eq'; |
||||
public const IN = 'in'; |
||||
public const NOT_IN = 'not_in'; |
||||
public const LIKE = 'like'; |
||||
public const IS_NULL = 'is_null'; |
||||
public const IS_NOT_NULL = 'is_not_null'; |
||||
} |
||||
@ -0,0 +1,236 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace AutoStore\Domain\Filters; |
||||
|
||||
class FilterSpecification |
||||
{ |
||||
private FilterGroup $rootGroup; |
||||
private FilterGroup $currentGroup; |
||||
private array $groupStack = []; |
||||
private ?string $lastField = null; |
||||
|
||||
public function __construct(?string $field = null) |
||||
{ |
||||
$this->rootGroup = new FilterGroup('AND'); |
||||
$this->currentGroup = $this->rootGroup; |
||||
if ($field !== null) { |
||||
$this->field($field); |
||||
} |
||||
} |
||||
|
||||
public function field(string $field): self |
||||
{ |
||||
$this->lastField = $field; |
||||
return $this; |
||||
} |
||||
|
||||
public function equals($value): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::EQUALS, $value)); |
||||
return $this; |
||||
} |
||||
|
||||
public function notEquals($value): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::NOT_EQUALS, $value)); |
||||
return $this; |
||||
} |
||||
|
||||
public function greaterThan($value): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::GREATER_THAN, $value)); |
||||
return $this; |
||||
} |
||||
|
||||
public function lessThan($value): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LESS_THAN, $value)); |
||||
return $this; |
||||
} |
||||
|
||||
public function greaterEq($value): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::GREATER_EQ, $value)); |
||||
return $this; |
||||
} |
||||
|
||||
public function lessEq($value): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LESS_EQ, $value)); |
||||
return $this; |
||||
} |
||||
|
||||
public function in(array $values): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::IN, $values)); |
||||
return $this; |
||||
} |
||||
|
||||
public function notIn(array $values): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::NOT_IN, $values)); |
||||
return $this; |
||||
} |
||||
|
||||
public function like(string $pattern): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, $pattern)); |
||||
return $this; |
||||
} |
||||
|
||||
public function contains(string $needle): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, "%$needle%")); |
||||
return $this; |
||||
} |
||||
|
||||
public function startsWith(string $prefix): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, $prefix . '%')); |
||||
return $this; |
||||
} |
||||
|
||||
public function endsWith(string $suffix): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, '%' . $suffix)); |
||||
return $this; |
||||
} |
||||
|
||||
public function isNull(): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::IS_NULL)); |
||||
return $this; |
||||
} |
||||
|
||||
public function isNotNull(): self |
||||
{ |
||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::IS_NOT_NULL)); |
||||
return $this; |
||||
} |
||||
|
||||
public function or(?string $field = null): self |
||||
{ |
||||
$newGroup = new FilterGroup('OR'); |
||||
if (!empty($this->currentGroup->children)) { |
||||
$lastExpr = array_pop($this->currentGroup->children); |
||||
$newGroup->add($lastExpr); |
||||
} |
||||
$this->currentGroup->add($newGroup); |
||||
$this->groupStack[] = $this->currentGroup; |
||||
$this->currentGroup = $newGroup; |
||||
if ($field !== null) { |
||||
$this->field($field); |
||||
} |
||||
return $this; |
||||
} |
||||
|
||||
public function and(?string $field = null): self |
||||
{ |
||||
$newGroup = new FilterGroup('AND'); |
||||
if (!empty($this->currentGroup->children)) { |
||||
$lastExpr = array_pop($this->currentGroup->children); |
||||
$newGroup->add($lastExpr); |
||||
} |
||||
$this->currentGroup->add($newGroup); |
||||
$this->groupStack[] = $this->currentGroup; |
||||
$this->currentGroup = $newGroup; |
||||
if ($field !== null) { |
||||
$this->field($field); |
||||
} |
||||
return $this; |
||||
} |
||||
|
||||
public function close(): self |
||||
{ |
||||
if (empty($this->groupStack)) { |
||||
throw new \LogicException('No group to close.'); |
||||
} |
||||
$this->currentGroup = array_pop($this->groupStack); |
||||
return $this; |
||||
} |
||||
|
||||
public function getExpression(): FilterExpr |
||||
{ |
||||
return $this->rootGroup; |
||||
} |
||||
|
||||
public function matches(array $data): bool |
||||
{ |
||||
return $this->evaluateExpression($this->rootGroup, $data); |
||||
} |
||||
|
||||
private function evaluateExpression(FilterExpr $expr, array $data): bool |
||||
{ |
||||
if ($expr instanceof FilterCondition) { |
||||
return $this->evaluateCondition($expr, $data); |
||||
} |
||||
|
||||
if ($expr instanceof FilterGroup) { |
||||
return $this->evaluateGroup($expr, $data); |
||||
} |
||||
|
||||
throw new \LogicException('Unknown filter expression type.'); |
||||
} |
||||
|
||||
private function evaluateCondition(FilterCondition $condition, array $data): bool |
||||
{ |
||||
if (!array_key_exists($condition->field, $data)) { |
||||
return false; |
||||
} |
||||
|
||||
$value = $data[$condition->field]; |
||||
|
||||
switch ($condition->operator) { |
||||
case FilterOperator::EQUALS: |
||||
return $value == $condition->value; |
||||
case FilterOperator::NOT_EQUALS: |
||||
return $value != $condition->value; |
||||
case FilterOperator::GREATER_THAN: |
||||
return $value > $condition->value; |
||||
case FilterOperator::LESS_THAN: |
||||
return $value < $condition->value; |
||||
case FilterOperator::GREATER_EQ: |
||||
return $value >= $condition->value; |
||||
case FilterOperator::LESS_EQ: |
||||
return $value <= $condition->value; |
||||
case FilterOperator::IN: |
||||
return in_array($value, $condition->value); |
||||
case FilterOperator::NOT_IN: |
||||
return !in_array($value, $condition->value); |
||||
case FilterOperator::LIKE: |
||||
return str_contains((string)$value, str_replace('%', '', $condition->value)); |
||||
case FilterOperator::IS_NULL: |
||||
return $value === null; |
||||
case FilterOperator::IS_NOT_NULL: |
||||
return $value !== null; |
||||
default: |
||||
throw new \LogicException("Unknown operator: {$condition->operator}"); |
||||
} |
||||
} |
||||
|
||||
private function evaluateGroup(FilterGroup $group, array $data): bool |
||||
{ |
||||
if (empty($group->children)) { |
||||
return true; |
||||
} |
||||
|
||||
$results = []; |
||||
foreach ($group->children as $child) { |
||||
$results[] = $this->evaluateExpression($child, $data); |
||||
} |
||||
|
||||
return $group->type === 'AND' ? !in_array(false, $results) : in_array(true, $results); |
||||
} |
||||
|
||||
private function consumeField(): string |
||||
{ |
||||
if ($this->lastField === null) { |
||||
throw new \LogicException("No field specified."); |
||||
} |
||||
$field = $this->lastField; |
||||
$this->lastField = null; |
||||
return $field; |
||||
} |
||||
} |
||||
@ -0,0 +1,313 @@
|
||||
<?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(); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,155 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace AutoStore\Tests\Unit\Domain\Policies; |
||||
|
||||
use AutoStore\Domain\Entities\Item; |
||||
use AutoStore\Domain\Filters\FilterSpecification; |
||||
use AutoStore\Domain\Policies\ItemExpirationPolicy; |
||||
use DateTimeImmutable; |
||||
use PHPUnit\Framework\TestCase; |
||||
|
||||
class ItemExpirationPolicyTest extends TestCase |
||||
{ |
||||
private const ITEM_ID = 'test-id'; |
||||
private const ITEM_ID_2 = 'test-id-2'; |
||||
private const ITEM_NAME = 'Test Item'; |
||||
private const ITEM_NAME_2 = 'Test Item 2'; |
||||
private const ORDER_URL = 'http://example.com/order'; |
||||
private const ORDER_URL_2 = 'http://example.com/order2'; |
||||
private const USER_ID = 'user-id'; |
||||
private const USER_ID_2 = 'user-id-2'; |
||||
private const CURRENT_TIME = '2023-01-01 12:00:00'; |
||||
private const EXPIRED_TIME = '2023-01-01 11:00:00'; |
||||
private const VALID_TIME = '2023-01-01 13:00:00'; |
||||
|
||||
private ItemExpirationPolicy $policy; |
||||
|
||||
protected function setUp(): void |
||||
{ |
||||
$this->policy = new ItemExpirationPolicy(); |
||||
} |
||||
|
||||
private function createExpiredItem(): Item |
||||
{ |
||||
return new Item( |
||||
self::ITEM_ID, |
||||
self::ITEM_NAME, |
||||
new DateTimeImmutable('-1 day'), |
||||
self::ORDER_URL, |
||||
self::USER_ID |
||||
); |
||||
} |
||||
|
||||
private function createValidItem(): Item |
||||
{ |
||||
return new Item( |
||||
self::ITEM_ID, |
||||
self::ITEM_NAME, |
||||
new DateTimeImmutable('+1 day'), |
||||
self::ORDER_URL, |
||||
self::USER_ID |
||||
); |
||||
} |
||||
|
||||
private function createItemWithExpiration(string $expirationDate): Item |
||||
{ |
||||
return new Item( |
||||
self::ITEM_ID, |
||||
self::ITEM_NAME, |
||||
new DateTimeImmutable($expirationDate), |
||||
self::ORDER_URL, |
||||
self::USER_ID |
||||
); |
||||
} |
||||
|
||||
private function createSecondItemWithExpiration(string $expirationDate): Item |
||||
{ |
||||
return new Item( |
||||
self::ITEM_ID_2, |
||||
self::ITEM_NAME_2, |
||||
new DateTimeImmutable($expirationDate), |
||||
self::ORDER_URL_2, |
||||
self::USER_ID_2 |
||||
); |
||||
} |
||||
|
||||
public function testWhenItemIsExpiredThenIsExpiredReturnsTrue(): void |
||||
{ |
||||
// Given |
||||
$item = $this->createExpiredItem(); |
||||
$currentTime = new DateTimeImmutable(); |
||||
|
||||
// When |
||||
$result = $this->policy->isExpired($item, $currentTime); |
||||
|
||||
// Then |
||||
$this->assertTrue($result); |
||||
} |
||||
|
||||
public function testWhenItemIsNotExpiredThenIsExpiredReturnsFalse(): void |
||||
{ |
||||
// Given |
||||
$item = $this->createValidItem(); |
||||
$currentTime = new DateTimeImmutable(); |
||||
|
||||
// When |
||||
$result = $this->policy->isExpired($item, $currentTime); |
||||
|
||||
// Then |
||||
$this->assertFalse($result); |
||||
} |
||||
|
||||
public function testGetExpirationSpecShouldReturnCorrectFilter(): void |
||||
{ |
||||
// Given |
||||
$currentTime = new DateTimeImmutable(self::CURRENT_TIME); |
||||
|
||||
// When |
||||
$specification = $this->policy->getExpirationSpec($currentTime); |
||||
|
||||
// Then |
||||
$this->assertInstanceOf(FilterSpecification::class, $specification); |
||||
|
||||
// Test the filter with mock data using the same format as Item::toArray() |
||||
$expiredData = [ |
||||
'id' => self::ITEM_ID, |
||||
'name' => self::ITEM_NAME, |
||||
'expirationDate' => self::EXPIRED_TIME, |
||||
'orderUrl' => self::ORDER_URL, |
||||
'userId' => self::USER_ID |
||||
]; |
||||
|
||||
$validData = [ |
||||
'id' => self::ITEM_ID_2, |
||||
'name' => self::ITEM_NAME_2, |
||||
'expirationDate' => self::VALID_TIME, |
||||
'orderUrl' => self::ORDER_URL_2, |
||||
'userId' => self::USER_ID_2 |
||||
]; |
||||
|
||||
$this->assertTrue($specification->matches($expiredData)); |
||||
$this->assertFalse($specification->matches($validData)); |
||||
} |
||||
|
||||
public function testGetExpirationSpecShouldWorkWithItemObjects(): void |
||||
{ |
||||
// Given |
||||
$currentTime = new DateTimeImmutable(self::CURRENT_TIME); |
||||
$expiredItem = $this->createItemWithExpiration(self::EXPIRED_TIME); |
||||
$validItem = $this->createSecondItemWithExpiration(self::VALID_TIME); |
||||
|
||||
// When |
||||
$specification = $this->policy->getExpirationSpec($currentTime); |
||||
|
||||
// Then |
||||
// Test with array representation |
||||
$expiredArray = $expiredItem->toArray(); |
||||
$validArray = $validItem->toArray(); |
||||
|
||||
// Check that the specification matches the array representation |
||||
$this->assertTrue($specification->matches($expiredArray)); |
||||
$this->assertFalse($specification->matches($validArray)); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue