Files
Gnuboard7/tests/Unit/Installer/ScanLanguagePacksTest.php
T
2026-05-11 11:29:41 +09:00

331 lines
12 KiB
PHP

<?php
namespace Tests\Unit\Installer;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunClassInSeparateProcess;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* 인스톨러 — 번들 언어팩 스캔 단위 테스트.
*
* `public/install/api/scan-extensions.php` 의 `scanLanguagePacks()` 함수를
* 라이브러리 모드(SCAN_EXTENSIONS_LIBRARY) 로 require 하여 직접 호출.
* Laravel 부팅 없이 BASE_PATH 만 격리된 디렉토리로 정의해 매니페스트 스캔
* 결과를 검증한다.
*
* BASE_PATH 는 PHP 상수이므로 클래스 라이프사이클 단위로 단 한 번 정의 (setUpBeforeClass).
* 각 테스트는 setUp 에서 BASE_PATH 하위의 lang-packs/_bundled 를 비워 초기화한다.
*
* 검증 항목:
* - language-pack.json 매니페스트가 응답 항목으로 노출됨
* - scope/target_identifier/locale 필드가 보존됨
* - hidden=true 매니페스트는 제외됨
* - 동일 디렉토리에 다수 패키지(코어 + 확장 + 동일 확장의 다중 locale)가 모두 노출됨
*/
#[RunClassInSeparateProcess]
#[PreserveGlobalState(false)]
class ScanLanguagePacksTest extends TestCase
{
private static string $sharedBase = '';
private static string $skipReason = '';
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
// 안전 가드: BASE_PATH 가 시스템 temp 디렉토리 하위가 아니면 (= 다른 테스트가 프로젝트 루트로 먼저 박은 경우)
// setUp/tearDown 의 enumerate-delete 가 실제 lang-packs/_bundled 를 파괴할 수 있으므로 skip.
$tempPrefix = realpath(sys_get_temp_dir()) ?: sys_get_temp_dir();
if (defined('BASE_PATH')) {
$resolved = realpath((string) BASE_PATH) ?: (string) BASE_PATH;
if (strpos($resolved, $tempPrefix) !== 0) {
self::$skipReason = 'BASE_PATH ('.$resolved.') 가 시스템 temp 하위가 아님 — '.
'다른 Installer 테스트의 BASE_PATH 정의가 선행됨. 격리 실행 필요: '.
'php vendor/bin/phpunit --filter=ScanLanguagePacksTest';
return;
}
self::$sharedBase = (string) BASE_PATH;
} else {
self::$sharedBase = sys_get_temp_dir().'/g7-installer-scanlp-test-'.bin2hex(random_bytes(4));
define('BASE_PATH', self::$sharedBase);
}
$bundleDir = self::$sharedBase.'/lang-packs/_bundled';
if (! is_dir($bundleDir)) {
mkdir($bundleDir, 0755, true);
}
if (! defined('SCAN_EXTENSIONS_LIBRARY')) {
define('SCAN_EXTENSIONS_LIBRARY', true);
}
require_once dirname(__DIR__, 3).'/public/install/api/scan-extensions.php';
}
public static function tearDownAfterClass(): void
{
// skipReason 이 설정되어 있으면 sharedBase 가 미초기화 — destructive cleanup 회피
if (self::$skipReason === '' && self::$sharedBase !== '') {
$bundleDir = self::$sharedBase.'/lang-packs/_bundled';
if (is_dir($bundleDir)) {
foreach (scandir($bundleDir) as $e) {
if ($e === '.' || $e === '..') {
continue;
}
self::removeRecursiveStatic($bundleDir.DIRECTORY_SEPARATOR.$e);
}
}
}
parent::tearDownAfterClass();
}
protected function setUp(): void
{
parent::setUp();
if (self::$skipReason !== '') {
$this->markTestSkipped(self::$skipReason);
}
// 각 테스트 시작 시 lang-packs/_bundled 디렉토리 초기화
$bundle = self::$sharedBase.'/lang-packs/_bundled';
if (is_dir($bundle)) {
foreach (scandir($bundle) as $e) {
if ($e === '.' || $e === '..') {
continue;
}
self::removeRecursiveStatic($bundle.DIRECTORY_SEPARATOR.$e);
}
} else {
mkdir($bundle, 0755, true);
}
}
#[Test]
public function scan_returns_empty_when_directory_empty(): void
{
$result = scanLanguagePacks();
$this->assertSame([], $result);
}
#[Test]
public function scan_includes_core_and_extension_packs(): void
{
$this->writeManifest('g7-core-ja', [
'identifier' => 'g7-core-ja',
'scope' => 'core',
'target_identifier' => null,
'locale' => 'ja',
'locale_native_name' => '日本語',
'name' => ['ko' => 'G7 Core Japanese', 'en' => 'G7 Core Japanese'],
'version' => '1.0.0',
]);
$this->writeManifest('g7-module-board-ja', [
'identifier' => 'g7-module-board-ja',
'scope' => 'module',
'target_identifier' => 'sirsoft-board',
'locale' => 'ja',
'locale_native_name' => '日本語',
'name' => 'Board JP',
'version' => '1.0.0',
]);
$result = scanLanguagePacks();
$ids = array_column($result, 'identifier');
sort($ids);
$this->assertSame(['g7-core-ja', 'g7-module-board-ja'], $ids);
$core = $this->findById($result, 'g7-core-ja');
$this->assertSame('core', $core['scope']);
$this->assertNull($core['target_identifier']);
$this->assertSame('ja', $core['locale']);
$this->assertSame('日本語', $core['locale_native_name']);
$module = $this->findById($result, 'g7-module-board-ja');
$this->assertSame('module', $module['scope']);
$this->assertSame('sirsoft-board', $module['target_identifier']);
}
#[Test]
public function scan_excludes_hidden_manifests(): void
{
$this->writeManifest('g7-sample-hidden-ja', [
'identifier' => 'g7-sample-hidden-ja',
'scope' => 'core',
'locale' => 'ja',
'name' => 'Hidden Sample',
'version' => '1.0.0',
'hidden' => true,
]);
$this->writeManifest('g7-core-ja', [
'identifier' => 'g7-core-ja',
'scope' => 'core',
'locale' => 'ja',
'name' => 'G7 Core Japanese',
'version' => '1.0.0',
]);
$result = scanLanguagePacks();
$ids = array_column($result, 'identifier');
$this->assertContains('g7-core-ja', $ids);
$this->assertNotContains('g7-sample-hidden-ja', $ids);
}
#[Test]
public function scan_supports_multiple_locales_for_same_extension(): void
{
$this->writeManifest('g7-module-board-ja', [
'identifier' => 'g7-module-board-ja',
'scope' => 'module',
'target_identifier' => 'sirsoft-board',
'locale' => 'ja',
'name' => 'Board JP',
'version' => '1.0.0',
]);
$this->writeManifest('g7-module-board-zh-CN', [
'identifier' => 'g7-module-board-zh-CN',
'scope' => 'module',
'target_identifier' => 'sirsoft-board',
'locale' => 'zh-CN',
'name' => 'Board CN',
'version' => '1.0.0',
]);
$result = scanLanguagePacks();
$this->assertCount(2, $result);
$locales = array_column($result, 'locale');
sort($locales);
$this->assertSame(['ja', 'zh-CN'], $locales);
foreach ($result as $row) {
$this->assertSame('sirsoft-board', $row['target_identifier']);
}
}
#[Test]
public function scan_excludes_packs_targeting_hidden_host_extensions(): void
{
// hidden 처리된 모듈 매니페스트 작성 (학습용 샘플 시뮬레이션)
$modulesDir = self::$sharedBase.'/modules/_bundled/sample-hidden-module';
if (! is_dir($modulesDir)) {
mkdir($modulesDir, 0755, true);
}
file_put_contents($modulesDir.'/module.json', json_encode([
'identifier' => 'sample-hidden-module',
'name' => ['ko' => '샘플 모듈', 'en' => 'Sample Module'],
'version' => '1.0.0',
'hidden' => true,
], JSON_UNESCAPED_UNICODE));
// 일반 모듈 매니페스트
$normalDir = self::$sharedBase.'/modules/_bundled/sirsoft-board';
if (! is_dir($normalDir)) {
mkdir($normalDir, 0755, true);
}
file_put_contents($normalDir.'/module.json', json_encode([
'identifier' => 'sirsoft-board',
'name' => ['ko' => '게시판', 'en' => 'Board'],
'version' => '1.0.0',
], JSON_UNESCAPED_UNICODE));
// hidden 모듈 종속 언어팩
$this->writeManifest('g7-module-sample-hidden-module-ja', [
'identifier' => 'g7-module-sample-hidden-module-ja',
'scope' => 'module',
'target_identifier' => 'sample-hidden-module',
'locale' => 'ja',
'name' => 'Sample Hidden JP',
'version' => '1.0.0',
]);
// 정상 모듈 종속 언어팩
$this->writeManifest('g7-module-sirsoft-board-ja', [
'identifier' => 'g7-module-sirsoft-board-ja',
'scope' => 'module',
'target_identifier' => 'sirsoft-board',
'locale' => 'ja',
'name' => 'Board JP',
'version' => '1.0.0',
]);
$result = scanLanguagePacks();
$ids = array_column($result, 'identifier');
$this->assertContains('g7-module-sirsoft-board-ja', $ids);
$this->assertNotContains('g7-module-sample-hidden-module-ja', $ids, 'hidden 호스트 확장 종속 언어팩은 cascade 제외되어야 함');
// fixture 정리
@unlink($modulesDir.'/module.json');
@rmdir($modulesDir);
@unlink($normalDir.'/module.json');
@rmdir($normalDir);
}
#[Test]
public function scan_skips_directory_without_manifest(): void
{
mkdir(self::$sharedBase.'/lang-packs/_bundled/g7-orphan-ja', 0755, true);
$this->writeManifest('g7-core-ja', [
'identifier' => 'g7-core-ja',
'scope' => 'core',
'locale' => 'ja',
'name' => 'G7 Core Japanese',
'version' => '1.0.0',
]);
$result = scanLanguagePacks();
$this->assertCount(1, $result);
$this->assertSame('g7-core-ja', $result[0]['identifier']);
}
private function writeManifest(string $directory, array $data): void
{
$dir = self::$sharedBase.'/lang-packs/_bundled/'.$directory;
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($dir.'/language-pack.json', json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
private function findById(array $rows, string $id): array
{
foreach ($rows as $row) {
if ($row['identifier'] === $id) {
return $row;
}
}
$this->fail("Identifier {$id} not found in scan result");
}
private static function removeRecursiveStatic(string $path): void
{
if (! file_exists($path) && ! is_link($path)) {
return;
}
if (is_file($path) || is_link($path)) {
@unlink($path);
return;
}
$entries = scandir($path);
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
self::removeRecursiveStatic($path.DIRECTORY_SEPARATOR.$entry);
}
@rmdir($path);
}
}