Files
Gnuboard7/tests/Feature/LanguagePack/HardcodedLocaleRegressionTest.php
2026-05-11 11:29:41 +09:00

239 lines
9.6 KiB
PHP

<?php
namespace Tests\Feature\LanguagePack;
use App\Enums\ExtensionOwnerType;
use App\Enums\LanguagePackScope;
use App\Enums\LanguagePackSourceType;
use App\Enums\LanguagePackStatus;
use App\Models\LanguagePack;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Providers\LanguagePackServiceProvider;
use App\Services\DriverRegistryService;
use App\Services\LanguagePack\LanguagePackRegistry;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
/**
* 이슈 #263 후속 — 활성 언어팩 locale(ja 등)이 백엔드 검증 / 라벨 빌더에서
* 거부되거나 누락되지 않음을 보장하는 회귀 테스트.
*
* 검증 대상 5건:
* 1. SaveSettingsRequest::SUPPORTED_LANGUAGES 상수 제거 → general.language=ja 저장 통과
* 2. ProfileController::updateLanguage 인라인 화이트리스트 → ja 통과
* 3. DriverRegistryService 라벨이 활성 translatable_locales 별 lang key 에서 동적 조회
* 4. AuthService Accept-Language 국가 매핑이 config('app.locale_country_fallback') 사용
* 5. ModuleManager / PluginManager 의 string→JSON 승격이 translatable_locales 기반
*/
class HardcodedLocaleRegressionTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private string $token;
protected function setUp(): void
{
parent::setUp();
// ja 코어 언어팩 활성화 → config('app.supported_locales')/translatable_locales 가 ja 포함하도록
LanguagePack::create([
'identifier' => 'g7-core-ja-test',
'vendor' => 'g7',
'name' => 'G7 Core Japanese (test)',
'version' => '1.0.0',
'scope' => LanguagePackScope::Core->value,
'target_identifier' => null,
'locale' => 'ja',
'locale_name' => 'Japanese',
'locale_native_name' => '日本語',
'text_direction' => 'ltr',
'is_protected' => false,
'manifest' => [
'identifier' => 'g7-core-ja-test',
'scope' => 'core',
'locale' => 'ja',
'requires' => ['depends_on_core_locale' => false],
],
'status' => LanguagePackStatus::Active->value,
'source_type' => LanguagePackSourceType::Bundled->value,
'installed_at' => now(),
'activated_at' => now(),
]);
// Registry 의 인스턴스 캐시를 무효화 (setUp 이전에 boot 된 싱글톤 회피)
app()->forgetInstance(LanguagePackRegistry::class);
$registry = app(LanguagePackRegistry::class);
config([
'app.supported_locales' => $registry->getActiveCoreLocales(),
'app.locale_names' => $registry->getLocaleNames(),
'app.translatable_locales' => $registry->getActiveCoreLocales(),
]);
$this->admin = $this->createAdminUser();
$this->token = $this->admin->createToken('test-token')->plainTextToken;
}
/**
* #1 회귀: 기본 언어 = ja 로 환경설정 저장 시 422 거부되지 않아야 한다.
*/
public function test_save_settings_accepts_active_locale_ja(): void
{
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->token,
'Accept' => 'application/json',
])->postJson('/api/admin/settings', [
'_tab' => 'general',
'general' => [
'site_name' => 'Test',
'site_url' => 'https://test.example.com',
'admin_email' => 'admin@example.com',
'timezone' => 'Asia/Seoul',
'language' => 'ja',
],
]);
$response->assertStatus(200);
$this->assertArrayNotHasKey('general.language', (array) $response->json('errors'));
}
/**
* #2 회귀: 프로필 언어 변경 API 가 활성 언어팩 ja 를 거부하지 않아야 한다.
*/
public function test_profile_update_language_accepts_active_locale_ja(): void
{
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->token,
'Accept' => 'application/json',
])->patchJson('/api/admin/users/me/language', [
'language' => 'ja',
]);
$response->assertStatus(200);
$this->assertSame('ja', $this->admin->fresh()->language);
}
/**
* #3 회귀: DriverRegistryService 가 활성 translatable_locales 모든 키에 대해 라벨을 반환해야 한다.
*/
public function test_driver_registry_returns_labels_for_all_active_locales(): void
{
$service = app(DriverRegistryService::class);
$drivers = $service->getAvailableDrivers('storage');
$this->assertNotEmpty($drivers);
foreach ($drivers as $driver) {
$this->assertArrayHasKey('label', $driver);
$this->assertArrayHasKey('ko', $driver['label']);
$this->assertArrayHasKey('en', $driver['label']);
$this->assertArrayHasKey('ja', $driver['label'], 'ja 활성 언어팩에 대한 라벨이 누락되어서는 안 됨');
}
// 'local' 드라이버의 ja 라벨이 lang-packs/_bundled/g7-core-ja/backend/ja/settings.php 의 키와 일치해야 함
$local = collect($drivers)->firstWhere('id', 'local');
$this->assertSame('로컬', $local['label']['ko']);
$this->assertSame('Local', $local['label']['en']);
}
/**
* #4 회귀: AuthService Accept-Language 국가 매핑이 config 기반으로 동작.
*/
public function test_auth_service_country_fallback_is_config_driven(): void
{
$this->assertSame('JP', config('app.locale_country_fallback.ja'));
$this->assertSame('KR', config('app.locale_country_fallback.ko'));
$this->assertSame('US', config('app.locale_country_fallback.en'));
}
/**
* 회귀: LanguagePackServiceProvider 가 활성 언어팩 boot 시 supported_locales 뿐 아니라
* translatable_locales 도 함께 갱신해야 한다 (모듈/플러그인의 TranslatableField /
* LocaleRequiredTranslatable Rule 이 ja 를 거부하던 회귀 차단).
*/
public function test_translatable_field_rule_accepts_active_locale_ja(): void
{
// setUp 이 수동 주입한 config 를 ko/en 만으로 되돌려, provider 의 refresh 로직만 검증.
config([
'app.supported_locales' => ['ko', 'en'],
'app.translatable_locales' => ['ko', 'en'],
]);
// provider 의 private refresh 메서드를 reflection 으로 호출 (boot 시점 시뮬레이션).
$provider = new \App\Providers\LanguagePackServiceProvider($this->app);
app()->forgetInstance(LanguagePackRegistry::class);
$registry = app(LanguagePackRegistry::class);
$ref = new \ReflectionClass($provider);
$method = $ref->getMethod('refreshSupportedLocales');
$method->setAccessible(true);
$method->invoke($provider, $registry);
$this->assertContains('ja', config('app.supported_locales'));
$this->assertContains('ja', config('app.translatable_locales'),
'translatable_locales 에도 활성 언어팩 ja 가 동기화되어야 함 (모듈 폼이 ja 를 거부하지 않도록)');
// Rule 동작도 함께 검증 — ja 데이터가 거부되지 않아야 함.
$rule = new \App\Rules\TranslatableField;
$failed = null;
$rule->validate('name', ['ja' => 'テスト共通情報'], function ($msg) use (&$failed) {
$failed = $msg;
});
$this->assertNull($failed, 'TranslatableField 가 활성 언어팩 locale 을 거부하면 안 됨');
}
/**
* #5 회귀: ModuleManager / PluginManager 의 string→JSON 승격이
* 활성 translatable_locales 의 모든 키를 포함해야 한다.
*/
public function test_translatable_promotion_covers_all_active_locales(): void
{
$name = 'Sample Permission';
$promoted = array_fill_keys(config('app.translatable_locales', ['ko', 'en']), $name);
$this->assertArrayHasKey('ko', $promoted);
$this->assertArrayHasKey('en', $promoted);
$this->assertArrayHasKey('ja', $promoted, 'translatable_locales 기반 승격이 ja 키를 포함해야 함');
$this->assertSame($name, $promoted['ja']);
}
private function createAdminUser(): User
{
$user = User::factory()->create([
'password' => Hash::make('password123'),
'language' => 'ko',
]);
$permissionIds = [];
foreach (['core.settings.read', 'core.settings.update'] as $identifier) {
$perm = Permission::firstOrCreate(
['identifier' => $identifier],
[
'name' => json_encode(['ko' => $identifier, 'en' => $identifier]),
'description' => json_encode(['ko' => $identifier, 'en' => $identifier]),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => 'admin',
]
);
$permissionIds[] = $perm->id;
}
$role = Role::firstOrCreate(
['identifier' => 'test-admin'],
[
'name' => json_encode(['ko' => '테스트관리자', 'en' => 'Test Admin']),
'description' => json_encode(['ko' => '테스트', 'en' => 'Test']),
'is_system' => false,
]
);
$role->permissions()->sync($permissionIds);
$user->roles()->sync([$role->id]);
return $user;
}
}