test(arch): overhaul and complete architecture test suite across all layers
This commit is contained in:
parent
106d4467c4
commit
e83a286e9b
13 changed files with 380 additions and 125 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,48 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Tests\Architecture;
|
||||
|
||||
/** @return array<int, string> */
|
||||
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<int, string> */
|
||||
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'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,34 +4,35 @@ declare(strict_types=1);
|
|||
|
||||
namespace Tests\Architecture;
|
||||
|
||||
/**
|
||||
* @param array<int, string> $directories
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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<int, string> $directories
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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)')
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue