markTestSkipped('POSIX 전용 — chmod/fileperms/umask 의미 제한'); } } /** * TARGETS 상수는 인스톨러 REQUIRED_DIRECTORIES (public/install/includes/config.php) * 및 config/app.php 의 restore_ownership_group_writable 기본값과 1:1 정렬되어야 한다. */ public function test_targets_constant_matches_config_default(): void { $reflection = new \ReflectionClassConstant(Upgrade_7_0_0_beta_3::class, 'TARGETS'); $targets = $reflection->getValue(); $expected = [ 'storage', 'bootstrap/cache', 'vendor', 'modules', 'modules/_pending', 'plugins', 'plugins/_pending', 'templates', 'templates/_pending', 'storage/app/core_pending', ]; $this->assertSame($expected, $targets, 'TARGETS 목록은 인스톨러 SSoT 와 동기화되어야 한다'); } /** * 동결된 TARGETS 는 살아있는 config 기본값과 의도적으로 갈라진다 (동결 규정 §7). * * beta.3 스텝은 실행 당시의 동작을 100% 보존해야 하므로 TARGETS 는 동결되어 있다. * 반면 config('app.update.restore_ownership_group_writable') 기본값은 #326 에서 * storage/app 사용자 데이터 보존을 위해 storage → storage/logs + storage/framework 로 * 세분화되고 lang-packs 등이 추가되며 정당하게 진화했다. * * 따라서 둘은 같을 수 없고 같아서도 안 된다. 이 테스트는 누군가 동결 스텝의 TARGETS 를 * 현재 config 에 맞추려 편집하는 동결 위반(또는 config 원복)을 차단한다. */ public function test_frozen_targets_intentionally_diverges_from_evolving_config(): void { $liveConfigDefault = array_values(config('app.update.restore_ownership_group_writable')); $reflection = new \ReflectionClassConstant(Upgrade_7_0_0_beta_3::class, 'TARGETS'); $targets = $reflection->getValue(); $this->assertNotSame( $liveConfigDefault, $targets, '동결 TARGETS 가 살아있는 config 와 동일해졌다면 동결 규정 위반이거나 config 가 되돌려진 것이다' ); // 동결 스텝은 세분화 이전 — storage 를 통째로 다뤘다. config 는 #326 에서 세분화됨. $this->assertContains('storage', $targets, '동결 TARGETS 는 세분화 이전의 storage 전체 항목을 유지한다'); $this->assertNotContains('storage/logs', $targets, '동결 TARGETS 에는 #326 의 세분화 항목이 유입되면 안 된다'); } /** * spawn 자식 탐지 — argv 에 `core:execute-upgrade-steps` 가 있으면 의도적으로 throw. * beta.2 의 spawnUpgradeStepsProcess 가 exit != 0 감지 → in-process fallback 유도. */ public function test_throws_when_running_inside_spawned_child(): void { $originalArgv = $_SERVER['argv'] ?? null; $_SERVER['argv'] = ['php', 'artisan', 'core:execute-upgrade-steps', '--from=7.0.0-beta.2', '--to=7.0.0-beta.3']; try { $step = new Upgrade_7_0_0_beta_3; $context = new UpgradeContext( fromVersion: '7.0.0-beta.2', toVersion: '7.0.0-beta.3', currentStep: '7.0.0-beta.3', ); $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/in-process/'); $step->run($context); } finally { if ($originalArgv === null) { unset($_SERVER['argv']); } else { $_SERVER['argv'] = $originalArgv; } } } /** * in-process 경로 (argv 에 core:update 등) 에서는 예외 없이 완료. * 현재 코드베이스의 storage 가 g+w 이든 g-w 이든 예외는 발생하지 않아야 한다 * (운영자 의도 존중 분기 또는 정상 실행 어느 쪽이든 정상 종료). */ public function test_run_completes_without_exception_in_process(): void { $originalArgv = $_SERVER['argv'] ?? null; $_SERVER['argv'] = ['php', 'artisan', 'core:update']; try { $step = new Upgrade_7_0_0_beta_3; $context = new UpgradeContext( fromVersion: '7.0.0-beta.2', toVersion: '7.0.0-beta.3', currentStep: '7.0.0-beta.3', ); $step->run($context); $this->assertTrue(true, 'run() 이 예외 없이 완료되었다'); } finally { if ($originalArgv === null) { unset($_SERVER['argv']); } else { $_SERVER['argv'] = $originalArgv; } } } /** * 경로 C 규율 준수 검증: 파일에서 `FilePermissionHelper::syncGroupWritability` 호출이 * 제거되었는지 확인. 경로 C 는 신설 메서드 호출 금지. */ public function test_does_not_call_new_file_permission_helper_method(): void { $source = file_get_contents(base_path('upgrades/Upgrade_7_0_0_beta_3.php')); // 호출 패턴(`::메서드명(`) 만 잡는다. docblock 설명 텍스트는 허용. $this->assertStringNotContainsString( 'FilePermissionHelper::syncGroupWritability(', $source, '경로 C 규율: beta.3 신설 메서드 호출 금지. 로컬 private 메서드로 인라인되어야 한다' ); // use 문도 제거되어야 한다 (의존성 최소화) $this->assertStringNotContainsString( 'use App\Extension\Helpers\FilePermissionHelper;', $source, 'FilePermissionHelper import 제거 — 경로 C 는 인라인 로직만 사용' ); $this->assertStringContainsString( '@upgrade-path C', $source, 'docblock 에 @upgrade-path C 선언이 있어야 한다' ); } /** * in-process 실행 시 storage g+w 환경에서는 umask 가 0002 로 전환되어야 한다. */ public function test_sets_umask_to_0002_when_storage_has_group_write(): void { $this->assertPosixOrSkip(); // storage 가 g+w 인 경우에만 의미. 테스트 환경 storage 퍼미션 확인. $storagePath = base_path('storage'); if (! is_dir($storagePath)) { $this->markTestSkipped('storage/ 디렉토리 미존재'); } $perms = fileperms($storagePath); if (($perms & 0020) === 0) { $this->markTestSkipped('테스트 환경 storage 가 g-w — 운영자 의도 존중 스킵 분기라 검증 불가'); } $originalUmask = umask(0022); $originalArgv = $_SERVER['argv'] ?? null; $_SERVER['argv'] = ['php', 'artisan', 'core:update']; try { $step = new Upgrade_7_0_0_beta_3; $context = new UpgradeContext( fromVersion: '7.0.0-beta.2', toVersion: '7.0.0-beta.3', currentStep: '7.0.0-beta.3', ); $step->run($context); $this->assertSame(0002, umask(), 'in-process 실행 후 umask 는 0002 로 전환되어야 한다'); } finally { umask($originalUmask); if ($originalArgv === null) { unset($_SERVER['argv']); } else { $_SERVER['argv'] = $originalArgv; } } } }