Files
Gnuboard7/tests/Unit/Extension/UpgradeStepsTest.php
T
2026-04-01 10:30:52 +09:00

342 lines
13 KiB
PHP

<?php
namespace Tests\Unit\Extension;
use App\Contracts\Extension\ModuleInterface;
use App\Contracts\Extension\UpgradeStepInterface;
use App\Extension\UpgradeContext;
use Mockery;
use Tests\TestCase;
/**
* 버전별 업그레이드 스텝 실행 테스트
*
* runUpgradeSteps() 메서드의 필터링, 정렬, 실행 로직을 검증합니다.
*/
class UpgradeStepsTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
/**
* 빈 업그레이드 스텝 배열에서 아무 작업도 하지 않는지 테스트
*/
public function test_run_upgrade_steps_does_nothing_with_empty_steps(): void
{
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([]);
// runUpgradeSteps는 protected이므로 Reflection으로 접근
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
// 예외 없이 정상 완료
$method->invoke($manager, $module, '1.0.0', '2.0.0');
$this->assertTrue(true);
}
/**
* fromVersion 초과 ~ toVersion 이하 필터링 테스트
*/
public function test_run_upgrade_steps_filters_by_version_range(): void
{
$executedSteps = [];
$step_1_1 = Mockery::mock(UpgradeStepInterface::class);
$step_1_1->shouldReceive('run')->once()->andReturnUsing(function (UpgradeContext $ctx) use (&$executedSteps) {
$executedSteps[] = $ctx->currentStep;
});
$step_1_2 = Mockery::mock(UpgradeStepInterface::class);
$step_1_2->shouldReceive('run')->once()->andReturnUsing(function (UpgradeContext $ctx) use (&$executedSteps) {
$executedSteps[] = $ctx->currentStep;
});
$step_0_5 = Mockery::mock(UpgradeStepInterface::class);
$step_0_5->shouldNotReceive('run'); // fromVersion 이하이므로 실행되지 않아야 함
$step_2_0 = Mockery::mock(UpgradeStepInterface::class);
$step_2_0->shouldNotReceive('run'); // toVersion(1.2.0) 초과이므로 실행되지 않아야 함
// fromVersion=1.0.0, toVersion=1.2.0 → 1.1.0과 1.2.0만 실행
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([
'0.5.0' => $step_0_5,
'1.1.0' => $step_1_1,
'1.2.0' => $step_1_2,
'2.0.0' => $step_2_0,
]);
$module->shouldReceive('getIdentifier')->andReturn('test-module');
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
$method->invoke($manager, $module, '1.0.0', '1.2.0');
$this->assertEquals(['1.1.0', '1.2.0'], $executedSteps);
}
/**
* 버전순 정렬 테스트 (역순으로 제공된 스텝이 올바른 순서로 실행되는지)
*/
public function test_run_upgrade_steps_executes_in_version_order(): void
{
$executedOrder = [];
$step_1_3 = Mockery::mock(UpgradeStepInterface::class);
$step_1_3->shouldReceive('run')->once()->andReturnUsing(function () use (&$executedOrder) {
$executedOrder[] = '1.3.0';
});
$step_1_1 = Mockery::mock(UpgradeStepInterface::class);
$step_1_1->shouldReceive('run')->once()->andReturnUsing(function () use (&$executedOrder) {
$executedOrder[] = '1.1.0';
});
$step_1_2 = Mockery::mock(UpgradeStepInterface::class);
$step_1_2->shouldReceive('run')->once()->andReturnUsing(function () use (&$executedOrder) {
$executedOrder[] = '1.2.0';
});
// 역순으로 제공
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([
'1.3.0' => $step_1_3,
'1.1.0' => $step_1_1,
'1.2.0' => $step_1_2,
]);
$module->shouldReceive('getIdentifier')->andReturn('test-module');
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
$method->invoke($manager, $module, '1.0.0', '2.0.0');
$this->assertEquals(['1.1.0', '1.2.0', '1.3.0'], $executedOrder);
}
/**
* callable 스텝 실행 테스트
*/
public function test_run_upgrade_steps_supports_callable(): void
{
$executed = false;
$receivedContext = null;
$callableStep = function (UpgradeContext $ctx) use (&$executed, &$receivedContext) {
$executed = true;
$receivedContext = $ctx;
};
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([
'1.1.0' => $callableStep,
]);
$module->shouldReceive('getIdentifier')->andReturn('test-module');
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
$method->invoke($manager, $module, '1.0.0', '2.0.0');
$this->assertTrue($executed);
$this->assertInstanceOf(UpgradeContext::class, $receivedContext);
$this->assertEquals('1.0.0', $receivedContext->fromVersion);
$this->assertEquals('2.0.0', $receivedContext->toVersion);
$this->assertEquals('1.1.0', $receivedContext->currentStep);
}
/**
* 스텝 실행 중 예외 발생 시 전파되는지 테스트
*/
public function test_run_upgrade_steps_propagates_exception(): void
{
$failingStep = Mockery::mock(UpgradeStepInterface::class);
$failingStep->shouldReceive('run')->once()->andThrow(new \RuntimeException('Step failed'));
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([
'1.1.0' => $failingStep,
]);
$module->shouldReceive('getIdentifier')->andReturn('test-module');
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Step failed');
$method->invoke($manager, $module, '1.0.0', '2.0.0');
}
/**
* UpgradeContext의 withCurrentStep이 올바르게 동작하는지 테스트
*/
public function test_upgrade_context_with_current_step_creates_new_instance(): void
{
$context = new UpgradeContext(
fromVersion: '1.0.0',
toVersion: '2.0.0',
);
$stepContext = $context->withCurrentStep('1.5.0');
$this->assertEquals('1.0.0', $stepContext->fromVersion);
$this->assertEquals('2.0.0', $stepContext->toVersion);
$this->assertEquals('1.5.0', $stepContext->currentStep);
// 원본은 변경되지 않음
$this->assertEquals('', $context->currentStep);
}
/**
* fromVersion과 toVersion이 같은 경우 아무 스텝도 실행되지 않는지 테스트
*/
public function test_run_upgrade_steps_no_execution_when_versions_equal(): void
{
$step = Mockery::mock(UpgradeStepInterface::class);
$step->shouldNotReceive('run');
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([
'1.0.0' => $step,
]);
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
$method->invoke($manager, $module, '1.0.0', '1.0.0');
$this->assertTrue(true);
}
/**
* 업그레이드 스텝 파일명에서 일반 버전을 올바르게 파싱합니다.
*/
public function test_upgrade_step_filename_parses_standard_version(): void
{
$pattern = '/^Upgrade_(\d+)_(\d+)_(\d+)(?:_([a-zA-Z]\w*(?:_\d+)*))?$/';
$this->assertMatchesRegularExpression($pattern, 'Upgrade_1_0_0');
$this->assertMatchesRegularExpression($pattern, 'Upgrade_1_2_3');
$this->assertMatchesRegularExpression($pattern, 'Upgrade_10_20_30');
// 파싱 결과 확인
preg_match($pattern, 'Upgrade_1_2_3', $matches);
$version = "{$matches[1]}.{$matches[2]}.{$matches[3]}";
$this->assertSame('1.2.3', $version);
$this->assertEmpty($matches[4] ?? '');
}
/**
* 업그레이드 스텝 파일명에서 프리릴리스 버전을 올바르게 파싱합니다.
*/
public function test_upgrade_step_filename_parses_prerelease_version(): void
{
$pattern = '/^Upgrade_(\d+)_(\d+)_(\d+)(?:_([a-zA-Z]\w*(?:_\d+)*))?$/';
// alpha.1
preg_match($pattern, 'Upgrade_7_0_0_alpha_1', $matches);
$version = "{$matches[1]}.{$matches[2]}.{$matches[3]}";
if (! empty($matches[4])) {
$version .= '-'.str_replace('_', '.', $matches[4]);
}
$this->assertSame('7.0.0-alpha.1', $version);
// rc.2
preg_match($pattern, 'Upgrade_1_0_0_rc_2', $matches);
$version = "{$matches[1]}.{$matches[2]}.{$matches[3]}";
if (! empty($matches[4])) {
$version .= '-'.str_replace('_', '.', $matches[4]);
}
$this->assertSame('1.0.0-rc.2', $version);
// alpha (숫자 없는 프리릴리스)
preg_match($pattern, 'Upgrade_2_0_0_alpha', $matches);
$version = "{$matches[1]}.{$matches[2]}.{$matches[3]}";
if (! empty($matches[4])) {
$version .= '-'.str_replace('_', '.', $matches[4]);
}
$this->assertSame('2.0.0-alpha', $version);
}
/**
* 무효한 업그레이드 스텝 파일명은 매칭되지 않습니다.
*/
public function test_upgrade_step_filename_rejects_invalid_names(): void
{
$pattern = '/^Upgrade_(\d+)_(\d+)_(\d+)(?:_([a-zA-Z]\w*(?:_\d+)*))?$/';
// 숫자로 시작하는 프리릴리스
$this->assertDoesNotMatchRegularExpression($pattern, 'Upgrade_1_0_0_1beta');
// 접두사 누락
$this->assertDoesNotMatchRegularExpression($pattern, '1_0_0');
// 버전 세그먼트 부족
$this->assertDoesNotMatchRegularExpression($pattern, 'Upgrade_1_0');
}
/**
* 프리릴리스 버전이 포함된 업그레이드 스텝의 범위 필터링 테스트
*/
public function test_run_upgrade_steps_filters_prerelease_versions(): void
{
$executedSteps = [];
$step_beta1 = Mockery::mock(UpgradeStepInterface::class);
$step_beta1->shouldReceive('run')->once()->andReturnUsing(function (UpgradeContext $ctx) use (&$executedSteps) {
$executedSteps[] = $ctx->currentStep;
});
$step_beta2 = Mockery::mock(UpgradeStepInterface::class);
$step_beta2->shouldReceive('run')->once()->andReturnUsing(function (UpgradeContext $ctx) use (&$executedSteps) {
$executedSteps[] = $ctx->currentStep;
});
$step_alpha = Mockery::mock(UpgradeStepInterface::class);
$step_alpha->shouldNotReceive('run');
// from=7.0.0-alpha.1, to=7.0.0-beta.2 → beta.1, beta.2만 실행
$module = Mockery::mock(ModuleInterface::class);
$module->shouldReceive('upgrades')->once()->andReturn([
'7.0.0-alpha.1' => $step_alpha,
'7.0.0-beta.1' => $step_beta1,
'7.0.0-beta.2' => $step_beta2,
]);
$module->shouldReceive('getIdentifier')->andReturn('test-module');
$manager = $this->createModuleManagerWithMocks();
$method = new \ReflectionMethod($manager, 'runUpgradeSteps');
$method->setAccessible(true);
$method->invoke($manager, $module, '7.0.0-alpha.1', '7.0.0-beta.2');
$this->assertEquals(['7.0.0-beta.1', '7.0.0-beta.2'], $executedSteps);
}
/**
* 모든 의존성을 Mock으로 대체한 ModuleManager 인스턴스를 생성합니다.
*/
private function createModuleManagerWithMocks(): \App\Extension\ModuleManager
{
return new \App\Extension\ModuleManager(
extensionManager: Mockery::mock(\App\Extension\ExtensionManager::class),
moduleRepository: Mockery::mock(\App\Contracts\Repositories\ModuleRepositoryInterface::class),
permissionRepository: Mockery::mock(\App\Contracts\Repositories\PermissionRepositoryInterface::class),
roleRepository: Mockery::mock(\App\Contracts\Repositories\RoleRepositoryInterface::class),
menuRepository: Mockery::mock(\App\Contracts\Repositories\MenuRepositoryInterface::class),
templateRepository: Mockery::mock(\App\Contracts\Repositories\TemplateRepositoryInterface::class),
pluginRepository: Mockery::mock(\App\Contracts\Repositories\PluginRepositoryInterface::class),
layoutRepository: Mockery::mock(\App\Contracts\Repositories\LayoutRepositoryInterface::class),
layoutExtensionService: Mockery::mock(\App\Services\LayoutExtensionService::class),
);
}
}