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

166 lines
6.7 KiB
PHP

<?php
namespace Tests\Feature\LanguagePack;
use App\Enums\LanguagePackScope;
use App\Enums\LanguagePackStatus;
use App\Models\LanguagePack;
use App\Services\LanguagePackService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 슬롯 다중 벤더 Feature 테스트 (계획서 §16.9 케이스 66~70).
*
* 동일 슬롯(scope, target_identifier, locale)에 여러 벤더 언어팩이 공존할 때의 동작을 검증합니다.
* application-level 트랜잭션이 슬롯당 active 1개를 보장하는지, 자동 승격/강등 흐름이 정확한지 확인.
*/
class SlotMultiVendorTest extends TestCase
{
use RefreshDatabase;
private LanguagePackService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = $this->app->make(LanguagePackService::class);
}
/**
* 코어 ja 슬롯 후보 1건을 생성합니다.
*
* @param string $vendor 벤더 식별자
* @param string $status 초기 상태
* @param string $version 버전
* @return LanguagePack 생성된 언어팩
*/
private function makeJaPack(string $vendor, string $status, string $version = '1.0.0'): LanguagePack
{
return LanguagePack::query()->create([
'identifier' => sprintf('%s-core-ja', $vendor),
'vendor' => $vendor,
'scope' => LanguagePackScope::Core->value,
'target_identifier' => null,
'locale' => 'ja',
'locale_name' => 'Japanese',
'locale_native_name' => '日本語',
'text_direction' => 'ltr',
'version' => $version,
'status' => $status,
'is_protected' => false,
'manifest' => ['conflicts' => ['vendors' => []]],
'source_type' => 'zip',
'installed_at' => now(),
'activated_at' => $status === LanguagePackStatus::Active->value ? now() : null,
]);
}
/**
* 케이스 66: 동일 슬롯에 벤더 A(active) + 벤더 B 설치 — B 는 installed, A 유지.
*
* @return void
*/
public function test_case_66_second_vendor_enters_installed_when_slot_has_active(): void
{
$a = $this->makeJaPack('vendor-a', LanguagePackStatus::Active->value);
$b = $this->makeJaPack('vendor-b', LanguagePackStatus::Installed->value);
$this->assertSame(LanguagePackStatus::Active->value, $a->fresh()->status);
$this->assertSame(LanguagePackStatus::Installed->value, $b->fresh()->status);
}
/**
* 케이스 67: 벤더 B 를 activate → 벤더 A 자동 inactive.
*
* @return void
*/
public function test_case_67_activating_b_demotes_existing_a(): void
{
$a = $this->makeJaPack('vendor-a', LanguagePackStatus::Active->value);
$b = $this->makeJaPack('vendor-b', LanguagePackStatus::Installed->value);
// 슬롯 충돌은 force=true 로 명시적 교체 의사 확인 후 demotion 수행 (기본은 SlotConflictException).
$this->service->activate($b, force: true);
$this->assertSame(LanguagePackStatus::Inactive->value, $a->fresh()->status);
$this->assertSame(LanguagePackStatus::Active->value, $b->fresh()->status);
// application-level 보장: 동일 슬롯의 active 가 정확히 1개
$activeCount = LanguagePack::query()
->where('scope', LanguagePackScope::Core->value)
->whereNull('target_identifier')
->where('locale', 'ja')
->where('status', LanguagePackStatus::Active->value)
->count();
$this->assertSame(1, $activeCount);
}
/**
* 케이스 68: active 벤더 A 제거 → 벤더 B 자동 active 승격.
*
* @return void
*/
public function test_case_68_uninstalling_active_promotes_next_candidate(): void
{
$a = $this->makeJaPack('vendor-a', LanguagePackStatus::Active->value);
$b = $this->makeJaPack('vendor-b', LanguagePackStatus::Installed->value);
$this->service->uninstall($a, false);
// A 제거 + B 자동 승격
$this->assertNull(LanguagePack::query()->find($a->id));
$this->assertSame(LanguagePackStatus::Active->value, $b->fresh()->status);
}
/**
* 케이스 69: conflicts.vendors 에 포함된 벤더와 공존 시도 — 슬롯 충돌은 application 정책으로 검증.
*
* 본 테스트는 conflicts 정책이 활성화 직전 거부되는 동작을 직접 확인합니다.
* 현 구현은 manifest 검증 시 conflicts 를 체크하지 않으므로 (v1 범위 외), 본 테스트는
* 같은 슬롯에 여러 active 가 동시에 존재하지 않는다는 핵심 불변량을 대신 검증합니다.
*
* @return void
*/
public function test_case_69_slot_invariant_only_one_active_at_a_time(): void
{
$a = $this->makeJaPack('vendor-a', LanguagePackStatus::Active->value);
$b = $this->makeJaPack('vendor-b', LanguagePackStatus::Installed->value);
$c = $this->makeJaPack('vendor-c', LanguagePackStatus::Installed->value);
// 순차 활성화 — 슬롯 충돌은 force=true 로 명시 교체. 항상 active 1개 유지.
$this->service->activate($b, force: true);
$activeAfterB = LanguagePack::query()
->where('locale', 'ja')->where('scope', 'core')
->where('status', LanguagePackStatus::Active->value)->count();
$this->assertSame(1, $activeAfterB);
$this->service->activate($c, force: true);
$activeAfterC = LanguagePack::query()
->where('locale', 'ja')->where('scope', 'core')
->where('status', LanguagePackStatus::Active->value)->count();
$this->assertSame(1, $activeAfterC);
$this->assertSame(LanguagePackStatus::Active->value, $c->fresh()->status);
}
/**
* 케이스 70: 3개 벤더 공존 — 중간 벤더(inactive) 제거 시 나머지 2개 유지, active 변경 없음.
*
* @return void
*/
public function test_case_70_removing_inactive_candidate_does_not_affect_active(): void
{
$a = $this->makeJaPack('vendor-a', LanguagePackStatus::Active->value);
$b = $this->makeJaPack('vendor-b', LanguagePackStatus::Inactive->value);
$c = $this->makeJaPack('vendor-c', LanguagePackStatus::Inactive->value);
$this->service->uninstall($b, false);
$this->assertNotNull(LanguagePack::query()->find($a->id));
$this->assertNull(LanguagePack::query()->find($b->id));
$this->assertNotNull(LanguagePack::query()->find($c->id));
$this->assertSame(LanguagePackStatus::Active->value, $a->fresh()->status);
$this->assertSame(LanguagePackStatus::Inactive->value, $c->fresh()->status);
}
}