ldc-arcadia-api/tests/Unit/Domain/Services/BrandResolverTest.php

240 lines
8.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Services;
use Domain\Events\BrandCreated;
use Domain\Exceptions\BrandNotFoundException;
use Domain\Models\Entities\Brand;
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
use Domain\Models\ValueObjects\Brand\ArcadiaId;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Brand\BrandId;
use Domain\Models\ValueObjects\Brand\BrandName;
use Domain\Services\BrandCodeGenerator;
use Domain\Services\BrandResolver;
use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory;
use Infra\Adapters\Repositories\InMemoryBrandRepository;
use Ramsey\Uuid\Uuid;
use Shared\DDD\Event\DomainEventDispatcher;
use Shared\DDD\Event\Event;
use Domain\Events\BrandRenamed;
covers(BrandResolver::class);
describe('BrandResolver Unit Tests', function (): void {
$arcadiaDirectory = new InMemoryArcadiaBrandDirectory();
$brandRepository = new InMemoryBrandRepository();
$codeGenerator = new BrandCodeGenerator();
beforeEach(function () use ($arcadiaDirectory, $brandRepository): void {
$arcadiaDirectory->clear();
$brandRepository->clear();
});
it('returns existing brand if already present in repository without dispatching event', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
// GIVEN
$existing = Brand::hydrate(
id: new BrandId(Uuid::uuid7()),
name: new BrandName('Le Gaulois'),
code: new BrandCode('LGL01'),
arcadiaId: new ArcadiaId('1'),
);
$brandRepository->add($existing);
$dispatcher = new class () implements DomainEventDispatcher
{
/** @var list<Event> */
public array $dispatched = [];
public function dispatch(Event $event): void
{
$this->dispatched[] = $event;
}
};
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
// WHEN
$resolved = $resolver->resolve(new ArcadiaId('1'));
// THEN
expect($resolved)->toBe($existing)
->and($dispatcher->dispatched)->toBeEmpty();
});
it('generates, persists and dispatches BrandCreated event when brand is not in repository but in Arcadia directory', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
// GIVEN
$arcadiaDirectory->add(new ArcadiaBrand(new ArcadiaId('2'), new BrandName('Maître CoQ')));
$dispatcher = new class () implements DomainEventDispatcher
{
/** @var list<Event> */
public array $dispatched = [];
public function dispatch(Event $event): void
{
$this->dispatched[] = $event;
}
};
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
// WHEN
$resolved = $resolver->resolve(new ArcadiaId('2'));
// THEN
expect($resolved->arcadiaId->value)->toBe('2')
->and($resolved->name->value)->toBe('Maître Coq')
->and(str_starts_with($resolved->code->value, 'MCQ'))->toBeTrue();
expect($brandRepository->findByArcadiaId(new ArcadiaId('2')))->not->toBeNull();
expect($dispatcher->dispatched)->toHaveCount(1)
->and($dispatcher->dispatched[0])->toBeInstanceOf(BrandCreated::class);
/** @var BrandCreated $event */
$event = $dispatcher->dispatched[0];
expect($event->arcadiaId->value)->toBe('2')
->and($event->brandName->value)->toBe('Maître Coq')
->and($event->brandCode->value)->toBe($resolved->code->value)
->and($event->brandId->value)->toBe($resolved->id->value);
});
it('throws BrandNotFoundException when resolving an unknown ArcadiaId', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
// GIVEN
$dispatcher = new class () implements DomainEventDispatcher
{
public function dispatch(Event $event): void
{
}
};
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
// WHEN & THEN
expect(fn () => $resolver->resolve(new ArcadiaId('999')))
->toThrow(BrandNotFoundException::class, 'Arcadia brand with ID 999 not found.');
});
it('returns existing brand directly without querying Arcadia directory when already in repository', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
// GIVEN
$brandId = new BrandId(Uuid::uuid7());
$oldBrand = Brand::hydrate(
id: $brandId,
name: new BrandName('Le Gaulois'),
code: new BrandCode('LGL01'),
arcadiaId: new ArcadiaId('1'),
);
$brandRepository->add($oldBrand);
// Arcadia directory has a different name, but resolve() prioritizes local repository without extra Oracle calls
$arcadiaDirectory->add(new ArcadiaBrand(new ArcadiaId('1'), new BrandName('Le Gaulois France')));
$dispatcher = new class () implements DomainEventDispatcher
{
/** @var list<Event> */
public array $dispatched = [];
public function dispatch(Event $event): void
{
$this->dispatched[] = $event;
}
};
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
// WHEN
$resolved = $resolver->resolve(new ArcadiaId('1'));
// THEN
expect($resolved)->toBe($oldBrand)
->and($resolved->name->value)->toBe('Le Gaulois')
->and($dispatcher->dispatched)->toBeEmpty();
});
it('preserves and returns existing brand when brand was removed from Arcadia directory', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
// GIVEN
$brandId = new BrandId(Uuid::uuid7());
$existing = Brand::hydrate(
id: $brandId,
name: new BrandName('Historical Brand'),
code: new BrandCode('HST01'),
arcadiaId: new ArcadiaId('77'),
);
$brandRepository->add($existing);
// Arcadia directory does NOT have brand 77 (deleted/purged amont)
// $arcadiaDirectory is empty for 77
$dispatcher = new class () implements DomainEventDispatcher
{
/** @var list<Event> */
public array $dispatched = [];
public function dispatch(Event $event): void
{
$this->dispatched[] = $event;
}
};
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
// WHEN
$resolved = $resolver->resolve(new ArcadiaId('77'));
// THEN
expect($resolved)->toBe($existing)
->and($resolved->name->value)->toBe('Historical Brand')
->and($dispatcher->dispatched)->toBeEmpty();
});
it('updates brand name, persists and dispatches BrandRenamed when resolving from ArcadiaBrand with changed name', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
// GIVEN
$brandId = new BrandId(Uuid::uuid7());
$oldBrand = Brand::hydrate(
id: $brandId,
name: new BrandName('Le Gaulois'),
code: new BrandCode('LGL01'),
arcadiaId: new ArcadiaId('1'),
);
$brandRepository->add($oldBrand);
$renamedArcadia = new ArcadiaBrand(new ArcadiaId('1'), new BrandName('Le Gaulois France'));
$dispatcher = new class () implements DomainEventDispatcher
{
/** @var list<Event> */
public array $dispatched = [];
public function dispatch(Event $event): void
{
$this->dispatched[] = $event;
}
};
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
// WHEN
$resolved = $resolver->resolveFromArcadia($renamedArcadia);
// THEN
expect($resolved->name->value)->toBe('Le Gaulois France')
->and($resolved->id()->value)->toBe($brandId->value)
->and($resolved->code->value)->toBe('LGL01');
$persisted = $brandRepository->findByArcadiaId(new ArcadiaId('1'));
expect($persisted)->not->toBeNull()
->and($persisted->name->value)->toBe('Le Gaulois France');
expect($dispatcher->dispatched)->toHaveCount(1)
->and($dispatcher->dispatched[0])->toBeInstanceOf(BrandRenamed::class);
/** @var \Domain\Events\BrandRenamed $event */
$event = $dispatcher->dispatched[0];
expect($event->oldName->value)->toBe('Le Gaulois')
->and($event->newName->value)->toBe('Le Gaulois France')
->and($event->brandId->value)->toBe($brandId->value);
});
});