Files
Gnuboard7/tests/Feature/Console/CoreUpdateWithBundleTest.php
HeuJung d0943d6cb6 feat(core): core:update 기본을 코어 변경분만 적용(3-way skip)으로 전환
core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여
사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를
차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한
파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재
디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면
--prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전
회귀 + 안내를 출력한다.

기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune
컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune
커맨드 표면 테스트를 추가했다.

번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서
동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어
번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
2026-07-07 22:45:16 +09:00

137 lines
5.5 KiB
PHP

<?php
namespace Tests\Feature\Console;
use App\Extension\Vendor\VendorBundler;
use App\Extension\Vendor\VendorMode;
use App\Services\CoreUpdateService;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* core:update --vendor-mode 3 모드 Feature 테스트.
*
* 실제 core:update 커맨드를 풀 체인으로 검증하는 것은 마이그레이션/GitHub 연동 등
* 사이드 이펙트가 크므로, CoreUpdateService::runVendorInstallInPending() 경로를
* 중심으로 VendorResolver 분기 + 해시 검증 + zip 추출 동작을 격리 검증한다.
*/
class CoreUpdateWithBundleTest extends TestCase
{
private CoreUpdateService $service;
private string $pendingDir;
protected function setUp(): void
{
parent::setUp();
$this->service = app(CoreUpdateService::class);
$this->pendingDir = storage_path('app/test-core-vendor-bundle-'.uniqid());
File::ensureDirectoryExists($this->pendingDir);
}
protected function tearDown(): void
{
if (File::isDirectory($this->pendingDir)) {
File::deleteDirectory($this->pendingDir);
}
parent::tearDown();
}
/**
* 테스트용 _pending 디렉토리에 vendor-bundle.zip 을 생성합니다.
*/
private function createPendingBundle(): void
{
File::put($this->pendingDir.'/composer.json', json_encode([
'name' => 'test/core',
'require' => ['php' => '^8.2'],
]));
$zipPath = $this->pendingDir.'/vendor-bundle.zip';
$zip = new \ZipArchive;
$zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
$zip->addFromString('vendor/autoload.php', '<?php // test autoload');
$zip->addFromString('vendor/laravel/framework/src/file.php', '<?php // framework');
$zip->close();
File::put($this->pendingDir.'/vendor-bundle.json', json_encode([
'schema_version' => VendorBundler::SCHEMA_VERSION,
'zip_sha256' => hash_file('sha256', $zipPath),
'package_count' => 1,
]));
}
public function test_vendor_mode_bundled_extracts_bundle_zip(): void
{
$this->createPendingBundle();
$result = $this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Bundled);
$this->assertSame(VendorMode::Bundled, $result->mode);
$this->assertSame('bundled', $result->strategy);
$this->assertFileExists($this->pendingDir.'/vendor/autoload.php');
$this->assertFileExists($this->pendingDir.'/vendor/laravel/framework/src/file.php');
}
public function test_vendor_mode_bundled_fails_when_bundle_missing(): void
{
// composer.json 은 있지만 vendor-bundle.zip 없음
File::put($this->pendingDir.'/composer.json', json_encode(['name' => 'test/core']));
$this->expectException(\App\Extension\Vendor\Exceptions\VendorInstallException::class);
$this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Bundled);
}
public function test_vendor_mode_bundled_fails_on_tampered_zip(): void
{
$this->createPendingBundle();
// zip 해시를 조작하여 무결성 검증 실패 유도
$manifestPath = $this->pendingDir.'/vendor-bundle.json';
$manifest = json_decode(File::get($manifestPath), true);
$manifest['zip_sha256'] = hash('sha256', 'tampered');
File::put($manifestPath, json_encode($manifest));
$this->expectException(\App\Extension\Vendor\Exceptions\VendorInstallException::class);
$this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Bundled);
}
public function test_vendor_mode_composer_requires_composer_executor_fallback(): void
{
// composer 모드 요청이지만 실제 composer 실행은 스킵될 수 있는 환경
// runVendorInstallInPending 은 내부적으로 CoreUpdateService::runComposerInstallInPending() 을
// composerExecutor 로 전달하므로, composer 바이너리 존재 여부에 따라 동작이 달라짐.
File::put($this->pendingDir.'/composer.json', json_encode(['name' => 'test/core']));
// composer 모드에서 번들 zip 없어도 composer 실행을 시도하지 — zip 무결성 검증 불필요
// 실제 실행은 환경에 따라 실패할 수 있으므로 예외 타입만 확인
try {
$this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Composer);
$this->assertTrue(true); // composer 실행 성공 (거의 없음)
} catch (\Throwable $e) {
$this->assertNotEmpty($e->getMessage());
}
}
public function test_core_update_command_accepts_vendor_mode_option(): void
{
// 실제 core:update 는 풀 체인 실행이 복잡하므로 --help 출력에서 옵션 존재만 검증
$this->artisan('core:update', ['--help' => true])
->expectsOutputToContain('--vendor-mode')
->assertExitCode(0);
}
/**
* `--prune` 옵션이 등록되어 있는지 검증합니다 (공개 #64).
*
* 실제 파일 적용 로직(증분/prune)은 CoreUpdateServiceTest 의 applyUpdate 단위
* 테스트가 커버하며, 여기서는 커맨드 표면에 옵션이 노출되는지만 확인한다.
*/
public function test_core_update_command_accepts_prune_option(): void
{
$this->artisan('core:update', ['--help' => true])
->expectsOutputToContain('--prune')
->assertExitCode(0);
}
}