373 lines
12 KiB
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');
|
|
});
|
|
});
|