Composer 실행 모드로 설치할 때 일부 Windows 환경에서 composer 출력의 invalid UTF-8 바이트가 정규화 없이 로그에 저장되어, 진행 상태 응답의 json_encode 가 false 를 반환 → 빈 본문(HTTP 200) → 프론트 파싱 실패로 진행 상황이 표시되지 않던 문제. - 로그 저장(addLog)에서 mb_scrub 로 항상 유효 UTF-8 보장 (근본) - composer 스트림 라인 정규화 (SSE/폴링 공통 경계) - 폴링(state-management)·SSE(progress-emitter) 응답 json_encode 에 JSON_INVALID_UTF8_SUBSTITUTE + false 가드 (양쪽 안전망) 무관 결함 동시 정리: 신규 격리 테스트가 트리거한 BASE_PATH 누수로 깨지던 DeleteDirectoryTest·InstallerWindowsCommandsTest 를 프로젝트 루트 절대경로 앵커로 격리. 공개 이슈 gnuboard/g7 (@kitrio 제보).
230 lines
8.3 KiB
PHP
230 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Installer;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* deleteDirectory() 함수의 보호 파일 보존 테스트
|
|
*
|
|
* .gitkeep, .gitignore 등 Git 추적 파일이 삭제되지 않는지 검증합니다.
|
|
*/
|
|
class DeleteDirectoryTest extends TestCase
|
|
{
|
|
/**
|
|
* 테스트용 임시 디렉토리 경로
|
|
*/
|
|
private string $tempDir;
|
|
|
|
/**
|
|
* 테스트 전 인스톨러 함수 로드 및 임시 디렉토리 생성
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// 상수 정의 (한 번만)
|
|
if (! defined('BASE_PATH')) {
|
|
define('BASE_PATH', dirname(__DIR__, 2).'/..');
|
|
}
|
|
if (! defined('REQUIRED_DIRECTORY_PERMISSIONS')) {
|
|
define('REQUIRED_DIRECTORY_PERMISSIONS', 0770);
|
|
}
|
|
if (! defined('REQUIRED_DIRECTORY_PERMISSIONS_DISPLAY')) {
|
|
define('REQUIRED_DIRECTORY_PERMISSIONS_DISPLAY', '770');
|
|
}
|
|
if (! defined('REQUIRED_DIRECTORIES')) {
|
|
define('REQUIRED_DIRECTORIES', ['storage' => true]);
|
|
}
|
|
if (! defined('SUPPORTED_LANGUAGES')) {
|
|
define('SUPPORTED_LANGUAGES', ['ko' => '한국어', 'en' => 'English']);
|
|
}
|
|
if (! defined('INSTALLER_BASE_URL')) {
|
|
define('INSTALLER_BASE_URL', '/install');
|
|
}
|
|
|
|
// 인스톨러 함수 로드 (pure PHPUnit TestCase 라서 Laravel base_path() 사용 불가).
|
|
// 프로젝트 루트 절대경로(__DIR__ 기준)로 로드한다 — 다른 인스톨러 테스트
|
|
// (#[RunClassInSeparateProcess] + temp BASE_PATH)가 메인 프로세스로 BASE_PATH 를
|
|
// 누수시켜도 그 temp 경로에는 public/install 이 없어 require 가 깨지므로, 누수된
|
|
// BASE_PATH 값과 무관하게 항상 실제 프로젝트 파일을 참조한다.
|
|
$installerRoot = dirname(__DIR__, 3);
|
|
require_once $installerRoot.'/public/install/includes/functions.php';
|
|
require_once $installerRoot.'/public/install/api/rollback-functions.php';
|
|
|
|
// 임시 디렉토리 생성
|
|
$this->tempDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'g7_test_delete_'.uniqid();
|
|
mkdir($this->tempDir, 0755, true);
|
|
}
|
|
|
|
/**
|
|
* 테스트 후 임시 디렉토리 정리
|
|
*/
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupTempDir($this->tempDir);
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 .gitkeep 파일을 보존하는지 확인합니다.
|
|
*/
|
|
public function test_preserves_gitkeep_file(): void
|
|
{
|
|
// Given: 디렉토리에 .gitkeep과 일반 파일이 있음
|
|
file_put_contents($this->tempDir.'/.gitkeep', '');
|
|
file_put_contents($this->tempDir.'/some_file.txt', 'test');
|
|
mkdir($this->tempDir.'/subdir', 0755);
|
|
file_put_contents($this->tempDir.'/subdir/nested.txt', 'test');
|
|
|
|
// When: deleteDirectory 호출 (기본: removeDir=false)
|
|
$result = deleteDirectory($this->tempDir);
|
|
|
|
// Then: .gitkeep은 보존, 나머지는 삭제, 디렉토리 자체는 유지
|
|
$this->assertTrue($result);
|
|
$this->assertDirectoryExists($this->tempDir);
|
|
$this->assertFileExists($this->tempDir.'/.gitkeep');
|
|
$this->assertFileDoesNotExist($this->tempDir.'/some_file.txt');
|
|
$this->assertDirectoryDoesNotExist($this->tempDir.'/subdir');
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 .gitignore 파일을 보존하는지 확인합니다.
|
|
*/
|
|
public function test_preserves_gitignore_file(): void
|
|
{
|
|
// Given: 디렉토리에 .gitignore와 일반 파일이 있음
|
|
file_put_contents($this->tempDir.'/.gitignore', '*');
|
|
file_put_contents($this->tempDir.'/data.log', 'log');
|
|
|
|
// When
|
|
$result = deleteDirectory($this->tempDir);
|
|
|
|
// Then
|
|
$this->assertTrue($result);
|
|
$this->assertDirectoryExists($this->tempDir);
|
|
$this->assertFileExists($this->tempDir.'/.gitignore');
|
|
$this->assertFileDoesNotExist($this->tempDir.'/data.log');
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 보호 파일이 없으면 디렉토리를 유지하되 비워두는지 확인합니다.
|
|
*/
|
|
public function test_keeps_directory_when_no_preserve_files_and_remove_dir_false(): void
|
|
{
|
|
// Given: 보호 파일 없이 일반 파일만 있음
|
|
file_put_contents($this->tempDir.'/file1.txt', 'test');
|
|
file_put_contents($this->tempDir.'/file2.txt', 'test');
|
|
|
|
// When: removeDir=false (기본값)
|
|
$result = deleteDirectory($this->tempDir);
|
|
|
|
// Then: 디렉토리 자체는 유지, 내용물은 삭제
|
|
$this->assertTrue($result);
|
|
$this->assertDirectoryExists($this->tempDir);
|
|
$this->assertFileDoesNotExist($this->tempDir.'/file1.txt');
|
|
$this->assertFileDoesNotExist($this->tempDir.'/file2.txt');
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 removeDir=true이고 보호 파일이 있으면 디렉토리를 유지하는지 확인합니다.
|
|
*/
|
|
public function test_keeps_directory_when_preserve_files_exist_even_with_remove_dir(): void
|
|
{
|
|
// Given: .gitkeep이 있음
|
|
file_put_contents($this->tempDir.'/.gitkeep', '');
|
|
file_put_contents($this->tempDir.'/other.txt', 'test');
|
|
|
|
// When: removeDir=true여도 보호 파일이 있으면 디렉토리 유지
|
|
$result = deleteDirectory($this->tempDir, true);
|
|
|
|
// Then
|
|
$this->assertTrue($result);
|
|
$this->assertDirectoryExists($this->tempDir);
|
|
$this->assertFileExists($this->tempDir.'/.gitkeep');
|
|
$this->assertFileDoesNotExist($this->tempDir.'/other.txt');
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 removeDir=true이고 보호 파일이 없으면 디렉토리를 삭제하는지 확인합니다.
|
|
*/
|
|
public function test_removes_directory_when_no_preserve_files_and_remove_dir_true(): void
|
|
{
|
|
// Given: 보호 파일 없음
|
|
file_put_contents($this->tempDir.'/file.txt', 'test');
|
|
|
|
// When
|
|
$result = deleteDirectory($this->tempDir, true);
|
|
|
|
// Then: 디렉토리도 삭제
|
|
$this->assertDirectoryDoesNotExist($this->tempDir);
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 중첩 디렉토리를 재귀적으로 처리하는지 확인합니다.
|
|
*/
|
|
public function test_handles_nested_directories(): void
|
|
{
|
|
// Given: vendor 디렉토리 구조 시뮬레이션
|
|
file_put_contents($this->tempDir.'/.gitkeep', '');
|
|
mkdir($this->tempDir.'/laravel/framework/src', 0755, true);
|
|
file_put_contents($this->tempDir.'/laravel/framework/src/Application.php', '<?php');
|
|
file_put_contents($this->tempDir.'/autoload.php', '<?php');
|
|
|
|
// When
|
|
$result = deleteDirectory($this->tempDir);
|
|
|
|
// Then: .gitkeep만 보존, 나머지 전부 삭제
|
|
$this->assertTrue($result);
|
|
$this->assertDirectoryExists($this->tempDir);
|
|
$this->assertFileExists($this->tempDir.'/.gitkeep');
|
|
$this->assertFileDoesNotExist($this->tempDir.'/autoload.php');
|
|
$this->assertDirectoryDoesNotExist($this->tempDir.'/laravel');
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 빈 디렉토리에서도 정상 동작하는지 확인합니다.
|
|
*/
|
|
public function test_handles_empty_directory(): void
|
|
{
|
|
// When
|
|
$result = deleteDirectory($this->tempDir);
|
|
|
|
// Then: 디렉토리 유지 (removeDir=false 기본값)
|
|
$this->assertTrue($result);
|
|
$this->assertDirectoryExists($this->tempDir);
|
|
}
|
|
|
|
/**
|
|
* deleteDirectory()가 존재하지 않는 경로에서 false를 반환하는지 확인합니다.
|
|
*/
|
|
public function test_returns_false_for_nonexistent_directory(): void
|
|
{
|
|
$result = deleteDirectory($this->tempDir.'/nonexistent');
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* 임시 디렉토리를 완전히 정리합니다 (테스트용).
|
|
*
|
|
* @param string $dir 정리할 디렉토리 경로
|
|
*/
|
|
private function cleanupTempDir(string $dir): void
|
|
{
|
|
if (! is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$items = array_diff(scandir($dir), ['.', '..']);
|
|
foreach ($items as $item) {
|
|
$path = $dir.DIRECTORY_SEPARATOR.$item;
|
|
if (is_dir($path)) {
|
|
$this->cleanupTempDir($path);
|
|
} else {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
@rmdir($dir);
|
|
}
|
|
}
|