Files
Gnuboard7/tests/Feature/Extension/ExtensionPendingOwnershipTest.php
T
HeuJung b4ed33aa44 feat(core): 정적 게시 수명주기 보완 — 빌드 파괴·권한·무결성
https://github.com/gnuboard/g7/issues/122 제보자의 추가 지적 2건(빌드가 게시본을
지움 / CLI 최초 게시 후 웹 재생성 불가)에 대응하고, 같은 근본 원인을 공유하는
결함을 전수조사로 함께 고친다.

- 빌드가 자기 산출물만 교체한다: 루트 vite `emptyOutDir: false`
 기본값 true 는 폴백 없는 코어 3번들과 배달된 immutable URL 의 게시본을 함께 지웠다
- 게시 트리 권한을 웹이 이어받는다: 병합 전 프리플라이트 + 부모 그룹 상속·g+w
 종전 소유권 정상화는 root 축만 덮어 비-root CLI 계정은 무방비였다
- 갱신 → 생성 → 완전성 확인을 한 묶음으로: 기록 바이트 대조, .old 원자 스왑,
 rename 일시 거부 재시도, 프론트 JSON 파싱 검증
- 실패를 억제하고 기록한다: 버전 한정 실패 마커 + 사유별 대시보드 알림
 + ext-static:status 점검 커맨드
- 파생: catch-all 제외 목록을 에셋 화이트리스트 합집합에서 파생(mjs·webp·otf 누락),
 번들 디스크 쓰기 fail-soft·빈 번들 503, 활성 디렉토리 prune 회피
2026-08-28 08:02:22 +09:00

142 lines
5.4 KiB
PHP

<?php
namespace Tests\Feature\Extension;
use App\Extension\Helpers\ExtensionBackupHelper;
use App\Extension\Helpers\ExtensionPendingHelper;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* 확장 설치/업데이트 복사의 디렉토리 소유권 상속 테스트 (#122 P3).
*
* 파일은 `FilePermissionHelper::copyFile` 이 부모 소유권을 상속시키는데 **디렉토리는
* 그렇지 않았다.** sudo 로 실행된 설치/업데이트가 만든 디렉토리가 root 소유로 남으면
* 이후 웹 프로세스의 쓰기가 그 디렉토리에서 막힌다. 형제 구현
* `ExtensionBackupHelper::copyDirectoryWithProgress` 는 이미 같은 방어를 갖고 있었다
* (계층 불균형).
*
* 소유권 자체는 CI/개발 환경에서 단일 계정이라 값으로 단언할 수 없다 — 그래서 **복사
* 경로 세 곳 모두가 상속 호출을 갖는지**를 소스에서 검사하고, 복사 자체가 정상 동작하는지를
* 실제 파일시스템으로 확인한다. 한 경로만 고치면 나머지가 조용히 옛 동작으로 남기 때문이다.
*/
class ExtensionPendingOwnershipTest extends TestCase
{
private string $root;
protected function setUp(): void
{
parent::setUp();
$this->root = storage_path('framework/testing/pending-ownership');
File::deleteDirectory($this->root);
}
protected function tearDown(): void
{
File::deleteDirectory($this->root);
parent::tearDown();
}
/**
* 디렉토리를 만드는 복사 경로 전부가 부모 소유권을 상속시킨다.
*
* 대상 3곳: `copyDirectoryWithProgress`(rename 경로) / `syncDirectoryContents` ·
* `overlayDirectory`(제자리 동기화 폴백). 폴백만 빠뜨리면 파일 잠금으로 그 경로로
* 떨어진 교체에서만 소유권이 어긋나, Windows 잠금 상황에서만 재현되는 결함이 된다.
*
* @effects extension_copy_paths_inherit_directory_ownership
*/
public function test_every_directory_creating_copy_path_inherits_ownership(): void
{
$source = (new \ReflectionClass(ExtensionPendingHelper::class))->getFileName();
$content = (string) file_get_contents($source);
$methods = ['copyDirectoryWithProgress', 'syncDirectoryContents', 'overlayDirectory'];
foreach ($methods as $method) {
$body = $this->methodBody($content, $method);
$this->assertNotSame('', $body, "{$method} 을 소스에서 찾지 못했다 — 검사가 공허하다");
$this->assertStringContainsString(
'ensureDirectoryExists($dest',
$body,
"{$method} 이 디렉토리를 만들지 않는다 — 대상 목록이 낡았다"
);
$this->assertStringContainsString(
'FilePermissionHelper::inheritOwnershipFromParent($dest)',
$body,
"{$method} 이 디렉토리 소유권을 상속시키지 않는다 — sudo 설치 후 root 소유로 남는다"
);
}
}
/**
* 형제 구현(`ExtensionBackupHelper`)과 방어 수준이 같다 — 계층 불균형이 남지 않는다.
*
* @effects extension_copy_ownership_matches_sibling_helper
*/
public function test_matches_sibling_backup_helper_defense(): void
{
$sibling = (string) file_get_contents(
(new \ReflectionClass(ExtensionBackupHelper::class))->getFileName()
);
$this->assertStringContainsString(
'FilePermissionHelper::inheritOwnershipFromParent($dest)',
$sibling,
'기준으로 삼은 형제 구현이 바뀌었다 — 이 테스트의 전제를 다시 확인할 것'
);
}
/**
* 상속 호출을 추가한 뒤에도 실제 복사가 정상 동작한다 (회귀 가드).
*
* @effects extension_copy_still_produces_expected_tree
*/
public function test_copy_still_produces_expected_tree(): void
{
$source = $this->root.'/src';
$dest = $this->root.'/dest';
File::ensureDirectoryExists($source.'/nested/deep');
File::put($source.'/root.txt', 'a');
File::put($source.'/nested/deep/leaf.txt', 'b');
$method = new \ReflectionMethod(ExtensionPendingHelper::class, 'copyDirectoryWithProgress');
$method->setAccessible(true);
$method->invoke(null, $source, $dest, $source, null);
$this->assertFileExists($dest.'/root.txt');
$this->assertFileExists($dest.'/nested/deep/leaf.txt');
$this->assertSame('b', file_get_contents($dest.'/nested/deep/leaf.txt'));
}
/**
* 메서드 본문을 대략 잘라냅니다 (다음 메서드 선언 직전까지).
*
* @param string $content 파일 전문
* @param string $method 메서드명
* @return string 본문 (못 찾으면 빈 문자열)
*/
private function methodBody(string $content, string $method): string
{
$start = strpos($content, "function {$method}(");
if ($start === false) {
return '';
}
$rest = substr($content, $start);
$next = preg_match(
'/\n (?:public|protected|private)\s+(?:static\s+)?function\s/',
substr($rest, 1),
$m,
PREG_OFFSET_CAPTURE
);
return $next === 1 ? substr($rest, 0, $m[0][1] + 1) : $rest;
}
}