합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다. 쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다. 레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다. 되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스 23줄)은 재적용했다. 성능 개선분은 전부 잔존한다. 통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가 주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로 보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다. 확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을 막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다. 같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고, 코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다. 상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를 지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는 대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다. 환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두 있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를 설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다. 범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다. 언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가 HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
1043 lines
44 KiB
PHP
1043 lines
44 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Services\LanguagePack;
|
|
|
|
use App\Enums\LanguagePackScope;
|
|
use App\Enums\LanguagePackStatus;
|
|
use App\Exceptions\LanguagePackOperationException;
|
|
use App\Extension\HookManager;
|
|
use App\Models\LanguagePack;
|
|
use App\Services\LanguagePack\LanguagePackRegistry;
|
|
use App\Services\LanguagePackService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* LanguagePackService 단위 테스트.
|
|
*
|
|
* 슬롯 스위칭, 자동 승격, 비활성화 후 후속 활성 등 활성/비활성 사이클의 핵심 동작을 검증합니다.
|
|
*/
|
|
class LanguagePackServiceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private LanguagePackService $service;
|
|
|
|
private LanguagePackRegistry $registry;
|
|
|
|
/**
|
|
* 테스트 픽스처 초기화.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->service = $this->app->make(LanguagePackService::class);
|
|
$this->registry = $this->app->make(LanguagePackRegistry::class);
|
|
}
|
|
|
|
/**
|
|
* 테스트 언어팩 1건을 DB 에 직접 생성합니다.
|
|
*
|
|
* @param string $vendor 벤더
|
|
* @param string $locale 로케일
|
|
* @param string $status 상태
|
|
* @return LanguagePack 생성된 언어팩
|
|
*/
|
|
private function makePack(string $vendor, string $locale, string $status = LanguagePackStatus::Installed->value): LanguagePack
|
|
{
|
|
return LanguagePack::query()->create([
|
|
'identifier' => sprintf('%s-core-%s', $vendor, $locale),
|
|
'vendor' => $vendor,
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => $locale,
|
|
'locale_name' => strtoupper($locale),
|
|
'locale_native_name' => $locale,
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0',
|
|
'status' => $status,
|
|
'is_protected' => false,
|
|
'manifest' => [],
|
|
]);
|
|
}
|
|
|
|
public function test_activate_promotes_inactive_to_active(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Inactive->value);
|
|
|
|
$result = $this->service->activate($pack);
|
|
|
|
$this->assertSame(LanguagePackStatus::Active->value, $result->status);
|
|
$this->assertNotNull($result->activated_at);
|
|
}
|
|
|
|
public function test_activate_demotes_existing_active_in_same_slot(): void
|
|
{
|
|
$existing = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$candidate = $this->makePack('acme', 'ja', LanguagePackStatus::Installed->value);
|
|
|
|
// 슬롯 충돌은 force=true 로 명시적 교체 의사 확인 후 demotion 수행 (기본은 SlotConflictException).
|
|
$this->service->activate($candidate, force: true);
|
|
|
|
$existing->refresh();
|
|
$this->assertSame(LanguagePackStatus::Inactive->value, $existing->status);
|
|
$this->assertSame(LanguagePackStatus::Active->value, $candidate->fresh()->status);
|
|
}
|
|
|
|
public function test_activate_is_idempotent_for_already_active_pack(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$originalActivatedAt = $pack->activated_at;
|
|
|
|
$result = $this->service->activate($pack);
|
|
|
|
$this->assertSame(LanguagePackStatus::Active->value, $result->status);
|
|
$this->assertEquals($originalActivatedAt, $result->activated_at);
|
|
}
|
|
|
|
public function test_deactivate_promotes_slot_successor(): void
|
|
{
|
|
$active = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$other = $this->makePack('acme', 'ja', LanguagePackStatus::Inactive->value);
|
|
|
|
$this->service->deactivate($active);
|
|
|
|
$this->assertSame(LanguagePackStatus::Inactive->value, $active->fresh()->status);
|
|
$this->assertSame(LanguagePackStatus::Active->value, $other->fresh()->status);
|
|
}
|
|
|
|
public function test_deactivate_protected_pack_throws(): void
|
|
{
|
|
$pack = LanguagePack::query()->create([
|
|
'identifier' => 'g7-core-ko',
|
|
'vendor' => 'g7',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'ko',
|
|
'locale_name' => 'Korean',
|
|
'locale_native_name' => '한국어',
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0',
|
|
'status' => LanguagePackStatus::Active->value,
|
|
'is_protected' => true,
|
|
'manifest' => [],
|
|
]);
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->service->deactivate($pack);
|
|
}
|
|
|
|
public function test_uninstall_removes_db_record(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Inactive->value);
|
|
$id = $pack->id;
|
|
|
|
$this->service->uninstall($pack, false);
|
|
|
|
$this->assertNull(LanguagePack::query()->find($id));
|
|
}
|
|
|
|
public function test_uninstall_protected_pack_throws(): void
|
|
{
|
|
$pack = LanguagePack::query()->create([
|
|
'identifier' => 'g7-core-en',
|
|
'vendor' => 'g7',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'en',
|
|
'locale_name' => 'English',
|
|
'locale_native_name' => 'English',
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0',
|
|
'status' => LanguagePackStatus::Active->value,
|
|
'is_protected' => true,
|
|
'manifest' => [],
|
|
]);
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->service->uninstall($pack, false);
|
|
}
|
|
|
|
public function test_list_returns_paginator(): void
|
|
{
|
|
$this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$this->makePack('acme', 'ja', LanguagePackStatus::Inactive->value);
|
|
$this->makePack('sirsoft', 'fr', LanguagePackStatus::Active->value);
|
|
|
|
// installed 상태로만 필터해 미설치 번들 가상 레코드를 제외하고 DB 만 검증.
|
|
$paginator = $this->service->list(['locale' => 'ja', 'status' => LanguagePackStatus::Installed->value], 20);
|
|
$this->assertSame(0, $paginator->total(), 'Installed 상태 일치 0건');
|
|
|
|
// 활성/비활성 + locale=ja 로 DB 의 2건이 정확히 잡히는지(번들 가상은 status filter 가 차단).
|
|
$activeOnly = $this->service->list(['locale' => 'ja', 'status' => LanguagePackStatus::Active->value], 20);
|
|
$this->assertSame(1, $activeOnly->total());
|
|
|
|
$inactiveOnly = $this->service->list(['locale' => 'ja', 'status' => LanguagePackStatus::Inactive->value], 20);
|
|
$this->assertSame(1, $inactiveOnly->total());
|
|
}
|
|
|
|
public function test_find_returns_pack_or_null(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
|
|
$this->assertNotNull($this->service->find($pack->id));
|
|
$this->assertNull($this->service->find(999999));
|
|
}
|
|
|
|
public function test_resolve_install_blocked_reason_returns_null_for_core(): void
|
|
{
|
|
$reason = $this->service->resolveInstallBlockedReason([
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'locale' => 'ja',
|
|
]);
|
|
|
|
$this->assertNull($reason);
|
|
}
|
|
|
|
public function test_resolve_install_blocked_reason_core_locale_missing(): void
|
|
{
|
|
$reason = $this->service->resolveInstallBlockedReason([
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'gnuboard7-hello_module',
|
|
'locale' => 'ja',
|
|
]);
|
|
|
|
$this->assertSame('core_locale_missing', $reason);
|
|
}
|
|
|
|
public function test_resolve_install_blocked_reason_target_not_installed(): void
|
|
{
|
|
// 코어 ja 활성화 → 코어 의존성 통과시키고 target 검증으로 진입.
|
|
$this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$this->registry->invalidate();
|
|
|
|
$reason = $this->service->resolveInstallBlockedReason([
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'no-such-module',
|
|
'locale' => 'ja',
|
|
]);
|
|
|
|
$this->assertSame('target_not_installed', $reason);
|
|
}
|
|
|
|
public function test_resolve_install_blocked_reason_target_inactive(): void
|
|
{
|
|
$this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$this->registry->invalidate();
|
|
|
|
DB::table('modules')->insert([
|
|
'identifier' => 'gnuboard7-hello_module',
|
|
'name' => 'Hello',
|
|
'vendor' => 'gnuboard7',
|
|
'version' => '1.0.0',
|
|
'status' => 'inactive',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$reason = $this->service->resolveInstallBlockedReason([
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'gnuboard7-hello_module',
|
|
'locale' => 'ja',
|
|
]);
|
|
|
|
$this->assertSame('target_inactive', $reason);
|
|
}
|
|
|
|
public function test_resolve_install_blocked_reason_target_version_too_old(): void
|
|
{
|
|
$this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$this->registry->invalidate();
|
|
|
|
DB::table('modules')->insert([
|
|
'identifier' => 'gnuboard7-hello_module',
|
|
'name' => 'Hello',
|
|
'vendor' => 'gnuboard7',
|
|
'version' => '1.0.0',
|
|
'status' => 'active',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$reason = $this->service->resolveInstallBlockedReason([
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'gnuboard7-hello_module',
|
|
'locale' => 'ja',
|
|
'requires' => ['target_version' => '^2.0.0'],
|
|
]);
|
|
|
|
$this->assertSame('target_version_too_old', $reason);
|
|
}
|
|
|
|
public function test_resolve_install_blocked_reason_returns_null_when_all_satisfied(): void
|
|
{
|
|
$this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$this->registry->invalidate();
|
|
|
|
DB::table('modules')->insert([
|
|
'identifier' => 'gnuboard7-hello_module',
|
|
'name' => 'Hello',
|
|
'vendor' => 'gnuboard7',
|
|
'version' => '1.5.0',
|
|
'status' => 'active',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$reason = $this->service->resolveInstallBlockedReason([
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'gnuboard7-hello_module',
|
|
'locale' => 'ja',
|
|
'requires' => ['target_version' => '^1.0.0'],
|
|
]);
|
|
|
|
$this->assertNull($reason);
|
|
}
|
|
|
|
public function test_check_updates_skips_non_github_packs(): void
|
|
{
|
|
$this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
|
|
$result = $this->service->checkUpdates();
|
|
|
|
$this->assertSame(['checked' => 0, 'updates' => 0, 'details' => []], $result);
|
|
}
|
|
|
|
public function test_perform_update_throws_when_no_github_source(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->service->performUpdate($pack);
|
|
}
|
|
|
|
/**
|
|
* 모듈/플러그인/템플릿과 일관되게, 매니페스트 `github_url` 이 SSoT 로 사용되는지 검증.
|
|
*
|
|
* source_type=bundled 로 설치된 팩이라도 매니페스트에 github_url 이 명시되어 있으면
|
|
* checkUpdates() 의 점검 대상이 되어야 함 (count > 0). 실제 GitHub 호출은 발생하지만
|
|
* 네트워크 결과와 무관하게 `checked` 가 증가하면 SSoT 해석이 정상 동작한 것.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function test_check_updates_includes_packs_with_manifest_github_url(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$pack->forceFill([
|
|
'source_type' => 'bundled',
|
|
'source_url' => $pack->identifier,
|
|
'manifest' => [
|
|
'identifier' => $pack->identifier,
|
|
'namespace' => 'g7',
|
|
'vendor' => 'sirsoft',
|
|
'github_url' => 'https://github.com/gnuboard/'.$pack->identifier,
|
|
],
|
|
])->save();
|
|
|
|
$result = $this->service->checkUpdates();
|
|
|
|
$this->assertSame(1, $result['checked'], '매니페스트 github_url 이 있으면 점검 대상에 포함되어야 한다');
|
|
$this->assertCount(1, $result['details']);
|
|
$this->assertSame($pack->identifier, $result['details'][0]['identifier']);
|
|
}
|
|
|
|
/**
|
|
* 매니페스트 github_url 이 비어있고 source_url 도 GitHub 가 아니면서
|
|
* 번들도 없으면 checkUpdates 점검 대상에서 제외되는지 확인 (회귀 가드).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function test_check_updates_excludes_packs_with_blank_manifest_github_url(): void
|
|
{
|
|
$pack = $this->makePack('sirsoft', 'ja', LanguagePackStatus::Active->value);
|
|
$pack->forceFill([
|
|
'manifest' => [
|
|
'identifier' => $pack->identifier,
|
|
'github_url' => '',
|
|
],
|
|
])->save();
|
|
|
|
$result = $this->service->checkUpdates();
|
|
|
|
$this->assertSame(0, $result['checked']);
|
|
}
|
|
|
|
public function test_refresh_cache_returns_status_map(): void
|
|
{
|
|
$result = $this->service->refreshCache();
|
|
|
|
$this->assertArrayHasKey('registry', $result);
|
|
$this->assertArrayHasKey('translator', $result);
|
|
$this->assertArrayHasKey('template', $result);
|
|
$this->assertTrue($result['registry']);
|
|
}
|
|
|
|
/**
|
|
* 공식 일본어 번들 언어팩(g7-core-ja) 을 bundled 소스로 설치 → 자동 활성 검증.
|
|
*
|
|
* 본 테스트는 빌드 산출 디렉토리(`lang-packs/_bundled/g7-core-ja/`)가 존재할 때만 의미가 있으며,
|
|
* 빌드 미완료 시 자동 스킵하여 CI 가 깨지지 않도록 한다.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function test_install_g7_core_ja_from_bundled_promotes_to_active(): void
|
|
{
|
|
$bundledPath = base_path('lang-packs/_bundled/g7-core-ja');
|
|
if (! is_dir($bundledPath) || ! is_file($bundledPath.'/language-pack.json')) {
|
|
$this->markTestSkipped('g7-core-ja 번들이 아직 빌드되지 않음 — build-language-pack-ja.cjs 실행 후 재시도');
|
|
}
|
|
|
|
// 번들 manifest 의 현재 버전을 기준으로 설치 결과 검증
|
|
// (코어 lang 변경 시 g7-core-ja 버전이 patch bump 되므로 하드코딩 회피)
|
|
$bundledManifest = json_decode(file_get_contents($bundledPath.'/language-pack.json'), true);
|
|
$bundledVersion = $bundledManifest['version'] ?? null;
|
|
$this->assertNotNull($bundledVersion, '번들 manifest 에 version 누락');
|
|
|
|
$pack = $this->service->installFromBundled('g7-core-ja', autoActivate: true);
|
|
|
|
$this->assertSame('g7-core-ja', $pack->identifier);
|
|
$this->assertSame(LanguagePackScope::Core->value, $pack->scope);
|
|
$this->assertNull($pack->target_identifier);
|
|
$this->assertSame('ja', $pack->locale);
|
|
$this->assertSame($bundledVersion, $pack->version);
|
|
$this->assertSame(LanguagePackStatus::Active->value, $pack->status);
|
|
$this->assertSame('bundled', $pack->source_type);
|
|
// is_protected 는 모든 install 흐름에서 항상 false (보호 행은 코어/번들 확장의 lang/ 디렉토리를 가상 행으로 합성하는 경로에서만 true)
|
|
$this->assertFalse($pack->is_protected);
|
|
|
|
// 산출 자산 검증 — backend/ja/*.php 가 실제로 복사되었는지
|
|
$installedDir = base_path('lang-packs/g7-core-ja');
|
|
$this->assertFileExists($installedDir.'/backend/ja/common.php');
|
|
$this->assertFileExists($installedDir.'/seed/permissions.json');
|
|
|
|
// cleanup — 활성 디렉토리 제거 (RefreshDatabase 와 별개)
|
|
File::deleteDirectory($installedDir);
|
|
}
|
|
|
|
/**
|
|
* 회귀 가드 — 재설치(installFromBundled 두 번째 호출) 시 자기 자신을
|
|
* 슬롯 충돌로 오인하여 status 가 active → installed 로 강등되는 회귀 차단.
|
|
*
|
|
* 인스톨러가 retry 되거나 사용자가 동일 identifier 를 재설치할 때, 이전엔
|
|
* `findActiveForSlot` 가 자기 자신(이미 active 인 row)을 반환 → `shouldActivate=false` →
|
|
* 새 status='installed' 로 떨어져 의존하는 확장 언어팩이 'core_locale_missing' 으로
|
|
* 차단되는 인스톨러 hang/실패가 발생했음.
|
|
*
|
|
* fix: `finalizeInstall` 가 `findActiveForSlot` 호출 시 `$existing?->id` 를 excludeId 로 전달.
|
|
*/
|
|
public function test_reinstall_active_pack_keeps_active_status(): void
|
|
{
|
|
$bundledPath = base_path('lang-packs/_bundled/g7-core-ja');
|
|
if (! is_dir($bundledPath) || ! is_file($bundledPath.'/language-pack.json')) {
|
|
$this->markTestSkipped('g7-core-ja 번들이 아직 빌드되지 않음 — build-language-pack-ja.cjs 실행 후 재시도');
|
|
}
|
|
|
|
// 첫 install — autoActivate=true → status=active
|
|
$first = $this->service->installFromBundled('g7-core-ja', autoActivate: true);
|
|
$this->assertSame(LanguagePackStatus::Active->value, $first->status, '첫 install 은 자동 활성화되어야 함');
|
|
|
|
// 재설치 — 같은 identifier 가 update path 로 진입.
|
|
// self-conflict fix 가 없다면 status 가 'installed' 로 강등됨 (회귀).
|
|
// fix 후엔 자기 자신 제외하고 슬롯 검사하므로 active 유지.
|
|
$second = $this->service->installFromBundled('g7-core-ja', autoActivate: true);
|
|
$this->assertSame($first->id, $second->id, '같은 row 가 update 되어야 함');
|
|
$this->assertSame(LanguagePackStatus::Active->value, $second->status,
|
|
'재설치 시 자기 자신을 슬롯 충돌로 오인하여 active → installed 로 강등되면 안 됨 (회귀)');
|
|
|
|
// cleanup
|
|
File::deleteDirectory(base_path('lang-packs/g7-core-ja'));
|
|
}
|
|
|
|
/**
|
|
* 자동 활성화는 `activated` 훅을 정확히 한 번만 발화한다.
|
|
*
|
|
* 이전 구현은 설치 트랜잭션에서 status 를 직접 active 로 쓴 뒤 훅을 발화했고,
|
|
* 활성화 경로(activate)도 같은 훅을 발화해 두 경로가 공존했다. 활성화를 activate()
|
|
* 한 곳으로 모았으므로 발화도 한 번이어야 한다 — 두 번 발화되면 훅 구독자가
|
|
* 캐시 재생성·알림 등을 중복 수행한다.
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects activated_hook_fires_exactly_once_on_auto_activate
|
|
*/
|
|
public function test_activated_hook_fires_exactly_once_on_auto_activate(): void
|
|
{
|
|
$bundledPath = base_path('lang-packs/_bundled/g7-core-ja');
|
|
if (! is_dir($bundledPath) || ! is_file($bundledPath.'/language-pack.json')) {
|
|
$this->markTestSkipped('g7-core-ja 번들이 아직 빌드되지 않음 — build-language-pack-ja.cjs 실행 후 재시도');
|
|
}
|
|
|
|
$fired = 0;
|
|
HookManager::addAction('core.language_packs.activated', function () use (&$fired) {
|
|
$fired++;
|
|
});
|
|
|
|
try {
|
|
$pack = $this->service->installFromBundled('g7-core-ja', autoActivate: true);
|
|
|
|
$this->assertSame(LanguagePackStatus::Active->value, $pack->status);
|
|
$this->assertSame(1, $fired, 'activated 훅이 정확히 1회 발화되지 않음');
|
|
} finally {
|
|
HookManager::clearAction('core.language_packs.activated');
|
|
File::deleteDirectory(base_path('lang-packs/g7-core-ja'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 같은 슬롯에 이미 다른 활성 팩이 있으면 자동 활성화를 건너뛴다 (기존 동작 유지).
|
|
*
|
|
* 활성화를 activate() 경유로 바꾸면서 슬롯이 점유된 경우 예외가 새로 터지지 않아야 한다 —
|
|
* 설치 자체는 성공하고 상태만 installed 로 남는 것이 기존 동작이다.
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects auto_activate_skipped_when_slot_occupied
|
|
*/
|
|
public function test_auto_activate_skipped_when_slot_occupied(): void
|
|
{
|
|
$bundledPath = base_path('lang-packs/_bundled/g7-core-ja');
|
|
if (! is_dir($bundledPath) || ! is_file($bundledPath.'/language-pack.json')) {
|
|
$this->markTestSkipped('g7-core-ja 번들이 아직 빌드되지 않음 — build-language-pack-ja.cjs 실행 후 재시도');
|
|
}
|
|
|
|
// 같은 슬롯(core / ja)을 다른 팩이 이미 점유한 상태
|
|
$this->makePack('other', 'ja', LanguagePackStatus::Active->value);
|
|
|
|
try {
|
|
$pack = $this->service->installFromBundled('g7-core-ja', autoActivate: true);
|
|
|
|
$this->assertSame(
|
|
LanguagePackStatus::Installed->value,
|
|
$pack->status,
|
|
'슬롯이 점유되어 있으면 설치는 성공하되 활성화는 건너뛰어야 한다'
|
|
);
|
|
} finally {
|
|
File::deleteDirectory(base_path('lang-packs/g7-core-ja'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 자동 활성화는 트랜잭션의 직접 기록이 아니라 activate() 를 경유하며,
|
|
* 그 경로에는 의존성 검사가 실제로 걸린다.
|
|
*
|
|
* 두 가지를 각각 단언한다.
|
|
* (1) 경로 — `installed` 훅이 발화하는 시점(트랜잭션 직후)의 DB 상태가 `installed` 여야
|
|
* 한다. 예전처럼 트랜잭션이 status 를 active 로 직접 썼다면 이 시점에 이미 active 다.
|
|
* 그 뒤 `activated` 훅이 발화하고 최종 상태가 active 가 되는 순서까지 확인하면
|
|
* 활성 승격이 activate() 에서 일어났다는 것이 확정된다.
|
|
* (2) 검사 — activate() 는 저장된 manifest 로 의존성을 검사한다. 코어 로케일이
|
|
* 활성이 아닌 확장 스코프 팩은 activate() 단계에서 거부되어야 한다.
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects auto_activate_goes_through_activate_and_enforces_dependencies
|
|
*/
|
|
public function test_finalize_install_with_auto_activate_goes_through_activate_and_runs_dependency_checks(): void
|
|
{
|
|
$identifier = 'actpath-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'actpath', '1.0.0'));
|
|
|
|
/** @var list<string> $order 훅 발화 순서 */
|
|
$order = [];
|
|
$statusAtInstalledHook = null;
|
|
|
|
HookManager::addAction('core.language_packs.installed', function () use (&$order, &$statusAtInstalledHook, $identifier) {
|
|
$order[] = 'installed';
|
|
$statusAtInstalledHook = DB::table('language_packs')
|
|
->where('identifier', $identifier)
|
|
->value('status');
|
|
});
|
|
HookManager::addAction('core.language_packs.activated', function () use (&$order) {
|
|
$order[] = 'activated';
|
|
});
|
|
|
|
try {
|
|
$pack = $this->service->installFromBundled($identifier, autoActivate: true);
|
|
|
|
// (1) 경로 — 트랜잭션은 installed 로만 기록하고, 활성 승격은 그 뒤 activate() 에서 일어난다.
|
|
$this->assertSame(
|
|
LanguagePackStatus::Installed->value,
|
|
$statusAtInstalledHook,
|
|
'설치 트랜잭션이 status 를 active 로 직접 기록했다 — activate() 를 경유하지 않는다'
|
|
);
|
|
$this->assertSame(['installed', 'activated'], $order, '활성화가 설치 트랜잭션보다 먼저/동시에 일어났다');
|
|
$this->assertSame(LanguagePackStatus::Active->value, $pack->status);
|
|
} finally {
|
|
HookManager::clearAction('core.language_packs.installed');
|
|
HookManager::clearAction('core.language_packs.activated');
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
|
|
// (2) 검사 — activate() 는 저장된 manifest 의 의존성을 실제로 검사한다.
|
|
// 코어 로케일이 활성이 아닌 상태에서 모듈 스코프 팩을 활성화하면 거부되어야 한다.
|
|
// (위에서 설치한 팩이 코어 ja 를 점유하므로 의존성이 미충족인 다른 로케일을 쓴다)
|
|
$dependent = LanguagePack::query()->create([
|
|
'identifier' => 'actpath-module-de',
|
|
'vendor' => 'actpath',
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'acme-demo',
|
|
'locale' => 'de',
|
|
'locale_name' => 'DE',
|
|
'locale_native_name' => 'Deutsch',
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0',
|
|
'status' => LanguagePackStatus::Installed->value,
|
|
'is_protected' => false,
|
|
'manifest' => [
|
|
'identifier' => 'actpath-module-de',
|
|
'scope' => LanguagePackScope::Module->value,
|
|
'target_identifier' => 'acme-demo',
|
|
'locale' => 'de',
|
|
],
|
|
]);
|
|
|
|
$this->assertFalse(
|
|
$this->registry->hasActiveCoreLocale('de'),
|
|
'전제 조건 붕괴 — 코어 de 가 활성이면 의존성 검사가 통과해버려 이 단언이 무의미해진다'
|
|
);
|
|
|
|
$this->expectException(LanguagePackOperationException::class);
|
|
$this->service->activate($dependent);
|
|
}
|
|
|
|
/**
|
|
* 활성 팩을 같은 identifier 로 재설치해도 자기 자신을 슬롯 충돌로 오인해 강등하지 않는다.
|
|
*
|
|
* `finalizeInstall` 은 트랜잭션에서 항상 `installed` 로 기록하고 그 뒤 activate() 를
|
|
* 호출한다. 이때 `findActiveForSlot` 이 `$existing?->id` 를 제외하지 않으면 이미 활성인
|
|
* 자기 자신이 슬롯 점유자로 잡혀 `shouldActivate=false` 가 되고, 팩이 조용히 `installed`
|
|
* 로 내려간다 — 의존하는 확장 언어팩이 `core_locale_missing` 으로 차단되어 인스톨러가
|
|
* 멈추던 회귀다.
|
|
*
|
|
* 같은 회귀를 고정하는 `test_reinstall_active_pack_keeps_active_status` 는 실제 ja 번들
|
|
* 산출물을 요구해 미빌드 환경에서 skip 된다. 활성화 경로가 activate() 경유로 바뀐 뒤에도
|
|
* 이 가드가 환경에 따라 조용히 건너뛰어지지 않도록, 합성 번들 픽스처로 항상 실행되는
|
|
* 판을 따로 둔다.
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects reinstalling_an_active_pack_does_not_demote_it
|
|
*/
|
|
public function test_finalize_install_does_not_demote_self_on_reinstall_of_active_pack(): void
|
|
{
|
|
$identifier = 'selfslot-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'selfslot', '1.0.0'));
|
|
|
|
try {
|
|
$first = $this->service->installFromBundled($identifier, autoActivate: true);
|
|
$this->assertSame(
|
|
LanguagePackStatus::Active->value,
|
|
$first->status,
|
|
'전제 조건 붕괴 — 첫 설치가 활성화되지 않으면 강등 여부를 관측할 수 없다'
|
|
);
|
|
|
|
$second = $this->service->installFromBundled($identifier, autoActivate: true);
|
|
|
|
$this->assertSame($first->id, $second->id, '같은 identifier 는 같은 행을 갱신해야 한다');
|
|
$this->assertSame(
|
|
LanguagePackStatus::Active->value,
|
|
$second->status,
|
|
'재설치가 자기 자신을 슬롯 충돌로 오인해 active → installed 로 강등했다 (회귀)'
|
|
);
|
|
$this->assertSame(
|
|
LanguagePackStatus::Active->value,
|
|
DB::table('language_packs')->where('identifier', $identifier)->value('status'),
|
|
'반환값만 active 이고 DB 에는 강등된 값이 남았다'
|
|
);
|
|
} finally {
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CLI 설치는 HTTP 활성화 권한 게이트의 영향을 받지 않는다.
|
|
*
|
|
* `auto_activate` 의 활성화 권한 검사는 FormRequest 계층(`RequiresActivationPermission`)
|
|
* 이 소유한다. 이 검사가 Service 계층으로 내려가면 인증 컨텍스트가 없는 CLI 가 전부
|
|
* 깨진다 — `language-pack:install` 과 `language-pack:provision` 은 `--no-activate` 를
|
|
* 주지 않는 한 설치 직후 자동 활성화를 기본 동작으로 삼기 때문이다.
|
|
*
|
|
* Service 를 목으로 바꾸지 않고 실제 커맨드를 인증 없이 실행해 활성화까지 도달하는지
|
|
* 고정한다. 게이트가 서비스 계층으로 새어 들어오면 여기서 red 가 된다.
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects cli_installation_is_unaffected_by_the_http_activation_gate
|
|
*/
|
|
public function test_cli_install_auto_activates_without_an_http_permission_context(): void
|
|
{
|
|
$identifier = 'cliact-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'cliact', '1.0.0'));
|
|
|
|
try {
|
|
$this->assertFalse(
|
|
Auth::check(),
|
|
'전제 조건 붕괴 — 인증된 사용자가 있으면 CLI 컨텍스트를 재현한 것이 아니다'
|
|
);
|
|
|
|
$this->artisan('language-pack:install', [
|
|
'identifier' => $identifier,
|
|
'--source' => 'bundled',
|
|
])->assertExitCode(0);
|
|
|
|
$this->assertSame(
|
|
LanguagePackStatus::Active->value,
|
|
DB::table('language_packs')->where('identifier', $identifier)->value('status'),
|
|
'CLI 설치가 자동 활성화되지 않았다 — 활성화 권한 검사가 Service 계층으로 내려왔을 가능성'
|
|
);
|
|
} finally {
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 활성 팩의 업데이트는 활성 상태를 유지한다 — activate() 경유 전환의 회귀 가드.
|
|
*
|
|
* `performUpdate` 는 `$autoActivate = $pack->isActive()` 로 활성 여부를 승계해
|
|
* 재설치 경로로 넘긴다. 활성화가 트랜잭션 직접 기록에서 activate() 경유로 바뀌면서
|
|
* 이 경로에 의존성 검사·슬롯 충돌 검사가 새로 끼어들었으므로, 활성 팩의 업데이트가
|
|
* 조용히 installed 로 떨어지거나 예외로 막히지 않는지 고정한다.
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects update_of_an_active_pack_keeps_it_active
|
|
*/
|
|
public function test_perform_update_of_active_pack_keeps_it_active(): void
|
|
{
|
|
$identifier = 'updact-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'updact', '1.0.0'));
|
|
|
|
try {
|
|
$pack = $this->service->installFromBundled($identifier, autoActivate: true);
|
|
$this->assertSame(LanguagePackStatus::Active->value, $pack->status, '전제 조건 — 업데이트 대상은 활성 팩이어야 한다');
|
|
|
|
// 번들에 신버전 배치 후 업데이트 (bundled 소스 강제)
|
|
$this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'updact', '1.0.1'));
|
|
|
|
$updated = $this->service->performUpdate($pack, force: true);
|
|
|
|
$this->assertSame('1.0.1', $updated->version);
|
|
$this->assertSame(
|
|
LanguagePackStatus::Active->value,
|
|
$updated->status,
|
|
'활성 팩을 업데이트했더니 활성 상태를 잃었다 — activate() 경유 전환의 회귀'
|
|
);
|
|
} finally {
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 비활성 팩의 업데이트는 활성으로 승격되지 않는다 (기존 동작 유지).
|
|
*
|
|
* @scenario vector=finalize_install_activation, actor_permission=cli_system
|
|
*
|
|
* @effects update_of_an_inactive_pack_does_not_promote_it
|
|
*/
|
|
public function test_perform_update_of_inactive_pack_does_not_promote_it(): void
|
|
{
|
|
$identifier = 'updinact-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'updinact', '1.0.0'));
|
|
|
|
try {
|
|
$pack = $this->service->installFromBundled($identifier, autoActivate: false);
|
|
$this->assertSame(LanguagePackStatus::Installed->value, $pack->status);
|
|
|
|
$this->createBundledFixture($identifier, $this->coreJaManifest($identifier, 'updinact', '1.0.1'));
|
|
|
|
$updated = $this->service->performUpdate($pack, force: true);
|
|
|
|
$this->assertSame('1.0.1', $updated->version);
|
|
$this->assertSame(
|
|
LanguagePackStatus::Installed->value,
|
|
$updated->status,
|
|
'비활성 팩이 업데이트만으로 활성 승격되면 안 된다'
|
|
);
|
|
} finally {
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 코어 ja 번들 픽스처용 manifest 를 만듭니다.
|
|
*
|
|
* @param string $identifier 번들 식별자
|
|
* @param string $vendor 벤더
|
|
* @param string $version 버전
|
|
* @return array<string, mixed> manifest 데이터
|
|
*/
|
|
private function coreJaManifest(string $identifier, string $vendor, string $version): array
|
|
{
|
|
return [
|
|
'identifier' => $identifier,
|
|
'namespace' => $vendor,
|
|
'vendor' => $vendor,
|
|
'name' => ['ko' => $identifier, 'en' => $identifier, 'ja' => $identifier],
|
|
'description' => ['ko' => '테스트', 'en' => 'Test'],
|
|
'version' => $version,
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'ja',
|
|
'locale_name' => 'JA',
|
|
'locale_native_name' => '日本語',
|
|
'text_direction' => 'ltr',
|
|
'g7_version' => '>=7.0.0-beta.4',
|
|
];
|
|
}
|
|
|
|
public function test_install_blocks_downgrade_without_force(): void
|
|
{
|
|
$identifier = 'dgblock-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, [
|
|
'identifier' => $identifier,
|
|
'namespace' => 'dgblock',
|
|
'vendor' => 'dgblock',
|
|
'name' => ['ko' => 'Downgrade Test', 'en' => 'Downgrade Test', 'ja' => 'Downgrade Test'],
|
|
'description' => ['ko' => '테스트', 'en' => 'Test'],
|
|
'version' => '1.0.0-beta.1',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'ja',
|
|
'locale_name' => 'JA',
|
|
'locale_native_name' => '日本語',
|
|
'text_direction' => 'ltr',
|
|
'g7_version' => '>=7.0.0-beta.4',
|
|
]);
|
|
|
|
try {
|
|
LanguagePack::query()->create([
|
|
'identifier' => $identifier,
|
|
'vendor' => 'dgblock',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'ja',
|
|
'locale_name' => 'JA',
|
|
'locale_native_name' => '日本語',
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0-beta.2',
|
|
'status' => LanguagePackStatus::Active->value,
|
|
'is_protected' => false,
|
|
'manifest' => [],
|
|
'source_type' => 'bundled',
|
|
]);
|
|
|
|
// force=false → 다운그레이드 차단 예외
|
|
$this->expectException(LanguagePackOperationException::class);
|
|
$this->service->installFromBundled($identifier);
|
|
} finally {
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
}
|
|
|
|
public function test_install_allows_downgrade_with_force(): void
|
|
{
|
|
$identifier = 'dgforce-core-ja';
|
|
$bundledDir = $this->createBundledFixture($identifier, [
|
|
'identifier' => $identifier,
|
|
'namespace' => 'dgforce',
|
|
'vendor' => 'dgforce',
|
|
'name' => ['ko' => 'Downgrade Force Test', 'en' => 'Downgrade Force Test', 'ja' => 'Downgrade Force Test'],
|
|
'description' => ['ko' => '테스트', 'en' => 'Test'],
|
|
'version' => '1.0.0-beta.1',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'ja',
|
|
'locale_name' => 'JA',
|
|
'locale_native_name' => '日本語',
|
|
'text_direction' => 'ltr',
|
|
'g7_version' => '>=7.0.0-beta.4',
|
|
]);
|
|
|
|
try {
|
|
LanguagePack::query()->create([
|
|
'identifier' => $identifier,
|
|
'vendor' => 'dgforce',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'ja',
|
|
'locale_name' => 'JA',
|
|
'locale_native_name' => '日本語',
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0-beta.2',
|
|
'status' => LanguagePackStatus::Active->value,
|
|
'is_protected' => false,
|
|
'manifest' => [],
|
|
'source_type' => 'bundled',
|
|
]);
|
|
|
|
// force=true → 다운그레이드 허용
|
|
$pack = $this->service->installFromBundled($identifier, force: true);
|
|
$this->assertSame('1.0.0-beta.1', $pack->version);
|
|
} finally {
|
|
File::deleteDirectory($bundledDir);
|
|
File::deleteDirectory(base_path('lang-packs/'.$identifier));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 임시 번들 디렉토리를 생성합니다 (테스트 격리용).
|
|
*
|
|
* @param string $identifier 번들 디렉토리 식별자 (lang-packs/_bundled/{identifier})
|
|
* @param array<string, mixed> $manifest manifest JSON 으로 직렬화할 데이터
|
|
* @return string 생성된 디렉토리 절대 경로
|
|
*/
|
|
private function createBundledFixture(string $identifier, array $manifest): string
|
|
{
|
|
$path = base_path('lang-packs/_bundled/'.$identifier);
|
|
File::ensureDirectoryExists($path);
|
|
File::put(
|
|
$path.DIRECTORY_SEPARATOR.'language-pack.json',
|
|
json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
);
|
|
|
|
return $path;
|
|
}
|
|
|
|
public function test_get_uninstalled_bundled_packs_returns_virtual_records(): void
|
|
{
|
|
$identifier = 'test-virtual-acme-zz-'.uniqid();
|
|
$manifest = [
|
|
'identifier' => $identifier,
|
|
'vendor' => 'acme',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'zz',
|
|
'locale_name' => 'Zzland',
|
|
'locale_native_name' => 'Zzland Native',
|
|
'text_direction' => 'ltr',
|
|
'version' => '0.1.0',
|
|
];
|
|
$path = $this->createBundledFixture($identifier, $manifest);
|
|
|
|
try {
|
|
$packs = $this->service->getUninstalledBundledPacks();
|
|
$match = $packs->firstWhere('identifier', $identifier);
|
|
|
|
$this->assertNotNull($match, '미설치 번들이 가상 레코드로 반환되어야 함');
|
|
$this->assertSame(LanguagePackStatus::Uninstalled->value, $match->status);
|
|
$this->assertSame('bundled', $match->source_type);
|
|
$this->assertSame($identifier, $match->getAttribute('bundled_identifier'));
|
|
$this->assertFalse($match->exists, '가상 레코드는 exists=false 여야 함');
|
|
$this->assertNull($match->id, '가상 레코드는 id 가 null 이어야 함');
|
|
} finally {
|
|
File::deleteDirectory($path);
|
|
}
|
|
}
|
|
|
|
public function test_get_uninstalled_bundled_packs_excludes_already_installed_slot(): void
|
|
{
|
|
$identifier = 'test-installed-acme-zz-'.uniqid();
|
|
$manifest = [
|
|
'identifier' => $identifier,
|
|
'vendor' => 'acme',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'zz',
|
|
'locale_name' => 'Zzland',
|
|
'locale_native_name' => 'Zzland Native',
|
|
'text_direction' => 'ltr',
|
|
'version' => '0.1.0',
|
|
];
|
|
$path = $this->createBundledFixture($identifier, $manifest);
|
|
// 동일 슬롯(scope=core, target_identifier=null, locale=zz) 의 DB 레코드를 만든다 → 가상 레코드 미포함되어야 함.
|
|
LanguagePack::query()->create([
|
|
'identifier' => 'other-vendor-core-zz',
|
|
'vendor' => 'other',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'zz',
|
|
'locale_name' => 'Zzland',
|
|
'locale_native_name' => 'Zzland',
|
|
'text_direction' => 'ltr',
|
|
'version' => '1.0.0',
|
|
'status' => LanguagePackStatus::Active->value,
|
|
'is_protected' => false,
|
|
'manifest' => [],
|
|
]);
|
|
|
|
try {
|
|
$packs = $this->service->getUninstalledBundledPacks();
|
|
$match = $packs->firstWhere('identifier', $identifier);
|
|
$this->assertNull($match, '동일 슬롯에 DB 레코드가 있으면 가상 레코드가 반환되지 않아야 함');
|
|
} finally {
|
|
File::deleteDirectory($path);
|
|
}
|
|
}
|
|
|
|
public function test_list_merges_db_records_and_uninstalled_bundled(): void
|
|
{
|
|
$this->makePack('sirsoft', 'fr', LanguagePackStatus::Active->value);
|
|
|
|
$identifier = 'test-merge-acme-yy-'.uniqid();
|
|
$manifest = [
|
|
'identifier' => $identifier,
|
|
'vendor' => 'acme',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'yy',
|
|
'locale_name' => 'Yyland',
|
|
'locale_native_name' => 'Yyland',
|
|
'text_direction' => 'ltr',
|
|
'version' => '0.1.0',
|
|
];
|
|
$path = $this->createBundledFixture($identifier, $manifest);
|
|
|
|
try {
|
|
$paginator = $this->service->list([], 100);
|
|
|
|
$items = collect($paginator->items());
|
|
$this->assertGreaterThanOrEqual(
|
|
2,
|
|
$paginator->total(),
|
|
'DB 레코드(fr) + 가상 번들(yy) 이 합쳐져야 함'
|
|
);
|
|
|
|
$virtual = $items->firstWhere('identifier', $identifier);
|
|
$this->assertNotNull($virtual);
|
|
$this->assertSame(LanguagePackStatus::Uninstalled->value, $virtual->status);
|
|
} finally {
|
|
File::deleteDirectory($path);
|
|
}
|
|
}
|
|
|
|
public function test_list_filters_uninstalled_status_excludes_db_records(): void
|
|
{
|
|
$this->makePack('sirsoft', 'fr', LanguagePackStatus::Active->value);
|
|
|
|
$identifier = 'test-filter-acme-xx-'.uniqid();
|
|
$manifest = [
|
|
'identifier' => $identifier,
|
|
'vendor' => 'acme',
|
|
'scope' => LanguagePackScope::Core->value,
|
|
'target_identifier' => null,
|
|
'locale' => 'xx',
|
|
'locale_name' => 'Xxland',
|
|
'locale_native_name' => 'Xxland',
|
|
'text_direction' => 'ltr',
|
|
'version' => '0.1.0',
|
|
];
|
|
$path = $this->createBundledFixture($identifier, $manifest);
|
|
|
|
try {
|
|
$paginator = $this->service->list(['status' => LanguagePackStatus::Uninstalled->value], 100);
|
|
$items = collect($paginator->items());
|
|
|
|
$this->assertNotNull($items->firstWhere('identifier', $identifier));
|
|
$this->assertNull($items->firstWhere('status', LanguagePackStatus::Active->value));
|
|
} finally {
|
|
File::deleteDirectory($path);
|
|
}
|
|
}
|
|
}
|