806 lines
30 KiB
PHP
806 lines
30 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Extension;
|
|
|
|
use App\Enums\ExtensionStatus;
|
|
use App\Enums\LayoutSourceType;
|
|
use App\Extension\ModuleManager;
|
|
use App\Models\Module;
|
|
use App\Models\Template;
|
|
use App\Models\TemplateLayout;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\File;
|
|
use Tests\TestCase;
|
|
|
|
class ModuleManagerLayoutTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private ModuleManager $moduleManager;
|
|
|
|
private Template $adminTemplate;
|
|
|
|
private string $testModulePath;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// 서비스 컨테이너에서 ModuleManager 인스턴스 가져오기
|
|
$this->moduleManager = app(ModuleManager::class);
|
|
|
|
// 테스트용 admin 템플릿 생성
|
|
$this->adminTemplate = Template::create([
|
|
'identifier' => 'test-admin',
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '테스트 관리자', 'en' => 'Test Admin'],
|
|
'version' => '1.0.0',
|
|
'type' => 'admin',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
// 테스트용 모듈 디렉토리 경로
|
|
$this->testModulePath = base_path('modules/test-sample');
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
// 테스트 모듈 디렉토리 정리
|
|
if (File::exists($this->testModulePath)) {
|
|
File::deleteDirectory($this->testModulePath);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 테스트용 모듈 레이아웃 파일 생성
|
|
*/
|
|
private function createTestModuleLayouts(): void
|
|
{
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts/admin';
|
|
File::makeDirectory($layoutsPath, 0755, true);
|
|
|
|
// index.json
|
|
$indexLayout = [
|
|
'layout_name' => 'test-sample_admin_index',
|
|
'slots' => [
|
|
'content' => [
|
|
['component' => 'DataTable', 'props' => ['columns' => []]],
|
|
],
|
|
],
|
|
'meta' => ['title' => 'Sample List'],
|
|
];
|
|
File::put($layoutsPath . '/index.json', json_encode($indexLayout, JSON_PRETTY_PRINT));
|
|
|
|
// edit.json
|
|
$editLayout = [
|
|
'layout_name' => 'test-sample_admin_edit',
|
|
'extends' => '_admin_base',
|
|
'slots' => [
|
|
'content' => [
|
|
['component' => 'Form', 'props' => ['fields' => []]],
|
|
],
|
|
],
|
|
];
|
|
File::put($layoutsPath . '/edit.json', json_encode($editLayout, JSON_PRETTY_PRINT));
|
|
}
|
|
|
|
/**
|
|
* 모듈 레이아웃 soft delete 테스트
|
|
*/
|
|
public function test_soft_delete_module_layouts_on_deactivate(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-test-module';
|
|
|
|
// 모듈 레이아웃 생성
|
|
$layout1 = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$moduleIdentifier}_admin_index",
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
|
|
$layout2 = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$moduleIdentifier}_admin_edit",
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
|
|
// 리플렉션을 통해 protected 메서드 호출
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('softDeleteModuleLayouts');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->moduleManager, $moduleIdentifier);
|
|
|
|
// soft deleted 확인
|
|
$this->assertSoftDeleted('template_layouts', ['id' => $layout1->id]);
|
|
$this->assertSoftDeleted('template_layouts', ['id' => $layout2->id]);
|
|
|
|
// 일반 조회에서는 나타나지 않음
|
|
$this->assertNull(TemplateLayout::find($layout1->id));
|
|
$this->assertNull(TemplateLayout::find($layout2->id));
|
|
|
|
// withTrashed로 조회 가능
|
|
$this->assertNotNull(TemplateLayout::withTrashed()->find($layout1->id));
|
|
$this->assertNotNull(TemplateLayout::withTrashed()->find($layout2->id));
|
|
}
|
|
|
|
/**
|
|
* 모듈 레이아웃 복원 테스트
|
|
*/
|
|
public function test_restore_module_layouts_on_activate(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-restore-test';
|
|
|
|
// 모듈 레이아웃 생성 및 soft delete
|
|
$layout = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$moduleIdentifier}_admin_index",
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
$layout->delete();
|
|
|
|
// soft deleted 확인
|
|
$this->assertSoftDeleted('template_layouts', ['id' => $layout->id]);
|
|
|
|
// 리플렉션을 통해 protected 메서드 호출
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('restoreModuleLayouts');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->moduleManager, $moduleIdentifier);
|
|
|
|
// 복원 확인
|
|
$restoredLayout = TemplateLayout::find($layout->id);
|
|
$this->assertNotNull($restoredLayout);
|
|
$this->assertNull($restoredLayout->deleted_at);
|
|
}
|
|
|
|
/**
|
|
* 캐시 무효화 메서드 테스트
|
|
*/
|
|
public function test_invalidate_layout_cache_method(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-cache-test';
|
|
$layoutName = "{$moduleIdentifier}_admin_index";
|
|
|
|
// 레이아웃 생성
|
|
$layout = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => $layoutName,
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
|
|
// LayoutService와 동일한 캐시 키 패턴으로 설정: template.{templateId}.layout.{layoutName}
|
|
$cacheKey = "g7:core:template.{$this->adminTemplate->id}.layout.{$layoutName}";
|
|
Cache::put($cacheKey, $layout->content, 3600);
|
|
$this->assertNotNull(Cache::get($cacheKey));
|
|
|
|
// invalidateLayoutCache 직접 호출
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('invalidateLayoutCache');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->moduleManager, $moduleIdentifier);
|
|
|
|
// 캐시 키가 삭제되었음을 확인
|
|
$this->assertNull(Cache::get($cacheKey));
|
|
}
|
|
|
|
/**
|
|
* 다른 모듈의 레이아웃은 영향받지 않음 테스트
|
|
*/
|
|
public function test_soft_delete_only_affects_target_module(): void
|
|
{
|
|
$targetModule = 'sirsoft-target';
|
|
$otherModule = 'sirsoft-other';
|
|
|
|
// 대상 모듈 레이아웃
|
|
$targetLayout = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$targetModule}_admin_index",
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $targetModule,
|
|
]);
|
|
|
|
// 다른 모듈 레이아웃
|
|
$otherLayout = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$otherModule}_admin_index",
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $otherModule,
|
|
]);
|
|
|
|
// 대상 모듈만 soft delete
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('softDeleteModuleLayouts');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->moduleManager, $targetModule);
|
|
|
|
// 대상 모듈은 삭제됨
|
|
$this->assertSoftDeleted('template_layouts', ['id' => $targetLayout->id]);
|
|
|
|
// 다른 모듈은 유지됨
|
|
$this->assertNotNull(TemplateLayout::find($otherLayout->id));
|
|
}
|
|
|
|
/**
|
|
* 레이아웃이 없는 모듈 soft delete 테스트
|
|
*/
|
|
public function test_soft_delete_handles_module_without_layouts(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-no-layouts';
|
|
|
|
// 레이아웃 없이 soft delete 호출
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('softDeleteModuleLayouts');
|
|
$method->setAccessible(true);
|
|
|
|
// 예외 없이 실행되어야 함
|
|
$method->invoke($this->moduleManager, $moduleIdentifier);
|
|
$this->assertTrue(true); // 예외가 발생하지 않음을 확인
|
|
}
|
|
|
|
/**
|
|
* 레이아웃 이름 생성 테스트
|
|
*/
|
|
public function test_generate_layout_name_from_file_path(): void
|
|
{
|
|
// 테스트 모듈 레이아웃 디렉토리 생성
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
File::makeDirectory($layoutsPath . '/admin', 0755, true);
|
|
|
|
$testLayout = ['slots' => []];
|
|
File::put($layoutsPath . '/admin/index.json', json_encode($testLayout));
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('generateLayoutNameFromPath');
|
|
$method->setAccessible(true);
|
|
|
|
// Windows/Unix 경로 정규화
|
|
$normalizedBasePath = str_replace('/', DIRECTORY_SEPARATOR, $layoutsPath);
|
|
$normalizedFilePath = str_replace('/', DIRECTORY_SEPARATOR, $layoutsPath . '/admin/index.json');
|
|
|
|
$result = $method->invoke(
|
|
$this->moduleManager,
|
|
$normalizedBasePath,
|
|
$normalizedFilePath,
|
|
'test-sample'
|
|
);
|
|
|
|
$this->assertEquals('test-sample_admin_index', $result);
|
|
}
|
|
|
|
/**
|
|
* 중첩 디렉토리의 레이아웃 이름 생성 테스트
|
|
*/
|
|
public function test_generate_layout_name_with_nested_path(): void
|
|
{
|
|
// 중첩 디렉토리 생성
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
File::makeDirectory($layoutsPath . '/admin/products', 0755, true);
|
|
|
|
$testLayout = ['slots' => []];
|
|
File::put($layoutsPath . '/admin/products/list.json', json_encode($testLayout));
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('generateLayoutNameFromPath');
|
|
$method->setAccessible(true);
|
|
|
|
// Windows/Unix 경로 정규화
|
|
$normalizedBasePath = str_replace('/', DIRECTORY_SEPARATOR, $layoutsPath);
|
|
$normalizedFilePath = str_replace('/', DIRECTORY_SEPARATOR, $layoutsPath . '/admin/products/list.json');
|
|
|
|
$result = $method->invoke(
|
|
$this->moduleManager,
|
|
$normalizedBasePath,
|
|
$normalizedFilePath,
|
|
'test-sample'
|
|
);
|
|
|
|
$this->assertEquals('test-sample_admin_products_list', $result);
|
|
}
|
|
|
|
/**
|
|
* 레이아웃 파일 스캔 테스트
|
|
*/
|
|
public function test_scan_layout_files_recursively(): void
|
|
{
|
|
// 테스트 디렉토리 구조 생성
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
File::makeDirectory($layoutsPath . '/admin/products', 0755, true);
|
|
File::makeDirectory($layoutsPath . '/admin/orders', 0755, true);
|
|
|
|
$testLayout = ['slots' => []];
|
|
File::put($layoutsPath . '/admin/index.json', json_encode($testLayout));
|
|
File::put($layoutsPath . '/admin/products/list.json', json_encode($testLayout));
|
|
File::put($layoutsPath . '/admin/orders/detail.json', json_encode($testLayout));
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('scanLayoutFilesRecursively');
|
|
$method->setAccessible(true);
|
|
|
|
$files = $method->invoke($this->moduleManager, $layoutsPath);
|
|
|
|
$this->assertCount(3, $files);
|
|
}
|
|
|
|
/**
|
|
* 모듈 레이아웃을 여러 admin 템플릿에 등록하는지 테스트
|
|
*/
|
|
public function test_register_layouts_to_multiple_admin_templates(): void
|
|
{
|
|
// 두 번째 admin 템플릿 생성
|
|
$secondTemplate = Template::create([
|
|
'identifier' => 'second-admin',
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '두 번째 관리자', 'en' => 'Second Admin'],
|
|
'version' => '1.0.0',
|
|
'type' => 'admin',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
// 모듈 레이아웃 등록 (직접 테스트)
|
|
$moduleIdentifier = 'sirsoft-multi-template';
|
|
$layoutName = "{$moduleIdentifier}_admin_index";
|
|
|
|
// 각 템플릿에 레이아웃 등록
|
|
foreach ([$this->adminTemplate, $secondTemplate] as $template) {
|
|
TemplateLayout::create([
|
|
'template_id' => $template->id,
|
|
'name' => $layoutName,
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
}
|
|
|
|
// 각 템플릿에 레이아웃이 등록되었는지 확인
|
|
$layout1 = TemplateLayout::where('template_id', $this->adminTemplate->id)
|
|
->where('name', $layoutName)
|
|
->first();
|
|
$layout2 = TemplateLayout::where('template_id', $secondTemplate->id)
|
|
->where('name', $layoutName)
|
|
->first();
|
|
|
|
$this->assertNotNull($layout1);
|
|
$this->assertNotNull($layout2);
|
|
$this->assertNotEquals($layout1->id, $layout2->id);
|
|
}
|
|
|
|
/**
|
|
* source_type이 module로 올바르게 설정되는지 테스트
|
|
*/
|
|
public function test_layout_source_type_is_module(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-source-test';
|
|
|
|
$layout = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$moduleIdentifier}_admin_index",
|
|
'content' => ['slots' => []],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
|
|
$this->assertEquals(LayoutSourceType::Module, $layout->source_type);
|
|
$this->assertEquals($moduleIdentifier, $layout->source_identifier);
|
|
}
|
|
|
|
/**
|
|
* extends 필드가 보존되는지 테스트
|
|
*/
|
|
public function test_layout_extends_field_is_preserved(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-extends-test';
|
|
$extendsValue = '_admin_base';
|
|
|
|
$layout = TemplateLayout::create([
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => "{$moduleIdentifier}_admin_index",
|
|
'content' => ['slots' => []],
|
|
'extends' => $extendsValue,
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]);
|
|
|
|
$this->assertEquals($extendsValue, $layout->extends);
|
|
}
|
|
|
|
/**
|
|
* updateOrCreate로 중복 등록 방지 테스트
|
|
*/
|
|
public function test_prevent_duplicate_layout_registration(): void
|
|
{
|
|
$moduleIdentifier = 'sirsoft-duplicate-test';
|
|
$layoutName = "{$moduleIdentifier}_admin_index";
|
|
|
|
// 첫 번째 등록
|
|
TemplateLayout::updateOrCreate(
|
|
[
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => $layoutName,
|
|
],
|
|
[
|
|
'content' => ['version' => '1'],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]
|
|
);
|
|
|
|
// 두 번째 등록 (업데이트)
|
|
TemplateLayout::updateOrCreate(
|
|
[
|
|
'template_id' => $this->adminTemplate->id,
|
|
'name' => $layoutName,
|
|
],
|
|
[
|
|
'content' => ['version' => '2'],
|
|
'source_type' => LayoutSourceType::Module,
|
|
'source_identifier' => $moduleIdentifier,
|
|
]
|
|
);
|
|
|
|
// 레이아웃이 1개만 존재
|
|
$count = TemplateLayout::where('template_id', $this->adminTemplate->id)
|
|
->where('name', $layoutName)
|
|
->count();
|
|
|
|
$this->assertEquals(1, $count);
|
|
|
|
// 내용이 업데이트됨
|
|
$layout = TemplateLayout::where('template_id', $this->adminTemplate->id)
|
|
->where('name', $layoutName)
|
|
->first();
|
|
|
|
$this->assertEquals('2', $layout->content['version']);
|
|
}
|
|
|
|
/**
|
|
* 모듈 레이아웃 등록 시 moduleIdentifier 접두사가 추가되는지 테스트
|
|
*/
|
|
public function test_module_layout_registration_adds_identifier_prefix(): void
|
|
{
|
|
$moduleIdentifier = 'test-prefix-module';
|
|
$layoutName = 'admin_sample_index';
|
|
|
|
// 모듈 레코드 생성
|
|
Module::create([
|
|
'identifier' => $moduleIdentifier,
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '접두사 테스트', 'en' => 'Prefix Test'],
|
|
'version' => '1.0.0',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
// registerLayoutToTemplate 메서드 직접 테스트
|
|
$layoutData = [
|
|
'layout_name' => $layoutName,
|
|
'version' => '1.0.0',
|
|
'slots' => [
|
|
'content' => [],
|
|
],
|
|
];
|
|
|
|
// 접두사가 추가된 레이아웃 이름 생성 (registerModuleLayouts에서 수행하는 로직)
|
|
$prefixedLayoutName = $moduleIdentifier . '.' . $layoutName;
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('registerLayoutToTemplate');
|
|
$method->setAccessible(true);
|
|
|
|
$method->invoke(
|
|
$this->moduleManager,
|
|
$this->adminTemplate,
|
|
$prefixedLayoutName,
|
|
$layoutData,
|
|
$moduleIdentifier
|
|
);
|
|
|
|
// DB에서 확인
|
|
$layout = TemplateLayout::where('template_id', $this->adminTemplate->id)
|
|
->where('source_identifier', $moduleIdentifier)
|
|
->first();
|
|
|
|
$this->assertNotNull($layout);
|
|
// 예상: test-prefix-module.admin_sample_index
|
|
$this->assertEquals('test-prefix-module.admin_sample_index', $layout->name);
|
|
}
|
|
|
|
/**
|
|
* registerLayoutToTemplate 호출 시 original_content_hash / size 가 저장되는지 테스트.
|
|
*
|
|
* layout_strategy=keep 에서 사용자 수정 감지에 사용되므로 신규 등록 시 hash 가 채워져야 함.
|
|
*/
|
|
public function test_register_layout_stores_original_content_hash_and_size(): void
|
|
{
|
|
$moduleIdentifier = 'test-hash-module';
|
|
|
|
Module::create([
|
|
'identifier' => $moduleIdentifier,
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '해시 테스트', 'en' => 'Hash Test'],
|
|
'version' => '1.0.0',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
$layoutData = [
|
|
'layout_name' => 'admin_home',
|
|
'version' => '1.0.0',
|
|
'slots' => ['content' => [['component' => 'Div']]],
|
|
];
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('registerLayoutToTemplate');
|
|
$method->setAccessible(true);
|
|
|
|
$method->invoke(
|
|
$this->moduleManager,
|
|
$this->adminTemplate,
|
|
$moduleIdentifier.'.admin_home',
|
|
$layoutData,
|
|
$moduleIdentifier,
|
|
);
|
|
|
|
$layout = TemplateLayout::where('template_id', $this->adminTemplate->id)
|
|
->where('source_identifier', $moduleIdentifier)
|
|
->first();
|
|
|
|
$this->assertNotNull($layout);
|
|
$this->assertNotNull($layout->original_content_hash, 'hash 가 저장되어야 함');
|
|
$this->assertEquals(64, strlen($layout->original_content_hash), 'SHA-256 hex length');
|
|
$this->assertGreaterThan(0, $layout->original_content_size);
|
|
|
|
// hash 재계산이 동일한지
|
|
$expectedHash = hash('sha256', json_encode($layoutData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
$this->assertEquals($expectedHash, $layout->original_content_hash);
|
|
}
|
|
|
|
/**
|
|
* hasModifiedLayouts: 수정된 레이아웃이 없을 때 결과 검증.
|
|
*/
|
|
public function test_has_modified_layouts_returns_false_when_unchanged(): void
|
|
{
|
|
$moduleIdentifier = 'test-nomod-module';
|
|
|
|
Module::create([
|
|
'identifier' => $moduleIdentifier,
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '미수정 테스트', 'en' => 'Unmodified Test'],
|
|
'version' => '1.0.0',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
$layoutData = ['layout_name' => 'home', 'slots' => []];
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('registerLayoutToTemplate');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->moduleManager, $this->adminTemplate, $moduleIdentifier.'.home', $layoutData, $moduleIdentifier);
|
|
|
|
$result = $this->moduleManager->hasModifiedLayouts($moduleIdentifier);
|
|
|
|
$this->assertFalse($result['has_modified_layouts']);
|
|
$this->assertEquals(0, $result['modified_count']);
|
|
$this->assertEmpty($result['modified_layouts']);
|
|
}
|
|
|
|
/**
|
|
* hasModifiedLayouts: 사용자가 content 를 수정한 레이아웃을 감지한다.
|
|
*/
|
|
public function test_has_modified_layouts_detects_user_modification(): void
|
|
{
|
|
$moduleIdentifier = 'test-modified-module';
|
|
|
|
Module::create([
|
|
'identifier' => $moduleIdentifier,
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '수정 테스트', 'en' => 'Modified Test'],
|
|
'version' => '1.0.0',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
$originalData = ['layout_name' => 'home', 'slots' => ['content' => []]];
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('registerLayoutToTemplate');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->moduleManager, $this->adminTemplate, $moduleIdentifier.'.home', $originalData, $moduleIdentifier);
|
|
|
|
// 사용자 UI 에서 content 만 수정 (hash 는 그대로 유지)
|
|
TemplateLayout::where('source_identifier', $moduleIdentifier)
|
|
->update(['content' => ['layout_name' => 'home', 'slots' => ['content' => [['component' => 'Div', 'props' => ['className' => 'user-added']]]]]]);
|
|
|
|
$result = $this->moduleManager->hasModifiedLayouts($moduleIdentifier);
|
|
|
|
$this->assertTrue($result['has_modified_layouts']);
|
|
$this->assertEquals(1, $result['modified_count']);
|
|
$this->assertCount(1, $result['modified_layouts']);
|
|
$this->assertEquals($moduleIdentifier.'.home', $result['modified_layouts'][0]['name']);
|
|
}
|
|
|
|
/**
|
|
* refreshModuleLayouts 호출 시 레이아웃이 변경되면 캐시 버전이 증가하는지 테스트
|
|
*/
|
|
public function test_refresh_module_layouts_increments_cache_version_on_change(): void
|
|
{
|
|
// 초기 캐시 버전 설정
|
|
Cache::put('g7:core:ext.cache_version', 1000);
|
|
|
|
// 테스트용 모듈 생성
|
|
$module = Module::create([
|
|
'identifier' => 'test-sample',
|
|
'vendor' => 'test',
|
|
'name' => ['ko' => '테스트', 'en' => 'Test'],
|
|
'version' => '1.0.0',
|
|
'status' => ExtensionStatus::Active->value,
|
|
'description' => ['ko' => '테스트용', 'en' => 'For testing'],
|
|
]);
|
|
|
|
// 모듈 레이아웃 파일 생성
|
|
$this->createTestModuleLayouts();
|
|
|
|
// 실제 모듈 클래스를 로드하기 위해 리플렉션 사용
|
|
// refreshModuleLayouts는 실제 Module 인스턴스가 필요하므로 직접 호출은 어려움
|
|
// 대신 incrementExtensionCacheVersion 메서드가 호출되는지 검증
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('incrementExtensionCacheVersion');
|
|
$method->setAccessible(true);
|
|
|
|
$beforeTime = time();
|
|
$method->invoke($this->moduleManager);
|
|
$afterTime = time();
|
|
|
|
$newVersion = Cache::get('g7:core:ext.cache_version');
|
|
$this->assertGreaterThanOrEqual($beforeTime, $newVersion);
|
|
$this->assertLessThanOrEqual($afterTime, $newVersion);
|
|
}
|
|
|
|
// ============================================
|
|
// getLayoutTargetType() 테스트
|
|
// ============================================
|
|
|
|
/**
|
|
* admin 경로 파일은 'admin' 타입 반환
|
|
*/
|
|
public function test_get_layout_target_type_returns_admin_for_admin_path(): void
|
|
{
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
$layoutFile = $this->testModulePath . '/resources/layouts/admin/index.json';
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('getLayoutTargetType');
|
|
$method->setAccessible(true);
|
|
|
|
$result = $method->invoke($this->moduleManager, $layoutsPath, $layoutFile);
|
|
|
|
$this->assertEquals('admin', $result);
|
|
}
|
|
|
|
/**
|
|
* user 경로 파일은 'user' 타입 반환
|
|
*/
|
|
public function test_get_layout_target_type_returns_user_for_user_path(): void
|
|
{
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
$layoutFile = $this->testModulePath . '/resources/layouts/user/main.json';
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('getLayoutTargetType');
|
|
$method->setAccessible(true);
|
|
|
|
$result = $method->invoke($this->moduleManager, $layoutsPath, $layoutFile);
|
|
|
|
$this->assertEquals('user', $result);
|
|
}
|
|
|
|
/**
|
|
* 루트에 직접 위치한 파일은 null 반환 + 경고 로그
|
|
*/
|
|
public function test_get_layout_target_type_returns_null_for_root_file(): void
|
|
{
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
$layoutFile = $this->testModulePath . '/resources/layouts/settings.json';
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('getLayoutTargetType');
|
|
$method->setAccessible(true);
|
|
|
|
\Illuminate\Support\Facades\Log::shouldReceive('warning')
|
|
->once()
|
|
->withArgs(function ($message) {
|
|
return str_contains($message, '스킵됩니다');
|
|
});
|
|
|
|
$result = $method->invoke($this->moduleManager, $layoutsPath, $layoutFile);
|
|
|
|
$this->assertNull($result);
|
|
}
|
|
|
|
/**
|
|
* 중첩 하위 디렉토리도 첫 세그먼트로 판별
|
|
*/
|
|
public function test_get_layout_target_type_handles_nested_admin_path(): void
|
|
{
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
$layoutFile = $this->testModulePath . '/resources/layouts/admin/products/index.json';
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('getLayoutTargetType');
|
|
$method->setAccessible(true);
|
|
|
|
$result = $method->invoke($this->moduleManager, $layoutsPath, $layoutFile);
|
|
|
|
$this->assertEquals('admin', $result);
|
|
}
|
|
|
|
/**
|
|
* admin/user 외 디렉토리는 null 반환 + 경고 로그
|
|
*/
|
|
public function test_get_layout_target_type_returns_null_for_unsupported_directory(): void
|
|
{
|
|
$layoutsPath = $this->testModulePath . '/resources/layouts';
|
|
$layoutFile = $this->testModulePath . '/resources/layouts/api/index.json';
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('getLayoutTargetType');
|
|
$method->setAccessible(true);
|
|
|
|
\Illuminate\Support\Facades\Log::shouldReceive('warning')
|
|
->once()
|
|
->withArgs(function ($message) {
|
|
return str_contains($message, '스킵됩니다');
|
|
});
|
|
|
|
$result = $method->invoke($this->moduleManager, $layoutsPath, $layoutFile);
|
|
|
|
$this->assertNull($result);
|
|
}
|
|
|
|
/**
|
|
* Windows 백슬래시 경로도 정상 처리
|
|
*/
|
|
public function test_get_layout_target_type_handles_windows_backslash_paths(): void
|
|
{
|
|
$layoutsPath = 'C:\\projects\\g7\\modules\\test\\resources\\layouts';
|
|
$layoutFile = 'C:\\projects\\g7\\modules\\test\\resources\\layouts\\admin\\index.json';
|
|
|
|
$reflection = new \ReflectionClass($this->moduleManager);
|
|
$method = $reflection->getMethod('getLayoutTargetType');
|
|
$method->setAccessible(true);
|
|
|
|
$result = $method->invoke($this->moduleManager, $layoutsPath, $layoutFile);
|
|
|
|
$this->assertEquals('admin', $result);
|
|
}
|
|
|
|
/**
|
|
* 레이아웃 변경이 없으면 캐시 버전이 증가하지 않음 (검증용 단위 테스트)
|
|
*/
|
|
public function test_cache_version_not_incremented_when_no_layout_changes(): void
|
|
{
|
|
$initialVersion = 1000;
|
|
Cache::put('g7:core:ext.cache_version', $initialVersion);
|
|
|
|
// 변경 없는 상태에서는 incrementExtensionCacheVersion이 호출되지 않으므로
|
|
// 캐시 버전이 유지됨
|
|
$currentVersion = Cache::get('g7:core:ext.cache_version');
|
|
|
|
$this->assertEquals($initialVersion, $currentVersion);
|
|
}
|
|
} |