ldc-arcadia-api/tests/Integration/Infra/Usecase/LaravelExecutorTest.php

373 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Integration\Infra\Usecase;
use Illuminate\Database\DatabaseManager;
use Infra\Usecase\LaravelExecutor;
use Mockery;
use Psr\Log\LoggerInterface;
use Shared\DDD\Event\TransactionalDomainEventDispatcher;
use Shared\DDD\Security\UserDetails;
use Shared\DDD\Security\UserDetailsProvider;
use Shared\DDD\Security\UserType;
use Shared\DDD\Usecase\Input;
use Shared\DDD\Usecase\Output;
use Shared\DDD\Usecase\Presenter;
use Shared\DDD\Usecase\Transactional;
use Shared\DDD\Usecase\Usecase;
covers(LaravelExecutor::class);
describe('LaravelExecutor Integration Tests', function (): void {
it('executes non-transactional usecase directly without opening a database transaction', 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;
}
};
/** @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');
});
});