output; } }; /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); $databaseMock->shouldNotReceive('transaction'); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once(); $loggerMock->shouldReceive('info')->once(); $executor = new LaravelExecutor($databaseMock, $loggerMock); // WHEN $result = $executor->execute($usecaseMock, $inputMock); // THEN expect($result)->toBe($outputMock); }); it('executes transactional usecase inside a database transaction', function (): void { // GIVEN $inputMock = new class () implements Input { }; $outputMock = new class () implements Output { }; $usecaseMock = new class ($outputMock) implements Transactional, Usecase { public function __construct(private readonly Output $output) { } public function execute(Input $input): Output { return $this->output; } }; /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); $databaseMock->shouldReceive('transaction') ->once() ->andReturnUsing(static fn (\Closure $callback) => $callback()); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once(); $loggerMock->shouldReceive('info')->once(); $executor = new LaravelExecutor($databaseMock, $loggerMock); // WHEN $result = $executor->execute($usecaseMock, $inputMock); // THEN expect($result)->toBe($outputMock); }); it('transforms output using presenter when presenter is provided', function (): void { // GIVEN $inputMock = new class () implements Input { }; $outputMock = new class () implements Output { }; $viewResult = new \stdClass(); $viewResult->success = true; $usecaseMock = new class ($outputMock) implements Usecase { public function __construct(private readonly Output $output) { } public function execute(Input $input): Output { return $this->output; } }; $presenterMock = new class ($viewResult) implements Presenter { public function __construct(private readonly object $view) { } public function present(Output $output): object { return $this->view; } }; /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); $databaseMock->shouldNotReceive('transaction'); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once(); $loggerMock->shouldReceive('info')->once(); $executor = new LaravelExecutor($databaseMock, $loggerMock); // WHEN $result = $executor->execute($usecaseMock, $inputMock, $presenterMock); // THEN expect($result)->toBe($viewResult); }); it('logs error and rethrows exception when usecase execution fails', function (): void { // GIVEN $inputMock = new class () implements Input { }; $exception = new \RuntimeException('Database connection lost'); $usecaseMock = new class ($exception) implements Transactional, Usecase { public function __construct(private readonly \RuntimeException $exception) { } public function execute(Input $input): Output { throw $this->exception; } }; /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); $databaseMock->shouldReceive('transaction') ->once() ->andReturnUsing(static fn (\Closure $callback) => $callback()); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once(); $loggerMock->shouldReceive('error')->once(); $executor = new LaravelExecutor($databaseMock, $loggerMock); // WHEN & THEN expect(fn () => $executor->execute($usecaseMock, $inputMock)) ->toThrow(\RuntimeException::class, 'Database connection lost'); }); it('includes user details in logging context when user details provider is present', function (): void { // GIVEN $inputMock = new class () implements Input { }; $outputMock = new class () implements Output { }; $usecaseMock = new class ($outputMock) implements Usecase { public function __construct(private readonly Output $output) { } public function execute(Input $input): Output { return $this->output; } }; $userType = new class () implements UserType { public function type(): string { return 'api_client'; } public function equals(mixed $other): bool { return $other instanceof self && $other->type() === $this->type(); } }; $userDetails = new class ($userType) implements UserDetails { public function __construct(private readonly UserType $userType) { } public function id(): string { return 'uuid-123'; } public function name(): string { return 'Test Client'; } public function type(): UserType { return $this->userType; } public function identifier(): string { return 'client-key-456'; } }; /** @var UserDetailsProvider&Mockery\MockInterface $userProviderMock */ $userProviderMock = Mockery::mock(UserDetailsProvider::class); $userProviderMock->shouldReceive('currentUser')->andReturn($userDetails); /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once()->with( 'Starting usecase execution', Mockery::on( static fn (array $context): bool => isset($context['user']) && $context['user']['id'] === 'uuid-123' && $context['user']['name'] === 'Test Client' && $context['user']['type'] === 'api_client' && $context['user']['identifier'] === 'client-key-456' && $context['transactional'] === false ) ); $loggerMock->shouldReceive('info')->once(); $executor = new LaravelExecutor($databaseMock, $loggerMock, $userProviderMock); // WHEN $result = $executor->execute($usecaseMock, $inputMock); // THEN expect($result)->toBe($outputMock); }); it('releases pending domain events after successful execution', function (): void { // GIVEN $inputMock = new class () implements Input { }; $outputMock = new class () implements Output { }; $usecaseMock = new class ($outputMock) implements Transactional, Usecase { public function __construct(private readonly Output $output) { } public function execute(Input $input): Output { return $this->output; } }; /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); $databaseMock->shouldReceive('transaction') ->once() ->andReturnUsing(static fn (\Closure $callback) => $callback()); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once(); $loggerMock->shouldReceive('info')->once(); /** @var \Shared\DDD\Event\TransactionalDomainEventDispatcher&Mockery\MockInterface $eventDispatcherMock */ $eventDispatcherMock = Mockery::mock(TransactionalDomainEventDispatcher::class); $eventDispatcherMock->expects('release')->once(); $eventDispatcherMock->shouldNotReceive('clear'); $executor = new LaravelExecutor($databaseMock, $loggerMock, null, $eventDispatcherMock); // WHEN $result = $executor->execute($usecaseMock, $inputMock); // THEN expect($result)->toBe($outputMock); }); it('clears pending domain events when usecase execution fails', function (): void { // GIVEN $inputMock = new class () implements Input { }; $exception = new \RuntimeException('Something went wrong'); $usecaseMock = new class ($exception) implements Transactional, Usecase { public function __construct(private readonly \RuntimeException $exception) { } public function execute(Input $input): Output { throw $this->exception; } }; /** @var DatabaseManager&Mockery\MockInterface $databaseMock */ $databaseMock = Mockery::mock(DatabaseManager::class); $databaseMock->shouldReceive('transaction') ->once() ->andReturnUsing(static fn (\Closure $callback) => $callback()); /** @var LoggerInterface&Mockery\MockInterface $loggerMock */ $loggerMock = Mockery::mock(LoggerInterface::class); $loggerMock->shouldReceive('debug')->once(); $loggerMock->shouldReceive('error')->once(); /** @var \Shared\DDD\Event\TransactionalDomainEventDispatcher&Mockery\MockInterface $eventDispatcherMock */ $eventDispatcherMock = Mockery::mock(TransactionalDomainEventDispatcher::class); $eventDispatcherMock->expects('clear')->once(); $eventDispatcherMock->shouldNotReceive('release'); $executor = new LaravelExecutor($databaseMock, $loggerMock, null, $eventDispatcherMock); // WHEN & THEN expect(fn () => $executor->execute($usecaseMock, $inputMock)) ->toThrow(\RuntimeException::class, 'Something went wrong'); }); });