*/ private array $tempDirs = []; protected function tearDown(): void { foreach ($this->tempDirs as $dir) { if (File::isDirectory($dir)) { File::deleteDirectory($dir); } } parent::tearDown(); } private function createTempDir(): string { $dir = storage_path('test_settings_owner_'.uniqid()); File::ensureDirectoryExists($dir); $this->tempDirs[] = $dir; return $dir; } /** * inheritOwnershipFromParent 가 public static 으로 노출되어야 함. * * 변경 7-(a): `SettingsMigrator::writeJsonFile` 같은 외부 호출처가 본 helper 를 * 사용해 부모 owner/group 을 상속하도록 노출. * * @return void */ public function test_inherit_ownership_from_parent_is_public(): void { $reflection = new \ReflectionMethod(FilePermissionHelper::class, 'inheritOwnershipFromParent'); $this->assertTrue( $reflection->isPublic(), 'FilePermissionHelper::inheritOwnershipFromParent 가 public 이어야 외부 호출 가능 (SettingsMigrator 등)', ); $this->assertTrue($reflection->isStatic(), 'static 메서드여야 함'); } /** * inheritOwnershipFromParent — 부모 디렉토리 stat 기반으로 owner/group 적용. * * POSIX 환경 전용. 일반 user 권한에서는 다른 user 로 chown 시도가 실패할 수 있으나, * 자기 자신 owner 인 경우 chown 자체가 발생하지 않으므로 멱등. * * @return void */ public function test_inherit_ownership_from_parent_idempotent_for_self_owner(): void { if (DIRECTORY_SEPARATOR !== '/' || ! function_exists('chown')) { $this->markTestSkipped('POSIX 환경 전용 (Windows 자동 스킵)'); } $parent = $this->createTempDir(); $child = $parent.'/child.json'; file_put_contents($child, '{}'); $parentOwnerBefore = fileowner($parent); $childOwnerBefore = fileowner($child); // 자기 자신 owner 면 부모와 자식 owner 가 동일 → 멱등 FilePermissionHelper::inheritOwnershipFromParent($child); $this->assertSame($parentOwnerBefore, fileowner($parent)); $this->assertSame($childOwnerBefore, fileowner($child)); $this->assertSame($parentOwnerBefore, fileowner($child), '자식 owner 가 부모와 동일'); } /** * settings 디렉토리 생성도 파일과 대칭으로 부모 소유권을 상속한다 (#651 B1). * * 파일(`writeJsonFile`)만 상속하고 디렉토리(`makeDirectory`)는 상속하지 않던 비대칭 — sudo * 업그레이드 스텝이 디렉토리를 root 로 만들면 `storage/app/{modules,plugins}` 는 restore_ownership * 의도적 제외 경로라 되돌려지지 않고, 이후 웹 프로세스의 그 모듈 설정 저장이 영구 실패한다. * * @effects settings_directory_seed_inherits_ownership */ public function test_settings_migrator_make_directory_invokes_inherit_ownership(): void { $body = (string) file_get_contents((new \ReflectionClass(SettingsMigrator::class))->getFileName()); $pos = strpos($body, 'File::makeDirectory($settingsDir'); $this->assertNotFalse($pos, 'SettingsMigrator 의 settings 디렉토리 생성 지점을 찾지 못했다'); $window = implode("\n", array_slice(explode("\n", substr($body, $pos)), 0, 4)); $this->assertStringContainsString( 'FilePermissionHelper::inheritOwnershipFromParent($settingsDir)', $window, 'SettingsMigrator: settings 디렉토리 생성 뒤 부모 소유권 상속이 없다 (파일만 상속하는 비대칭)' ); } /** * SettingsMigrator::writeJsonFile 호출 후 *.json 의 owner 가 부모와 일치. * * `SettingsMigrator` 는 protected 메서드 + non-static 이라 직접 호출이 어려우므로, * 핵심 통합 보장 = "writeJsonFile 메서드 안에서 inheritOwnershipFromParent 호출" 을 * reflection 으로 검증. * * @return void */ public function test_settings_migrator_write_json_file_invokes_inherit_ownership(): void { $reflection = new \ReflectionClass(SettingsMigrator::class); $method = $reflection->getMethod('writeJsonFile'); $body = file_get_contents($method->getFileName()); // writeJsonFile 메서드 내부에 inheritOwnershipFromParent 호출이 존재해야 함 $startLine = $method->getStartLine(); $endLine = $method->getEndLine(); $methodBody = implode("\n", array_slice( explode("\n", $body), $startLine - 1, $endLine - $startLine + 1 )); $this->assertStringContainsString( 'inheritOwnershipFromParent', $methodBody, 'SettingsMigrator::writeJsonFile 가 sudo update 시 root 로 만든 *.json 의 owner 를 부모로 상속해야 함 (FilePermissionHelper::inheritOwnershipFromParent 호출)', ); } }