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.
688 lines
21 KiB
688 lines
21 KiB
<?php |
|
|
|
declare(strict_types=1); |
|
|
|
namespace AutoStore\Tests\Unit\Domain\Specifications; |
|
|
|
use AutoStore\Domain\Specifications\Specification; |
|
use AutoStore\Domain\Specifications\Spec; |
|
use DateTimeImmutable; |
|
use PHPUnit\Framework\TestCase; |
|
use stdClass; |
|
|
|
class SpecificationTest extends TestCase |
|
{ |
|
// Test data constants |
|
private const TEST_STRING = 'test-value'; |
|
private const TEST_INT = 42; |
|
private const TEST_FLOAT = 3.14; |
|
private const TEST_DATE = '2023-01-15 10:30:00'; |
|
private const TEST_DATE_2 = '2023-02-15 10:30:00'; |
|
|
|
private function createSimpleObject(array $data): stdClass |
|
{ |
|
$object = new stdClass(); |
|
foreach ($data as $key => $value) { |
|
$object->$key = $value; |
|
} |
|
return $object; |
|
} |
|
|
|
private function createComplexObject(array $data): TestObject |
|
{ |
|
return new TestObject($data); |
|
} |
|
|
|
// EQ Operator Tests |
|
|
|
public function testWhenUsingEqWithStringThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('name', self::TEST_STRING)); |
|
$matchingObject = $this->createSimpleObject(['name' => self::TEST_STRING]); |
|
$nonMatchingObject = $this->createSimpleObject(['name' => 'different']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingEqWithIntegerThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('age', self::TEST_INT)); |
|
$matchingObject = $this->createSimpleObject(['age' => self::TEST_INT]); |
|
$nonMatchingObject = $this->createSimpleObject(['age' => 100]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingEqWithFloatThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('price', self::TEST_FLOAT)); |
|
$matchingObject = $this->createSimpleObject(['price' => self::TEST_FLOAT]); |
|
$nonMatchingObject = $this->createSimpleObject(['price' => 1.0]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingEqWithComplexObjectGetterThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('name', self::TEST_STRING)); |
|
$matchingObject = $this->createComplexObject(['name' => self::TEST_STRING]); |
|
$nonMatchingObject = $this->createComplexObject(['name' => 'different']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
// NEQ Operator Tests |
|
|
|
public function testWhenUsingNeqThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::neq('status', 'inactive')); |
|
$matchingObject = $this->createSimpleObject(['status' => 'active']); |
|
$nonMatchingObject = $this->createSimpleObject(['status' => 'inactive']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
// Comparison Operators Tests |
|
|
|
public function testWhenUsingGtThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::gt('score', 80)); |
|
$matchingObject = $this->createSimpleObject(['score' => 90]); |
|
$nonMatchingObject = $this->createSimpleObject(['score' => 70]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingGteThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::gte('score', 80)); |
|
$matchingObject1 = $this->createSimpleObject(['score' => 80]); |
|
$matchingObject2 = $this->createSimpleObject(['score' => 90]); |
|
$nonMatchingObject = $this->createSimpleObject(['score' => 70]); |
|
|
|
// When |
|
$matchResult1 = $spec->match($matchingObject1); |
|
$matchResult2 = $spec->match($matchingObject2); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult1); |
|
$this->assertTrue($matchResult2); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingLtThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::lt('score', 80)); |
|
$matchingObject = $this->createSimpleObject(['score' => 70]); |
|
$nonMatchingObject = $this->createSimpleObject(['score' => 90]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingLteThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::lte('score', 80)); |
|
$matchingObject1 = $this->createSimpleObject(['score' => 80]); |
|
$matchingObject2 = $this->createSimpleObject(['score' => 70]); |
|
$nonMatchingObject = $this->createSimpleObject(['score' => 90]); |
|
|
|
// When |
|
$matchResult1 = $spec->match($matchingObject1); |
|
$matchResult2 = $spec->match($matchingObject2); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult1); |
|
$this->assertTrue($matchResult2); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
// IN Operator Tests |
|
|
|
public function testWhenUsingInThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$validValues = ['admin', 'moderator', 'editor']; |
|
$spec = new Specification(Spec::in('role', $validValues)); |
|
$matchingObject = $this->createSimpleObject(['role' => 'admin']); |
|
$nonMatchingObject = $this->createSimpleObject(['role' => 'user']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingInWithEmptyArrayThenNeverMatches(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::in('role', [])); |
|
$testObject = $this->createSimpleObject(['role' => 'admin']); |
|
|
|
// When |
|
$result = $spec->match($testObject); |
|
|
|
// Then |
|
$this->assertFalse($result); |
|
} |
|
|
|
// NOT IN Operator Tests |
|
|
|
public function testWhenUsingNotInThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$invalidValues = ['banned', 'suspended']; |
|
$spec = new Specification(Spec::nin('status', $invalidValues)); |
|
$matchingObject = $this->createSimpleObject(['status' => 'active']); |
|
$nonMatchingObject = $this->createSimpleObject(['status' => 'banned']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
// DateTime Tests |
|
|
|
public function testWhenUsingDateTimeComparisonWithStringsThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::lt('createdAt', self::TEST_DATE_2)); |
|
$matchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE]); |
|
$nonMatchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE_2]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingDateTimeComparisonWithDateTimeObjectsThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$testDate = new DateTimeImmutable(self::TEST_DATE); |
|
$spec = new Specification(Spec::lte('expirationDate', $testDate)); |
|
$matchingObject = $this->createComplexObject(['expirationDate' => new DateTimeImmutable(self::TEST_DATE)]); |
|
$nonMatchingObject = $this->createComplexObject(['expirationDate' => new DateTimeImmutable(self::TEST_DATE_2)]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingDateTimeComparisonWithMixedTypesThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$testDate = new DateTimeImmutable(self::TEST_DATE); |
|
$spec = new Specification(Spec::eq('createdAt', $testDate)); |
|
$matchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE]); |
|
|
|
// When |
|
$result = $spec->match($matchingObject); |
|
|
|
// Then |
|
$this->assertTrue($result); |
|
} |
|
|
|
// AND Group Tests |
|
|
|
public function testWhenUsingAndGroupThenMatchesOnlyWhenAllConditionsMet(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::and([ |
|
Spec::eq('status', 'active'), |
|
Spec::gte('score', 80), |
|
Spec::in('role', ['admin', 'moderator']) |
|
])); |
|
$matchingObject = $this->createSimpleObject([ |
|
'status' => 'active', |
|
'score' => 85, |
|
'role' => 'admin' |
|
]); |
|
$nonMatchingObject1 = $this->createSimpleObject([ |
|
'status' => 'inactive', |
|
'score' => 85, |
|
'role' => 'admin' |
|
]); |
|
$nonMatchingObject2 = $this->createSimpleObject([ |
|
'status' => 'active', |
|
'score' => 70, |
|
'role' => 'admin' |
|
]); |
|
$nonMatchingObject3 = $this->createSimpleObject([ |
|
'status' => 'active', |
|
'score' => 85, |
|
'role' => 'user' |
|
]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult1 = $spec->match($nonMatchingObject1); |
|
$noMatchResult2 = $spec->match($nonMatchingObject2); |
|
$noMatchResult3 = $spec->match($nonMatchingObject3); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult1); |
|
$this->assertFalse($noMatchResult2); |
|
$this->assertFalse($noMatchResult3); |
|
} |
|
|
|
public function testWhenUsingEmptyAndGroupThenAlwaysMatches(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::and([])); |
|
$testObject = $this->createSimpleObject(['any' => 'value']); |
|
|
|
// When |
|
$result = $spec->match($testObject); |
|
|
|
// Then |
|
$this->assertTrue($result); |
|
} |
|
|
|
// OR Group Tests |
|
|
|
public function testWhenUsingOrGroupThenMatchesWhenAnyConditionMet(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::or([ |
|
Spec::eq('role', 'admin'), |
|
Spec::gte('score', 90), |
|
Spec::in('department', ['IT', 'HR']) |
|
])); |
|
$matchingObject1 = $this->createSimpleObject([ |
|
'role' => 'admin', |
|
'score' => 70, |
|
'department' => 'Finance' |
|
]); |
|
$matchingObject2 = $this->createSimpleObject([ |
|
'role' => 'user', |
|
'score' => 95, |
|
'department' => 'Finance' |
|
]); |
|
$matchingObject3 = $this->createSimpleObject([ |
|
'role' => 'user', |
|
'score' => 70, |
|
'department' => 'IT' |
|
]); |
|
$nonMatchingObject = $this->createSimpleObject([ |
|
'role' => 'user', |
|
'score' => 70, |
|
'department' => 'Finance' |
|
]); |
|
|
|
// When |
|
$matchResult1 = $spec->match($matchingObject1); |
|
$matchResult2 = $spec->match($matchingObject2); |
|
$matchResult3 = $spec->match($matchingObject3); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult1); |
|
$this->assertTrue($matchResult2); |
|
$this->assertTrue($matchResult3); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingEmptyOrGroupThenNeverMatches(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::or([])); |
|
$testObject = $this->createSimpleObject(['any' => 'value']); |
|
|
|
// When |
|
$result = $spec->match($testObject); |
|
|
|
// Then |
|
$this->assertFalse($result); |
|
} |
|
|
|
// NOT Group Tests |
|
|
|
public function testWhenUsingNotGroupThenInvertsCondition(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::not(Spec::eq('status', 'banned'))); |
|
$matchingObject = $this->createSimpleObject(['status' => 'active']); |
|
$nonMatchingObject = $this->createSimpleObject(['status' => 'banned']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
// Complex Nested Groups Tests |
|
|
|
public function testWhenUsingNestedAndOrGroupsThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::and([ |
|
Spec::eq('status', 'active'), |
|
Spec::or([ |
|
Spec::gte('score', 80), |
|
Spec::in('role', ['admin', 'moderator']) |
|
]) |
|
])); |
|
$matchingObject1 = $this->createSimpleObject([ |
|
'status' => 'active', |
|
'score' => 85, |
|
'role' => 'user' |
|
]); |
|
$matchingObject2 = $this->createSimpleObject([ |
|
'status' => 'active', |
|
'score' => 70, |
|
'role' => 'admin' |
|
]); |
|
$nonMatchingObject = $this->createSimpleObject([ |
|
'status' => 'inactive', |
|
'score' => 85, |
|
'role' => 'user' |
|
]); |
|
|
|
// When |
|
$matchResult1 = $spec->match($matchingObject1); |
|
$matchResult2 = $spec->match($matchingObject2); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult1); |
|
$this->assertTrue($matchResult2); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingTripleNestedGroupsThenMatchesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::and([ |
|
Spec::eq('active', true), |
|
Spec::not(Spec::or([ |
|
Spec::eq('role', 'banned'), |
|
Spec::eq('status', 'suspended') |
|
])) |
|
])); |
|
$matchingObject = $this->createSimpleObject([ |
|
'active' => true, |
|
'role' => 'user', |
|
'status' => 'active' |
|
]); |
|
$nonMatchingObject1 = $this->createSimpleObject([ |
|
'active' => false, |
|
'role' => 'user', |
|
'status' => 'active' |
|
]); |
|
$nonMatchingObject2 = $this->createSimpleObject([ |
|
'active' => true, |
|
'role' => 'banned', |
|
'status' => 'active' |
|
]); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult1 = $spec->match($nonMatchingObject1); |
|
$noMatchResult2 = $spec->match($nonMatchingObject2); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult1); |
|
$this->assertFalse($noMatchResult2); |
|
} |
|
|
|
// Edge Case Tests |
|
|
|
public function testWhenFieldDoesNotExistThenReturnsFalse(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('nonExistentField', 'value')); |
|
$testObject = $this->createSimpleObject(['existingField' => 'value']); |
|
|
|
// When |
|
$result = $spec->match($testObject); |
|
|
|
// Then |
|
$this->assertFalse($result); |
|
} |
|
|
|
public function testWhenFieldIsNullThenComparesCorrectly(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('optionalField', null)); |
|
$matchingObject = $this->createSimpleObject(['optionalField' => null]); |
|
$nonMatchingObject = $this->createSimpleObject(['optionalField' => 'value']); |
|
|
|
// When |
|
$matchResult = $spec->match($matchingObject); |
|
$noMatchResult = $spec->match($nonMatchingObject); |
|
|
|
// Then |
|
$this->assertTrue($matchResult); |
|
$this->assertFalse($noMatchResult); |
|
} |
|
|
|
public function testWhenUsingInvalidOperatorThenReturnsFalse(): void |
|
{ |
|
// Given |
|
$spec = new Specification(['field', 'INVALID_OP', 'value']); |
|
$testObject = $this->createSimpleObject(['field' => 'value']); |
|
|
|
// When |
|
$result = $spec->match($testObject); |
|
|
|
// Then |
|
$this->assertFalse($result); |
|
} |
|
|
|
public function testWhenUsingInvalidDateStringThenFallsBackToRegularComparison(): void |
|
{ |
|
// Given |
|
$spec = new Specification(Spec::eq('dateField', 'invalid-date')); |
|
$testObject = $this->createSimpleObject(['dateField' => 'invalid-date']); |
|
|
|
// When |
|
$result = $spec->match($testObject); |
|
|
|
// Then |
|
$this->assertTrue($result); |
|
} |
|
|
|
// Spec Helper Method Tests |
|
|
|
public function testWhenUsingSpecHelpersThenCreatesCorrectSpecificationArray(): void |
|
{ |
|
// Given |
|
$eqSpec = Spec::eq('field', 'value'); |
|
$neqSpec = Spec::neq('field', 'value'); |
|
$gtSpec = Spec::gt('field', 10); |
|
$gteSpec = Spec::gte('field', 10); |
|
$ltSpec = Spec::lt('field', 10); |
|
$lteSpec = Spec::lte('field', 10); |
|
$inSpec = Spec::in('field', ['a', 'b']); |
|
$ninSpec = Spec::nin('field', ['a', 'b']); |
|
|
|
// When & Then |
|
$this->assertEquals(['field', '=', 'value'], $eqSpec); |
|
$this->assertEquals(['field', '!=', 'value'], $neqSpec); |
|
$this->assertEquals(['field', '>', 10], $gtSpec); |
|
$this->assertEquals(['field', '>=', 10], $gteSpec); |
|
$this->assertEquals(['field', '<', 10], $ltSpec); |
|
$this->assertEquals(['field', '<=', 10], $lteSpec); |
|
$this->assertEquals(['field', 'IN', ['a', 'b']], $inSpec); |
|
$this->assertEquals(['field', 'NOT IN', ['a', 'b']], $ninSpec); |
|
} |
|
|
|
public function testWhenUsingLogicalGroupHelpersThenCreatesCorrectSpecificationArray(): void |
|
{ |
|
// Given |
|
$andSpec = Spec::and([Spec::eq('a', 1), Spec::eq('b', 2)]); |
|
$orSpec = Spec::or([Spec::eq('a', 1), Spec::eq('b', 2)]); |
|
$notSpec = Spec::not(Spec::eq('a', 1)); |
|
|
|
// When & Then |
|
$this->assertEquals(['AND' => [['a', '=', 1], ['b', '=', 2]]], $andSpec); |
|
$this->assertEquals(['OR' => [['a', '=', 1], ['b', '=', 2]]], $orSpec); |
|
$this->assertEquals(['NOT' => ['a', '=', 1]], $notSpec); |
|
} |
|
|
|
public function testGetSpecReturnsOriginalSpecification(): void |
|
{ |
|
// Given |
|
$originalSpec = Spec::eq('field', 'value'); |
|
$specification = new Specification($originalSpec); |
|
|
|
// When |
|
$retrievedSpec = $specification->getSpec(); |
|
|
|
// Then |
|
$this->assertEquals($originalSpec, $retrievedSpec); |
|
} |
|
} |
|
|
|
// Test helper class with getters for complex object testing |
|
class TestObject |
|
{ |
|
private array $data; |
|
|
|
public function __construct(array $data) |
|
{ |
|
$this->data = $data; |
|
} |
|
|
|
public function __get($name) |
|
{ |
|
return $this->data[$name] ?? null; |
|
} |
|
|
|
public function __isset($name) |
|
{ |
|
return isset($this->data[$name]); |
|
} |
|
|
|
public function getName() |
|
{ |
|
return $this->data['name'] ?? null; |
|
} |
|
|
|
public function getExpirationDate() |
|
{ |
|
return $this->data['expirationDate'] ?? null; |
|
} |
|
|
|
public function getCreatedAt() |
|
{ |
|
return $this->data['createdAt'] ?? null; |
|
} |
|
|
|
public function getScore() |
|
{ |
|
return $this->data['score'] ?? null; |
|
} |
|
|
|
public function getStatus() |
|
{ |
|
return $this->data['status'] ?? null; |
|
} |
|
|
|
public function getRole() |
|
{ |
|
return $this->data['role'] ?? null; |
|
} |
|
|
|
public function getAge() |
|
{ |
|
return $this->data['age'] ?? null; |
|
} |
|
|
|
public function getPrice() |
|
{ |
|
return $this->data['price'] ?? null; |
|
} |
|
|
|
public function getDepartment() |
|
{ |
|
return $this->data['department'] ?? null; |
|
} |
|
|
|
public function getActive() |
|
{ |
|
return $this->data['active'] ?? null; |
|
} |
|
|
|
public function getOptionalField() |
|
{ |
|
return $this->data['optionalField'] ?? null; |
|
} |
|
|
|
public function getDateField() |
|
{ |
|
return $this->data['dateField'] ?? null; |
|
} |
|
} |