Files
Gnuboard7/tests/Unit/Installer/DeleteDirectoryTest.php
T
HeuJung 342486a20b fix(installer): Composer 설치 로그 인코딩으로 인한 진행 상태 응답 빈 본문 보완
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 제보).
2026-07-06 17:09:41 +09:00

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);
}
}