From e83a286e9bbbfca07e938c7188231143fe26df65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Mon, 14 Sep 2026 19:13:41 +0200 Subject: [PATCH] test(arch): overhaul and complete architecture test suite across all layers --- src/Infra/Providers/AppServiceProvider.php | 2 +- src/Infra/Providers/DomainServiceProvider.php | 2 +- src/Infra/Providers/EventServiceProvider.php | 2 +- tests/Architecture/ApplicationTest.php | 131 +++++++++++------- tests/Architecture/CodingStandardsTest.php | 43 +++--- tests/Architecture/DomainTest.php | 50 +++++-- tests/Architecture/InfraTest.php | 61 +++++++- tests/Architecture/NamingConventionsTest.php | 81 ++++++++++- tests/Architecture/PresentationTest.php | 48 ++++++- tests/Architecture/SharedTest.php | 31 +++++ tests/Pest.php | 7 +- .../Infra/Adapters/Mappers/IncoMapperTest.php | 22 +-- .../Listeners/InvalidateBrandCacheTest.php | 25 ++-- 13 files changed, 380 insertions(+), 125 deletions(-) diff --git a/src/Infra/Providers/AppServiceProvider.php b/src/Infra/Providers/AppServiceProvider.php index 71a1299..99ee9f0 100644 --- a/src/Infra/Providers/AppServiceProvider.php +++ b/src/Infra/Providers/AppServiceProvider.php @@ -19,7 +19,7 @@ use Infra\Services\Health\LaravelSystemHealthChecker; use Shared\Pagination; use Uri\Rfc3986\Uri; -class AppServiceProvider extends ServiceProvider +final class AppServiceProvider extends ServiceProvider { #[\Override] public function register(): void diff --git a/src/Infra/Providers/DomainServiceProvider.php b/src/Infra/Providers/DomainServiceProvider.php index d8809f0..9d5db1a 100644 --- a/src/Infra/Providers/DomainServiceProvider.php +++ b/src/Infra/Providers/DomainServiceProvider.php @@ -34,7 +34,7 @@ use Shared\DDD\Event\TransactionalDomainEventDispatcher; use Shared\DDD\Usecase\Builder\UsecaseExecutor; use Webmozart\Assert\Assert; -class DomainServiceProvider extends ServiceProvider +final class DomainServiceProvider extends ServiceProvider { #[\Override] public function register(): void diff --git a/src/Infra/Providers/EventServiceProvider.php b/src/Infra/Providers/EventServiceProvider.php index b710148..ce50b25 100644 --- a/src/Infra/Providers/EventServiceProvider.php +++ b/src/Infra/Providers/EventServiceProvider.php @@ -19,7 +19,7 @@ use Infra\Listeners\LogClientStatusChanged; use Infra\Listeners\RecordClientSecurityAudit; use Infra\Listeners\SendClientSecurityAlertMail; -class EventServiceProvider extends ServiceProvider +final class EventServiceProvider extends ServiceProvider { public function boot(): void { diff --git a/tests/Architecture/ApplicationTest.php b/tests/Architecture/ApplicationTest.php index ca9645b..730885a 100644 --- a/tests/Architecture/ApplicationTest.php +++ b/tests/Architecture/ApplicationTest.php @@ -4,48 +4,13 @@ declare(strict_types=1); namespace Tests\Architecture; -/** @return array */ -function enumsIn(string $namespace): array -{ - $dir = dirname(__DIR__, 2) . '/src/' . str_replace('\\', '/', $namespace); - $enums = []; - if (! is_dir($dir)) { - return $enums; - } - - foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)) as $file) { - $class = $namespace . str_replace([$dir, '.php', '/'], ['', '', '\\'], $file->getPathname()); - if ($file->isFile() && enum_exists($class)) { - $enums[] = $class; - } - } - - return $enums; -} - -/** @return array */ -function valueObjectsIn(string $baseNamespace): array -{ - $dir = dirname(__DIR__, 2) . '/src/' . str_replace('\\', '/', $baseNamespace); - $namespaces = []; - if (! is_dir($dir)) { - return $namespaces; - } - - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::SELF_FIRST - ); - - foreach ($iterator as $item) { - if ($item->isDir() && $item->getBasename() === 'ValueObjects') { - $relativePath = str_replace([$dir, '/'], ['', '\\'], $item->getPathname()); - $namespaces[] = $baseNamespace . $relativePath; - } - } - - return $namespaces; -} +use Application\Security\Models\ValueObjects\Request\HttpMethod; +use Shared\DDD\Event\Event; +use Shared\DDD\Port\Directory; +use Shared\DDD\Port\Repository; +use Shared\DDD\Usecase\Input; +use Shared\DDD\Usecase\Output; +use Shared\DDD\Usecase\Usecase; describe('Application Architecture Rules', function (): void { arch('Application layer does not depend on Presentation, Infra, or App') @@ -105,11 +70,82 @@ describe('Application Architecture Rules', function (): void { ->toUseStrictTypes(); arch('Application Usecase classes are final and readonly') - ->expect('Application\Arcadia\Usecase') + ->expect([ + 'Application\Arcadia\Usecase', + 'Application\Security\Usecases', + ]) + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Classes implementing Shared\DDD\Usecase\Usecase are final and readonly') + ->expect(Usecase::class) ->classes() ->toBeReadonly() ->toBeFinal() - ->ignoring(enumsIn('Application\Arcadia\Usecase')); + ->ignoring(Usecase::class); + + arch('Application Input DTOs implement Shared\DDD\Usecase\Input and are final and readonly') + ->expect(Input::class) + ->classes() + ->toBeReadonly() + ->toBeFinal() + ->ignoring(Input::class); + + arch('Application Output DTOs implement Shared\DDD\Usecase\Output and are final and readonly') + ->expect(Output::class) + ->classes() + ->toBeReadonly() + ->toBeFinal() + ->ignoring(Output::class); + + arch('Application Health DTOs are final and readonly') + ->expect('Application\Health\Dto') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Application Config classes are final and readonly') + ->expect([ + 'Application\Arcadia\Config', + 'Application\Health\Config', + 'Application\Security\Config', + ]) + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Application Port Directories and Repositories contain only interfaces') + ->expect([ + 'Application\Security\Ports\Directories', + 'Application\Security\Ports\Repositories', + ]) + ->classes() + ->toBeInterface(); + + arch('Application Port Directories extend Shared\DDD\Port\Directory') + ->expect('Application\Security\Ports\Directories') + ->toExtend(Directory::class); + + arch('Application Port Repositories extend Shared\DDD\Port\Repository') + ->expect('Application\Security\Ports\Repositories') + ->toExtend(Repository::class); + + arch('Application Events implement Shared\DDD\Event\Event and are final and readonly') + ->expect('Application\Security\Events') + ->toImplement(Event::class) + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Application Security Value Objects are final and readonly') + ->expect('Application\Security\Models\ValueObjects') + ->classes() + ->toBeReadonly() + ->toBeFinal() + ->ignoring([ + HttpMethod::class, + ]); arch('Application layer must not contain Presenters') ->expect('Application') @@ -120,11 +156,4 @@ describe('Application Architecture Rules', function (): void { ->expect('Application') ->classes() ->not->toHaveSuffix('View'); - - arch('Application Value Objects are final and readonly') - ->expect(valueObjectsIn('Application')) - ->classes() - ->toBeReadonly() - ->toBeFinal() - ->ignoring(enumsIn('Application')); }); diff --git a/tests/Architecture/CodingStandardsTest.php b/tests/Architecture/CodingStandardsTest.php index e5c2506..556d464 100644 --- a/tests/Architecture/CodingStandardsTest.php +++ b/tests/Architecture/CodingStandardsTest.php @@ -4,34 +4,35 @@ declare(strict_types=1); namespace Tests\Architecture; -/** - * @param array $directories - * @return array - */ -function getTestFiles(array $directories): array -{ - $files = []; - foreach ($directories as $dir) { - $path = dirname(__DIR__, 2) . '/' . ltrim($dir, '/'); - if (! is_dir($path)) { - continue; - } - $dirIterator = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); - $iterator = new \RecursiveIteratorIterator($dirIterator); - /** @var \SplFileInfo $file */ - foreach ($iterator as $file) { - if ($file->isFile() && str_ends_with($file->getFilename(), 'Test.php')) { - $files[] = $file->getPathname(); +if (! function_exists('Tests\Architecture\getTestFiles')) { + /** + * @param array $directories + * @return array + */ + function getTestFiles(array $directories): array + { + $files = []; + foreach ($directories as $dir) { + $path = dirname(__DIR__, 2) . '/' . ltrim($dir, '/'); + if (! is_dir($path)) { + continue; + } + $dirIterator = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new \RecursiveIteratorIterator($dirIterator); + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + if ($file->isFile() && str_ends_with($file->getFilename(), 'Test.php')) { + $files[] = $file->getPathname(); + } } } - } - return $files; + return $files; + } } describe('Coding Standards & Project Rules', function (): void { arch()->preset()->php(); - // Le helper assert natif est interdit en faveur de Webmozart\Assert arch()->preset()->security(); arch('assert is not used in src (use Webmozart\Assert instead)') diff --git a/tests/Architecture/DomainTest.php b/tests/Architecture/DomainTest.php index 57b20be..87a5de9 100644 --- a/tests/Architecture/DomainTest.php +++ b/tests/Architecture/DomainTest.php @@ -4,6 +4,10 @@ declare(strict_types=1); namespace Tests\Architecture; +use Domain\Models\ValueObjects\Employees\EmployeeSortColumn; +use Domain\Models\ValueObjects\Inco\IncoSheetSortColumn; +use Domain\Models\ValueObjects\Inco\PortionType; +use Shared\DDD\Event\Event; use Shared\DDD\Model\AggregateRoot; use Shared\DDD\Model\Entity; use Shared\DDD\Model\ReadModel; @@ -33,6 +37,39 @@ describe('Domain Architecture Rules', function (): void { ->expect('Domain\Ports\Repositories') ->toExtend(Repository::class); + arch('Domain Value Objects are final and readonly') + ->expect('Domain\Models\ValueObjects') + ->classes() + ->toBeReadonly() + ->toBeFinal() + ->ignoring([ + EmployeeSortColumn::class, + IncoSheetSortColumn::class, + PortionType::class, + ]); + + arch('Domain Events implement Shared\DDD\Event\Event and are final and readonly') + ->expect('Domain\Events') + ->toImplement(Event::class) + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Domain Builders are final') + ->expect('Domain\Builders') + ->classes() + ->toBeFinal(); + + arch('Domain Services are final and readonly') + ->expect('Domain\Services') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Domain exceptions extend native DomainException') + ->expect('Domain\Exceptions') + ->toExtend(\DomainException::class); + arch('Domain does not depend on Application, Presentation, Infra, or App') ->expect('Domain') ->not->toUse([ @@ -50,15 +87,6 @@ describe('Domain Architecture Rules', function (): void { 'Symfony', ]); - arch('Domain Value Objects are readonly') - ->expect('Domain\Models\ValueObjects') - ->classes() - ->toBeReadonly(); - - arch('Domain exceptions extend native DomainException') - ->expect('Domain\Exceptions') - ->toExtend(\DomainException::class); - arch('Domain does not use Laravel helper functions') ->expect('Domain') ->not->toUse([ @@ -94,6 +122,10 @@ describe('Domain Architecture Rules', function (): void { 'trans_choice', 'view', ]); + + arch('Domain layer uses strict types') + ->expect('Domain') + ->toUseStrictTypes(); }); describe('AggregateRoot & ReadModel Architecture Rules', function (): void { diff --git a/tests/Architecture/InfraTest.php b/tests/Architecture/InfraTest.php index 1b7616f..117e3ea 100644 --- a/tests/Architecture/InfraTest.php +++ b/tests/Architecture/InfraTest.php @@ -4,19 +4,72 @@ declare(strict_types=1); namespace Tests\Architecture; +use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\ServiceProvider; +use Shared\DDD\Port\Directory; +use Shared\DDD\Port\Repository; describe('Infra Architecture Rules', function (): void { arch('Infra does not depend on App namespace') ->expect('Infra') ->not->toUse('App'); - arch('Infra Eloquent Database Models extend Eloquent Model') + arch('Infra Eloquent Database Models extend Eloquent Model and are final') ->expect('Infra\Database\Models') - ->toExtend(Model::class); + ->toExtend(Model::class) + ->classes() + ->toBeFinal(); - arch('Infra Eloquent Database Models are final') - ->expect('Infra\Database\Models') + arch('Infra Service Providers extend ServiceProvider and are final') + ->expect('Infra\Providers') + ->toExtend(ServiceProvider::class) + ->classes() + ->toBeFinal(); + + arch('Infra Casts implement CastsAttributes and are final') + ->expect('Infra\Casts') + ->toImplement(CastsAttributes::class) + ->classes() + ->toBeFinal(); + + arch('Infra directory adapters implement Directory port and are final') + ->expect('Infra\Adapters\Directories') + ->toImplement(Directory::class) + ->classes() + ->toBeFinal(); + + arch('Infra repository adapters implement Repository port and are final') + ->expect('Infra\Adapters\Repositories') + ->toImplement(Repository::class) + ->classes() + ->toBeFinal(); + + arch('Infra Mappers are final and readonly') + ->expect('Infra\Adapters\Mappers') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Infra Parsers are final and readonly') + ->expect('Infra\Adapters\Parsers') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Infra Jobs are final') + ->expect('Infra\Jobs') + ->classes() + ->toBeFinal(); + + arch('Infra Listeners are final') + ->expect('Infra\Listeners') + ->classes() + ->toBeFinal(); + + arch('Infra Mailables are final') + ->expect('Infra\Mail') + ->classes() ->toBeFinal(); arch('Infra layer uses strict types') diff --git a/tests/Architecture/NamingConventionsTest.php b/tests/Architecture/NamingConventionsTest.php index ffed59a..48f813d 100644 --- a/tests/Architecture/NamingConventionsTest.php +++ b/tests/Architecture/NamingConventionsTest.php @@ -4,9 +4,15 @@ declare(strict_types=1); namespace Tests\Architecture; +use Application\Security\Config\DevClientCredentials; +use Illuminate\Console\Command; +use Illuminate\Contracts\Database\Eloquent\CastsAttributes; +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\ServiceProvider; use Presentation\Api\V1\Contracts\ApiResponse; use Shared\DDD\Port\Directory; use Shared\DDD\Port\Repository; +use Shared\DDD\Usecase\Presenter; describe('Naming Conventions in Domain', function (): void { arch('Port Directory interfaces have Directory suffix') @@ -16,34 +22,80 @@ describe('Naming Conventions in Domain', function (): void { arch('Port Repository interfaces have Repository suffix') ->expect('Domain\Ports\Repositories') ->toHaveSuffix('Repository'); + + arch('Domain Builders have Builder suffix') + ->expect('Domain\Builders') + ->toHaveSuffix('Builder'); + + arch('Domain Exceptions have Exception suffix') + ->expect('Domain\Exceptions') + ->toHaveSuffix('Exception'); }); describe('Naming Conventions in Application', function (): void { - arch('Application usecases use strict types') - ->expect('Application\Arcadia\Usecase') - ->toUseStrictTypes(); + arch('Application Port Directory interfaces have Directory suffix') + ->expect('Application\Security\Ports\Directories') + ->toHaveSuffix('Directory'); + + arch('Application Port Repository interfaces have Repository suffix') + ->expect('Application\Security\Ports\Repositories') + ->toHaveSuffix('Repository'); + + arch('Application Config classes have Config suffix') + ->expect([ + 'Application\Arcadia\Config', + 'Application\Health\Config', + 'Application\Security\Config', + ]) + ->classes() + ->toHaveSuffix('Config') + ->ignoring([ + DevClientCredentials::class, + ]); + + arch('Application Exception classes have Exception suffix') + ->expect([ + 'Application\Arcadia\Exceptions', + 'Application\Health\Exceptions', + 'Application\Security\Exceptions', + ]) + ->toHaveSuffix('Exception'); }); describe('Naming Conventions in Presentation', function (): void { arch('Controllers have Controller suffix') - ->expect('Presentation\Api\V1\Http\Controllers') + ->expect([ + 'Presentation\Api\V1\Http\Controllers', + 'Presentation\Web\Http\Controllers', + ]) ->toHaveSuffix('Controller'); arch('Requests extending FormRequest must have Request suffix') ->expect('Presentation\Api\V1\Http\Requests') - ->toExtend('Illuminate\Foundation\Http\FormRequest') + ->toExtend(FormRequest::class) ->toHaveSuffix('Request'); arch('Responses implementing ApiResponse must have Response suffix') ->expect('Presentation\Api\V1\Http\Responses') ->toImplement(ApiResponse::class) ->toHaveSuffix('Response'); + + arch('Console Commands extending Command must have Command suffix') + ->expect('Presentation\Console\Commands') + ->toExtend(Command::class) + ->toHaveSuffix('Command'); + + arch('Classes implementing Presenter must have Presenter suffix') + ->expect(Presenter::class) + ->classes() + ->toHaveSuffix('Presenter') + ->ignoring(Presenter::class); }); describe('Naming Conventions in Infra', function (): void { arch('Infra classes extending ServiceProvider must have Provider suffix') ->expect('Infra\Providers') - ->toExtend('Illuminate\Support\ServiceProvider') + ->toExtend(ServiceProvider::class) ->toHaveSuffix('Provider'); arch('Infra directory adapters implementing Directory port must have Directory suffix') @@ -55,4 +107,21 @@ describe('Naming Conventions in Infra', function (): void { ->expect('Infra\Adapters\Repositories') ->toImplement(Repository::class) ->toHaveSuffix('Repository'); + + arch('Infra Mappers must have Mapper suffix') + ->expect('Infra\Adapters\Mappers') + ->toHaveSuffix('Mapper'); + + arch('Infra Parsers must have Parser suffix') + ->expect('Infra\Adapters\Parsers') + ->toHaveSuffix('Parser'); + + arch('Infra Casts implementing CastsAttributes must have Cast suffix') + ->expect('Infra\Casts') + ->toImplement(CastsAttributes::class) + ->toHaveSuffix('Cast'); + + arch('Infra Jobs must have Job suffix') + ->expect('Infra\Jobs') + ->toHaveSuffix('Job'); }); diff --git a/tests/Architecture/PresentationTest.php b/tests/Architecture/PresentationTest.php index 04cfa8b..f91994e 100644 --- a/tests/Architecture/PresentationTest.php +++ b/tests/Architecture/PresentationTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Architecture; +use Illuminate\Foundation\Http\FormRequest; use Presentation\Api\V1\Contracts\ApiResponse; describe('Presentation Architecture Rules', function (): void { @@ -15,17 +16,27 @@ describe('Presentation Architecture Rules', function (): void { 'App', ]); - arch('Presentation Controllers are invokable single action controllers') - ->expect('Presentation\Api\V1\Http\Controllers') - ->toBeInvokable(); + arch('Presentation Controllers are invokable single action controllers and are final') + ->expect([ + 'Presentation\Api\V1\Http\Controllers', + 'Presentation\Web\Http\Controllers', + ]) + ->toBeInvokable() + ->classes() + ->toBeFinal(); - arch('Presentation Responses implement Presentation\Api\V1\Contracts\ApiResponse') + arch('Presentation Responses implement Presentation\Api\V1\Contracts\ApiResponse and are final and readonly') ->expect('Presentation\Api\V1\Http\Responses') - ->toImplement(ApiResponse::class); + ->toImplement(ApiResponse::class) + ->classes() + ->toBeReadonly() + ->toBeFinal(); - arch('Presentation FormRequests extend Illuminate\Foundation\Http\FormRequest') + arch('Presentation FormRequests extend Illuminate\Foundation\Http\FormRequest and are final') ->expect('Presentation\Api\V1\Http\Requests') - ->toExtend('Illuminate\Foundation\Http\FormRequest'); + ->toExtend(FormRequest::class) + ->classes() + ->toBeFinal(); arch('Presentation FormRequests do not contain toInput method') ->expect('Presentation\Api\V1\Http\Requests') @@ -35,6 +46,29 @@ describe('Presentation Architecture Rules', function (): void { ->expect('Presentation\Api\V1\Http\Requests') ->not->toHaveMethod('authorize'); + arch('Presentation DTOs are final and readonly') + ->expect('Presentation\Api\V1\Dto') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Presentation Middlewares are final') + ->expect('Presentation\Api\V1\Http\Middlewares') + ->classes() + ->toBeFinal(); + + arch('Presentation Console Commands extend Illuminate\Console\Command and are final') + ->expect('Presentation\Console\Commands') + ->toExtend('Illuminate\Console\Command') + ->classes() + ->toBeFinal(); + + arch('Presentation Console Presenters are final and readonly') + ->expect('Presentation\Console\Presenters') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + arch('Presentation layer uses strict types') ->expect('Presentation') ->toUseStrictTypes(); diff --git a/tests/Architecture/SharedTest.php b/tests/Architecture/SharedTest.php index 90c0595..7781903 100644 --- a/tests/Architecture/SharedTest.php +++ b/tests/Architecture/SharedTest.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Tests\Architecture; +use Shared\DDD\Model\EntityId; + describe('Shared Architecture Rules', function (): void { arch('Shared layer does not depend on Domain, Application, Presentation, Infra, or App') ->expect('Shared') @@ -59,6 +61,35 @@ describe('Shared Architecture Rules', function (): void { 'view', ]); + arch('Shared Value Objects are final and readonly') + ->expect('Shared\ValueObjects') + ->classes() + ->toBeReadonly() + ->toBeFinal(); + + arch('Shared DDD Ports contain only interfaces') + ->expect('Shared\DDD\Port') + ->classes() + ->toBeInterface(); + + arch('Shared DDD Models contain interfaces') + ->expect('Shared\DDD\Model') + ->classes() + ->toBeInterface() + ->ignoring([ + EntityId::class, + ]); + + arch('Shared DDD Events contain only interfaces') + ->expect('Shared\DDD\Event') + ->classes() + ->toBeInterface(); + + arch('Shared DDD Security contracts contain only interfaces') + ->expect('Shared\DDD\Security') + ->classes() + ->toBeInterface(); + arch('Shared layer uses strict types') ->expect('Shared') ->toUseStrictTypes(); diff --git a/tests/Pest.php b/tests/Pest.php index 2c4a235..2967fa3 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -10,11 +10,8 @@ use Tests\TestCase as LaravelTestCase; BypassFinals::enable(bypassReadOnly: false, bypassFinal: true); BypassFinals::denyPaths([ - '*/Usecase/*', - '*/Dto/*', - '*/Models/*', - '*/Shared/DDD/*', - '*/ValueObjects/*', + '*/src/*', + '*/tests/*', ]); pest() diff --git a/tests/Unit/Infra/Adapters/Mappers/IncoMapperTest.php b/tests/Unit/Infra/Adapters/Mappers/IncoMapperTest.php index 5ea1486..4af28c5 100644 --- a/tests/Unit/Infra/Adapters/Mappers/IncoMapperTest.php +++ b/tests/Unit/Infra/Adapters/Mappers/IncoMapperTest.php @@ -4,15 +4,19 @@ declare(strict_types=1); namespace Tests\Unit\Infra\Adapters\Mappers; -use Domain\Models\Entities\Brand; use Domain\Models\Entities\Inco\IncoSheet; 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\BrandResolver; use Infra\Adapters\Mappers\IncoMapper; use PHPUnit\Framework\TestCase; +use Domain\Models\Entities\Brand; +use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\Brand\BrandId; +use Domain\Services\BrandCodeGenerator; +use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory; +use Infra\Adapters\Repositories\InMemoryBrandRepository; +use Shared\DDD\Event\DomainEventDispatcher; use Ramsey\Uuid\Uuid; covers(IncoMapper::class); @@ -21,17 +25,19 @@ uses(TestCase::class); describe('IncoMapper', function (): void { it('maps an array row to complete IncoSheet entity', function (): void { - $brandResolver = $this->createMock(BrandResolver::class); - $mockBrand = Brand::hydrate( + $arcadiaDirectory = new InMemoryArcadiaBrandDirectory(); + $brandRepository = new InMemoryBrandRepository(); + $brand = Brand::hydrate( id: new BrandId(Uuid::fromString('018e6e5b-1111-7000-8000-000000000001')), name: new BrandName('Le Gaulois'), code: new BrandCode('LGL01'), arcadiaId: new ArcadiaId('1'), ); + $brandRepository->add($brand); - $brandResolver->expects($this->once()) - ->method('resolve') - ->willReturn($mockBrand); + $brandCodeGenerator = new BrandCodeGenerator(); + $dispatcher = \Mockery::mock(DomainEventDispatcher::class); + $brandResolver = new BrandResolver($arcadiaDirectory, $brandRepository, $brandCodeGenerator, $dispatcher); $mapper = new IncoMapper($brandResolver); diff --git a/tests/Unit/Infra/Listeners/InvalidateBrandCacheTest.php b/tests/Unit/Infra/Listeners/InvalidateBrandCacheTest.php index 91614f7..1ac7822 100644 --- a/tests/Unit/Infra/Listeners/InvalidateBrandCacheTest.php +++ b/tests/Unit/Infra/Listeners/InvalidateBrandCacheTest.php @@ -14,6 +14,8 @@ use Infra\Cache\BrandCache; use Infra\Listeners\InvalidateBrandCache; use Mockery; use Ramsey\Uuid\Uuid; +use Psr\Log\LoggerInterface; +use Psr\SimpleCache\CacheInterface; covers(InvalidateBrandCache::class); @@ -28,16 +30,16 @@ describe('InvalidateBrandCache Listener Unit Tests', function (): void { occurredAt: new \DateTimeImmutable(), ); - /** @var BrandCache&Mockery\MockInterface $brandCacheMock */ - $brandCacheMock = Mockery::mock(BrandCache::class); - $brandCacheMock->shouldReceive('reset') - ->once(); + $cacheStore = Mockery::mock(CacheInterface::class); + $cacheStore->shouldReceive('get')->with('brand:all')->andReturn(null); + $cacheStore->shouldReceive('delete')->with('brand:all')->once()->andReturn(true); + $logger = Mockery::mock(LoggerInterface::class); - $listener = new InvalidateBrandCache($brandCacheMock); + $brandCache = new BrandCache($cacheStore, $logger); + $listener = new InvalidateBrandCache($brandCache); // WHEN $listener->handle($event); - // THEN expect(true)->toBeTrue(); }); @@ -53,12 +55,13 @@ describe('InvalidateBrandCache Listener Unit Tests', function (): void { occurredAt: new \DateTimeImmutable(), ); - /** @var BrandCache&Mockery\MockInterface $brandCacheMock */ - $brandCacheMock = Mockery::mock(BrandCache::class); - $brandCacheMock->shouldReceive('reset') - ->once(); + $cacheStore = Mockery::mock(CacheInterface::class); + $cacheStore->shouldReceive('get')->with('brand:all')->andReturn(null); + $cacheStore->shouldReceive('delete')->with('brand:all')->once()->andReturn(true); + $logger = Mockery::mock(LoggerInterface::class); - $listener = new InvalidateBrandCache($brandCacheMock); + $brandCache = new BrandCache($cacheStore, $logger); + $listener = new InvalidateBrandCache($brandCache); // WHEN $listener->handle($event);