|
2 | 2 |
|
3 | 3 | declare(strict_types=1); |
4 | 4 |
|
| 5 | +use ComplexHeart\Domain\Model\Contracts\Aggregatable; |
5 | 6 | use ComplexHeart\Domain\Model\Errors\ImmutabilityError; |
6 | 7 | use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; |
7 | 8 | use ComplexHeart\Domain\Model\Test\Fixtures\OrderManagement\Domain\Errors\InvalidPriceError; |
|
17 | 18 |
|
18 | 19 | test('Object with HasImmutability should expose primitive values.', function () { |
19 | 20 | $price = new Price(100.0, 'EUR'); |
20 | | - expect($price->amount)->toBeFloat(); |
21 | | - expect($price->currency)->toBeString(); |
| 21 | + expect($price->amount)->toBeFloat() |
| 22 | + ->and($price->currency)->toBeString(); |
22 | 23 | }) |
23 | 24 | ->group('Unit'); |
24 | 25 |
|
25 | 26 | test('Object with HasImmutability should return new instance with override values.', function () { |
26 | 27 | $price = new Price(100.0, 'EUR'); |
27 | 28 | $newPrice = $price->applyDiscount(10.0); |
28 | 29 |
|
29 | | - expect($newPrice)->toBeInstanceOf(Price::class); |
30 | | - expect($newPrice->amount)->toBe(90.0); |
| 30 | + expect($newPrice)->toBeInstanceOf(Price::class) |
| 31 | + ->and($newPrice->amount)->toBe(90.0); |
31 | 32 | }) |
32 | 33 | ->group('Unit'); |
33 | 34 |
|
@@ -165,3 +166,112 @@ protected function invariantSingleFailure(): bool |
165 | 166 | ->and($exception->getMessage())->toBe('Single error message'); |
166 | 167 | }) |
167 | 168 | ->group('Unit'); |
| 169 | + |
| 170 | +test('Custom non-aggregatable exception should be thrown immediately', function () { |
| 171 | + new class () { |
| 172 | + use HasInvariants; |
| 173 | + |
| 174 | + public function __construct() |
| 175 | + { |
| 176 | + $this->check(); |
| 177 | + } |
| 178 | + |
| 179 | + protected function invariantCustomError(): bool |
| 180 | + { |
| 181 | + throw new DomainException('Custom domain error'); |
| 182 | + } |
| 183 | + }; |
| 184 | +}) |
| 185 | + ->group('Unit') |
| 186 | + ->throws(DomainException::class, 'Custom domain error'); |
| 187 | + |
| 188 | +test('Custom non-aggregatable exception stops invariant checking', function () { |
| 189 | + new class () { |
| 190 | + use HasInvariants; |
| 191 | + |
| 192 | + public function __construct() |
| 193 | + { |
| 194 | + $this->check(); |
| 195 | + } |
| 196 | + |
| 197 | + protected function invariantFirstCheck(): bool |
| 198 | + { |
| 199 | + // This should throw immediately |
| 200 | + throw new RuntimeException('First error'); |
| 201 | + } |
| 202 | + |
| 203 | + protected function invariantSecondCheck(): bool |
| 204 | + { |
| 205 | + // This should NEVER be reached |
| 206 | + throw new DomainException('Second error - should not be reached'); |
| 207 | + } |
| 208 | + }; |
| 209 | +}) |
| 210 | + ->group('Unit') |
| 211 | + ->throws(RuntimeException::class, 'First error'); |
| 212 | + |
| 213 | +test('Custom aggregatable exception should be aggregated', function () { |
| 214 | + try { |
| 215 | + new class () { |
| 216 | + use HasInvariants; |
| 217 | + |
| 218 | + public function __construct() |
| 219 | + { |
| 220 | + $this->check(); |
| 221 | + } |
| 222 | + |
| 223 | + protected function invariantFirst(): bool |
| 224 | + { |
| 225 | + // Create aggregatable exception inline |
| 226 | + $exception = new class ('Aggregatable error') extends \Exception implements Aggregatable {}; |
| 227 | + throw $exception; |
| 228 | + } |
| 229 | + |
| 230 | + protected function invariantSecond(): bool |
| 231 | + { |
| 232 | + return false; // Regular InvariantViolation |
| 233 | + } |
| 234 | + }; |
| 235 | + } catch (InvariantViolation $e) { |
| 236 | + expect($e->hasMultipleViolations())->toBeTrue() |
| 237 | + ->and($e->getViolationCount())->toBe(2) |
| 238 | + ->and($e->getViolations())->toContain('Aggregatable error') |
| 239 | + ->and($e->getViolations())->toContain('second'); |
| 240 | + } |
| 241 | +}) |
| 242 | + ->group('Unit'); |
| 243 | + |
| 244 | +test('InvariantViolation implements Aggregatable', function () { |
| 245 | + $exception = InvariantViolation::fromViolations(['Test']); |
| 246 | + |
| 247 | + expect($exception)->toBeInstanceOf(Aggregatable::class); |
| 248 | +}) |
| 249 | + ->group('Unit'); |
| 250 | + |
| 251 | +test('Mix of custom non-aggregatable throws immediately before aggregation', function () { |
| 252 | + new class () { |
| 253 | + use HasInvariants; |
| 254 | + |
| 255 | + public function __construct() |
| 256 | + { |
| 257 | + $this->check(); |
| 258 | + } |
| 259 | + |
| 260 | + protected function invariantFirstAggregatable(): bool |
| 261 | + { |
| 262 | + return false; // InvariantViolation |
| 263 | + } |
| 264 | + |
| 265 | + protected function invariantCustomNonAggregatable(): bool |
| 266 | + { |
| 267 | + throw new RuntimeException('Non-aggregatable error'); |
| 268 | + } |
| 269 | + |
| 270 | + protected function invariantThirdAggregatable(): bool |
| 271 | + { |
| 272 | + return false; // Should not be reached |
| 273 | + } |
| 274 | + }; |
| 275 | +}) |
| 276 | + ->group('Unit') |
| 277 | + ->throws(RuntimeException::class, 'Non-aggregatable error'); |
0 commit comments