Files
Gnuboard7/tests/Unit/Installer/InstallerWindowsCommandsTest.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

204 lines
7.9 KiB
PHP

<?php
namespace Tests\Unit\Installer;
use PHPUnit\Framework\TestCase;
/**
* 인스톨러 Windows 명령어 대응 테스트
*
* functions.php의 isWindows(), getEnvCopyCommand(), getPermissionFixCommand() 헬퍼 함수를 테스트합니다.
*/
class InstallerWindowsCommandsTest extends TestCase
{
/**
* 프로젝트 루트 절대경로 (BASE_PATH 상수와 독립).
*
* 다른 인스톨러 테스트(#[RunClassInSeparateProcess] + temp BASE_PATH)가 메인
* 프로세스로 BASE_PATH 를 누수시키면, 그 temp 경로 하위에는 public/install 이 없어
* require 가 깨진다. 본 클래스는 __DIR__ 기준 절대경로로 인스톨러 파일을 로드해
* 누수된 BASE_PATH 값과 무관하게 항상 실제 프로젝트 파일을 참조한다.
*/
private function installerRoot(): string
{
return dirname(__DIR__, 3);
}
/**
* 테스트 전 인스톨러 설정 및 함수 파일을 로드합니다.
*/
protected function setUp(): void
{
parent::setUp();
// BASE_PATH 상수가 아직 정의되지 않은 경우만 정의
if (! defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2).'/..');
}
// REQUIRED_DIRECTORY_PERMISSIONS 상수들이 필요
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');
}
// functions.php 로드 (한 번만) — 누수된 BASE_PATH 와 무관하게 프로젝트 루트 절대경로 사용
require_once $this->installerRoot().'/public/install/includes/functions.php';
}
/**
* isWindows() 함수가 현재 OS를 정확히 감지하는지 확인합니다.
*/
public function test_is_windows_returns_bool(): void
{
$result = isWindows();
$this->assertIsBool($result);
// Windows 환경에서는 true여야 함
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$this->assertTrue($result);
} else {
$this->assertFalse($result);
}
}
/**
* getEnvCopyCommand()가 Windows에서 올바른 명령어를 반환하는지 확인합니다.
*/
public function test_get_env_copy_command_with_base_path(): void
{
$command = getEnvCopyCommand('/var/www/g7');
if (isWindows()) {
$this->assertStringContainsString('copy', $command);
$this->assertStringContainsString('\\', $command);
$this->assertStringContainsString('.env.example', $command);
$this->assertStringContainsString('.env', $command);
} else {
$this->assertStringContainsString('cp', $command);
$this->assertStringContainsString('/', $command);
$this->assertStringContainsString('.env.example', $command);
}
}
/**
* getEnvCopyCommand()가 빈 경로에서도 동작하는지 확인합니다.
*/
public function test_get_env_copy_command_without_base_path(): void
{
$command = getEnvCopyCommand('');
if (isWindows()) {
$this->assertStringStartsWith('copy ', $command);
} else {
$this->assertStringStartsWith('cp ', $command);
}
$this->assertStringContainsString('.env.example', $command);
$this->assertStringContainsString('.env', $command);
}
/**
* getPermissionFixCommand()가 Windows에서 icacls 명령어를 반환하는지 확인합니다.
*
* 현재 시그니처: getPermissionFixCommand(string $pathList, string $mode = 'ownership')
* (이전 (owner, path, mode) 3-arg 에서 리팩토링되어 owner 인자 제거됨)
*/
public function test_get_permission_fix_command_ownership_mode(): void
{
$command = getPermissionFixCommand('/var/www/storage', 'ownership');
if (isWindows()) {
$this->assertStringContainsString('icacls', $command);
$this->assertStringContainsString('/grant', $command);
$this->assertStringContainsString('Everyone:(OI)(CI)F', $command);
$this->assertStringContainsString('/T', $command);
} else {
$this->assertStringContainsString('chmod -R 755 /var/www/storage', $command);
}
}
/**
* getPermissionFixCommand()가 file 모드에서 올바른 명령어를 반환하는지 확인합니다.
*/
public function test_get_permission_fix_command_file_mode(): void
{
$command = getPermissionFixCommand('/var/www/.env', 'file');
if (isWindows()) {
$this->assertStringContainsString('icacls', $command);
$this->assertStringContainsString('Everyone:F', $command);
$this->assertStringNotContainsString('/T', $command);
} else {
$this->assertStringContainsString('chmod 644 /var/www/.env', $command);
}
}
/**
* getPermissionFixCommand()가 Windows에서 경로 구분자를 백슬래시로 변환하는지 확인합니다.
*/
public function test_get_permission_fix_command_windows_path_separator(): void
{
if (! isWindows()) {
$this->markTestSkipped('Windows 전용 테스트');
}
$command = getPermissionFixCommand('/var/www/storage', 'ownership');
$this->assertStringContainsString('\\var\\www\\storage', $command);
$this->assertStringNotContainsString('/var/', $command);
}
/**
* getEnvCopyCommand()가 Windows에서 경로 구분자를 백슬래시로 변환하는지 확인합니다.
*/
public function test_get_env_copy_command_windows_path_separator(): void
{
if (! isWindows()) {
$this->markTestSkipped('Windows 전용 테스트');
}
$command = getEnvCopyCommand('C:/Users/test/htdocs/g7');
$this->assertStringContainsString('C:\\Users\\test\\htdocs\\g7', $command);
$this->assertStringNotContainsString('C:/Users', $command);
}
/**
* 다국어 파일에 permission_windows_hint 키가 존재하는지 확인합니다.
*/
public function test_translation_keys_exist(): void
{
$koTranslations = require $this->installerRoot().'/public/install/lang/ko.php';
$enTranslations = require $this->installerRoot().'/public/install/lang/en.php';
$this->assertArrayHasKey('permission_windows_hint', $koTranslations, 'ko.php에 permission_windows_hint 키가 없습니다');
$this->assertArrayHasKey('permission_windows_hint', $enTranslations, 'en.php에 permission_windows_hint 키가 없습니다');
$this->assertNotEmpty($koTranslations['permission_windows_hint']);
$this->assertNotEmpty($enTranslations['permission_windows_hint']);
}
/**
* 다국어 파일에 directory_create_guide 키가 존재하는지 확인합니다.
*/
public function test_directory_create_guide_translation_keys_exist(): void
{
$koTranslations = require $this->installerRoot().'/public/install/lang/ko.php';
$enTranslations = require $this->installerRoot().'/public/install/lang/en.php';
$this->assertArrayHasKey('directory_create_guide', $koTranslations, 'ko.php에 directory_create_guide 키가 없습니다');
$this->assertArrayHasKey('directory_create_guide', $enTranslations, 'en.php에 directory_create_guide 키가 없습니다');
$this->assertNotEmpty($koTranslations['directory_create_guide']);
$this->assertNotEmpty($enTranslations['directory_create_guide']);
}
}