Files
Gnuboard7/tests/Unit/Rules/LayoutInheritanceValidationTest.php
2026-04-01 10:30:52 +09:00

386 lines
11 KiB
PHP

<?php
namespace Tests\Unit\Rules;
use App\Models\Template;
use App\Models\TemplateLayout;
use App\Rules\ValidDataSourceMerge;
use App\Rules\ValidParentLayout;
use App\Rules\ValidSlotStructure;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LayoutInheritanceValidationTest extends TestCase
{
use RefreshDatabase;
private Template $template;
protected function setUp(): void
{
parent::setUp();
// 한국어 로케일 설정
app()->setLocale('ko');
// 테스트용 템플릿 생성 - 고유 identifier 사용하여 트랜잭션 락 충돌 방지
$this->template = Template::factory()->create([
'identifier' => 'test-template-'.uniqid(),
'type' => 'user',
'status' => 'active',
]);
}
/**
* ValidParentLayout: 존재하지 않는 부모 레이아웃 검증 실패
*/
public function test_valid_parent_layout_fails_when_parent_not_found(): void
{
$rule = new ValidParentLayout;
$rule->setData([
'template_id' => $this->template->id,
'layout_name' => 'child',
]);
$failed = false;
$message = '';
$rule->validate('content.extends', 'non_existent_parent', function ($msg) use (&$failed, &$message) {
$failed = true;
$message = $msg;
});
$this->assertTrue($failed);
$this->assertStringContainsString('non_existent_parent', $message);
}
/**
* ValidParentLayout: 순환 참조 검증 실패
*/
public function test_valid_parent_layout_fails_on_circular_reference(): void
{
// 부모 레이아웃 생성
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => 'parent',
'content' => json_encode([
'version' => '1.0.0',
'layout_name' => 'parent',
'extends' => 'child', // 순환 참조: parent → child
'components' => [],
]),
]);
// 자식 레이아웃에서 parent를 extends로 지정
$rule = new ValidParentLayout;
$rule->setData([
'template_id' => $this->template->id,
'layout_name' => 'child',
]);
$failed = false;
$message = '';
$rule->validate('content.extends', 'parent', function ($msg) use (&$failed, &$message) {
$failed = true;
$message = $msg;
});
$this->assertTrue($failed);
$this->assertStringContainsString('순환', $message);
}
/**
* ValidParentLayout: 최대 상속 깊이 초과 검증 실패
*/
public function test_valid_parent_layout_fails_on_max_depth_exceeded(): void
{
// 깊은 상속 체인 생성 (11단계)
$previousName = null;
for ($i = 1; $i <= 11; $i++) {
$layoutName = "layout_{$i}";
$content = [
'version' => '1.0.0',
'layout_name' => $layoutName,
'components' => [],
];
if ($previousName) {
$content['extends'] = $previousName;
}
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => $layoutName,
'content' => json_encode($content),
]);
$previousName = $layoutName;
}
// 새 레이아웃에서 layout_11을 extends로 지정 (12단계가 됨)
$rule = new ValidParentLayout;
$rule->setData([
'template_id' => $this->template->id,
'layout_name' => 'child',
]);
$failed = false;
$message = '';
$rule->validate('content.extends', 'layout_11', function ($msg) use (&$failed, &$message) {
$failed = true;
$message = $msg;
});
$this->assertTrue($failed);
$this->assertStringContainsString('깊이', $message);
}
/**
* ValidParentLayout: 유효한 부모 레이아웃 검증 통과
*/
public function test_valid_parent_layout_passes_with_valid_parent(): void
{
// 부모 레이아웃 생성
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => 'parent',
'content' => json_encode([
'version' => '1.0.0',
'layout_name' => 'parent',
'components' => [],
]),
]);
$rule = new ValidParentLayout;
$rule->setData([
'template_id' => $this->template->id,
'layout_name' => 'child',
]);
$failed = false;
$rule->validate('content.extends', 'parent', function () use (&$failed) {
$failed = true;
});
$this->assertFalse($failed);
}
/**
* ValidSlotStructure: 부모에 없는 슬롯 검증 실패
*/
public function test_valid_slot_structure_fails_when_slot_not_in_parent(): void
{
// 부모 레이아웃 생성 (content 슬롯만 있음)
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => 'parent',
'content' => json_encode([
'version' => '1.0.0',
'layout_name' => 'parent',
'components' => [
[
'id' => 'slot_container',
'type' => 'layout',
'name' => 'Div',
'slot' => 'content',
'children' => [],
],
],
]),
]);
$rule = new ValidSlotStructure;
$rule->setData([
'template_id' => $this->template->id,
'content' => [
'extends' => 'parent',
],
]);
$failed = false;
$message = '';
// sidebar 슬롯은 부모에 정의되지 않음
$slots = [
'content' => [],
'sidebar' => [], // 존재하지 않는 슬롯
];
$rule->validate('content.slots', $slots, function ($msg) use (&$failed, &$message) {
$failed = true;
$message = $msg;
});
$this->assertTrue($failed);
$this->assertStringContainsString('sidebar', $message);
}
/**
* ValidSlotStructure: 유효한 슬롯 구조 검증 통과
*/
public function test_valid_slot_structure_passes_with_valid_slots(): void
{
// 부모 레이아웃 생성
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => 'parent',
'content' => json_encode([
'version' => '1.0.0',
'layout_name' => 'parent',
'components' => [
[
'id' => 'slot_content',
'type' => 'layout',
'name' => 'Div',
'slot' => 'content',
'children' => [],
],
[
'id' => 'slot_sidebar',
'type' => 'layout',
'name' => 'Div',
'slot' => 'sidebar',
'children' => [],
],
],
]),
]);
$rule = new ValidSlotStructure;
$rule->setData([
'template_id' => $this->template->id,
'content' => [
'extends' => 'parent',
],
]);
$failed = false;
$slots = [
'content' => [],
'sidebar' => [],
];
$rule->validate('content.slots', $slots, function () use (&$failed) {
$failed = true;
});
$this->assertFalse($failed);
}
/**
* ValidDataSourceMerge: 중복 ID 검증 실패 (현재 레이아웃 내)
*/
public function test_valid_data_source_merge_fails_on_duplicate_id_in_current(): void
{
$rule = new ValidDataSourceMerge;
$rule->setData([
'template_id' => $this->template->id,
]);
$failed = false;
$message = '';
$dataSources = [
['id' => 'users', 'type' => 'api', 'endpoint' => '/api/users'],
['id' => 'users', 'type' => 'api', 'endpoint' => '/api/admin/users'], // 중복 ID
];
$rule->validate('content.data_sources', $dataSources, function ($msg) use (&$failed, &$message) {
$failed = true;
$message = $msg;
});
$this->assertTrue($failed);
$this->assertStringContainsString('users', $message);
}
/**
* ValidDataSourceMerge: 부모와 중복 ID 검증 실패
*/
public function test_valid_data_source_merge_fails_on_duplicate_id_with_parent(): void
{
// 부모 레이아웃 생성 (stats data_source 포함)
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => 'parent',
'content' => json_encode([
'version' => '1.0.0',
'layout_name' => 'parent',
'data_sources' => [
['id' => 'stats', 'type' => 'api', 'endpoint' => '/api/stats'],
],
'components' => [],
]),
]);
$rule = new ValidDataSourceMerge;
$rule->setData([
'template_id' => $this->template->id,
'content' => [
'extends' => 'parent',
],
]);
$failed = false;
$message = '';
// 자식 레이아웃에서 stats ID 재사용 (중복)
$dataSources = [
['id' => 'stats', 'type' => 'api', 'endpoint' => '/api/admin/stats'],
];
$rule->validate('content.data_sources', $dataSources, function ($msg) use (&$failed, &$message) {
$failed = true;
$message = $msg;
});
$this->assertTrue($failed);
$this->assertStringContainsString('stats', $message);
}
/**
* ValidDataSourceMerge: 유효한 data_sources 검증 통과
*/
public function test_valid_data_source_merge_passes_with_unique_ids(): void
{
// 부모 레이아웃 생성
TemplateLayout::factory()->create([
'template_id' => $this->template->id,
'name' => 'parent',
'content' => json_encode([
'version' => '1.0.0',
'layout_name' => 'parent',
'data_sources' => [
['id' => 'stats', 'type' => 'api', 'endpoint' => '/api/stats'],
],
'components' => [],
]),
]);
$rule = new ValidDataSourceMerge;
$rule->setData([
'template_id' => $this->template->id,
'content' => [
'extends' => 'parent',
],
]);
$failed = false;
// 자식 레이아웃에서 다른 ID 사용
$dataSources = [
['id' => 'users', 'type' => 'api', 'endpoint' => '/api/users'],
];
$rule->validate('content.data_sources', $dataSources, function () use (&$failed) {
$failed = true;
});
$this->assertFalse($failed);
}
}