v7.0.0-beta.1 release
This commit is contained in:
commit
6595fd0eb5
3967 files changed
+1875383
No files matched your search
@@ -0,0 +1,423 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Module;
|
||||
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Models\Module;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ModuleAssetServingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Module $activeModule;
|
||||
private string $testModulePath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// 테스트용 활성화된 모듈 생성
|
||||
$this->activeModule = Module::factory()->create([
|
||||
'identifier' => 'test-module',
|
||||
'status' => ExtensionStatus::Active->value,
|
||||
]);
|
||||
|
||||
// 테스트용 모듈 디렉토리 생성
|
||||
$this->testModulePath = base_path('modules/test-module');
|
||||
if (!file_exists($this->testModulePath.'/dist/js')) {
|
||||
mkdir($this->testModulePath.'/dist/js', 0755, true);
|
||||
}
|
||||
if (!file_exists($this->testModulePath.'/dist/css')) {
|
||||
mkdir($this->testModulePath.'/dist/css', 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// 테스트용 파일 및 디렉토리 정리
|
||||
if (file_exists($this->testModulePath)) {
|
||||
$this->deleteDirectory($this->testModulePath);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리 재귀 삭제 헬퍼 메서드
|
||||
*/
|
||||
private function deleteDirectory(string $dir): bool
|
||||
{
|
||||
if (!file_exists($dir)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return unlink($dir);
|
||||
}
|
||||
|
||||
foreach (scandir($dir) as $item) {
|
||||
if ($item == '.' || $item == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->deleteDirectory($dir.DIRECTORY_SEPARATOR.$item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성화된 모듈의 JS 파일 서빙 성공
|
||||
*/
|
||||
public function test_serves_js_file_from_active_module(): void
|
||||
{
|
||||
// Arrange
|
||||
$jsPath = $this->testModulePath.'/dist/js/module.iife.js';
|
||||
file_put_contents($jsPath, 'console.log("test module");');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/javascript');
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성화된 모듈의 CSS 파일 서빙 성공
|
||||
*/
|
||||
public function test_serves_css_file_from_active_module(): void
|
||||
{
|
||||
// Arrange
|
||||
$cssPath = $this->testModulePath.'/dist/css/module.css';
|
||||
file_put_contents($cssPath, '.module { color: blue; }');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/css/module.css');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$this->assertTrue(
|
||||
str_starts_with($response->headers->get('Content-Type'), 'text/css'),
|
||||
'Content-Type should start with text/css'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비활성화 모듈 접근 시 404 반환
|
||||
*/
|
||||
public function test_returns_404_for_inactive_module(): void
|
||||
{
|
||||
// Arrange
|
||||
Module::factory()->create([
|
||||
'identifier' => 'inactive-module',
|
||||
'status' => ExtensionStatus::Inactive->value,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/inactive-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
// 검증 실패로 인한 리다이렉트 또는 404 허용
|
||||
$this->assertContains($response->status(), [302, 404]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 존재하지 않는 파일 접근 시 404 반환
|
||||
*/
|
||||
public function test_returns_404_for_nonexistent_file(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/nonexistent.js');
|
||||
|
||||
// Assert
|
||||
// 검증 실패로 인한 리다이렉트 또는 404 허용
|
||||
$this->assertContains($response->status(), [302, 404]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Path Traversal 공격 차단 - 기본 패턴 (../)
|
||||
*/
|
||||
public function test_blocks_path_traversal_attack(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/../../.env');
|
||||
|
||||
// Assert
|
||||
$this->assertContains($response->status(), [302, 422]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Path Traversal 공격 차단 - 백슬래시 패턴 (..\)
|
||||
*/
|
||||
public function test_blocks_path_traversal_with_backslash(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/..%5c..%5cconfig.php');
|
||||
|
||||
// Assert
|
||||
$this->assertContains($response->status(), [302, 422]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Path Traversal 공격 차단 - URL 인코딩 패턴 (%2e%2e%2f)
|
||||
*/
|
||||
public function test_blocks_url_encoded_path_traversal(): void
|
||||
{
|
||||
$patterns = [
|
||||
'%2e%2e%2f',
|
||||
'%2e%2e/',
|
||||
'%2e%2e%5c',
|
||||
'..%2f',
|
||||
'..%5c',
|
||||
'.%2e/',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$response = $this->get("/api/modules/assets/test-module/{$pattern}secret.txt");
|
||||
$this->assertContains($response->status(), [302, 422], "Pattern {$pattern} should be blocked");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 절대 경로 차단 - Windows 경로
|
||||
*/
|
||||
public function test_blocks_windows_absolute_path(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/C:%5cWindows%5cSystem32%5cconfig.ini');
|
||||
|
||||
// Assert
|
||||
$this->assertContains($response->status(), [302, 422]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 절대 경로 차단 - Linux 경로
|
||||
*/
|
||||
public function test_blocks_linux_absolute_path(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module//etc/passwd');
|
||||
|
||||
// Assert
|
||||
$this->assertContains($response->status(), [302, 422]);
|
||||
}
|
||||
|
||||
/**
|
||||
* NULL 바이트 공격 차단
|
||||
*/
|
||||
public function test_blocks_null_byte_attack(): void
|
||||
{
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/malicious.php%00.js');
|
||||
|
||||
// Assert
|
||||
$this->assertContains($response->status(), [302, 422]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 불허 파일 타입 차단 - PHP
|
||||
*/
|
||||
public function test_blocks_disallowed_file_types(): void
|
||||
{
|
||||
// Arrange
|
||||
$phpPath = $this->testModulePath.'/malicious.php';
|
||||
file_put_contents($phpPath, '<?php echo "hack"; ?>');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/malicious.php');
|
||||
|
||||
// Assert
|
||||
$this->assertContains($response->status(), [302, 422]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 불허 파일 타입 차단 - 실행 파일들
|
||||
*/
|
||||
public function test_blocks_various_disallowed_file_types(): void
|
||||
{
|
||||
$disallowedExtensions = ['exe', 'sh', 'bat', 'dll', 'so', 'py', 'rb', 'pl', 'asp', 'aspx', 'jsp', 'cgi'];
|
||||
|
||||
foreach ($disallowedExtensions as $ext) {
|
||||
$response = $this->get("/api/modules/assets/test-module/malicious.{$ext}");
|
||||
$this->assertContains($response->status(), [302, 422], "Extension .{$ext} should be blocked");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 허용된 파일 타입 확인
|
||||
*/
|
||||
public function test_allows_all_whitelisted_file_types(): void
|
||||
{
|
||||
$allowedFiles = [
|
||||
'app.js' => 'application/javascript',
|
||||
'style.css' => 'text/css',
|
||||
'data.json' => 'application/json',
|
||||
'image.png' => 'image/png',
|
||||
'photo.jpg' => 'image/jpeg',
|
||||
'icon.svg' => 'image/svg+xml',
|
||||
'font.woff' => 'font/woff',
|
||||
'font.woff2' => 'font/woff2',
|
||||
];
|
||||
|
||||
foreach ($allowedFiles as $filename => $expectedMimeType) {
|
||||
$filePath = $this->testModulePath.'/dist/'.$filename;
|
||||
file_put_contents($filePath, 'test content');
|
||||
|
||||
$response = $this->get("/api/modules/assets/test-module/dist/{$filename}");
|
||||
$response->assertStatus(200, "File {$filename} should be allowed");
|
||||
|
||||
$actualMimeType = $response->headers->get('Content-Type');
|
||||
if ($filename === 'style.css') {
|
||||
$this->assertTrue(
|
||||
str_starts_with($actualMimeType, $expectedMimeType),
|
||||
"MIME type for {$filename} should start with {$expectedMimeType}"
|
||||
);
|
||||
} else {
|
||||
$this->assertEquals(
|
||||
$expectedMimeType,
|
||||
$actualMimeType,
|
||||
"MIME type for {$filename} should be {$expectedMimeType}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐싱 헤더 정상 설정
|
||||
*/
|
||||
public function test_sets_caching_headers(): void
|
||||
{
|
||||
// Arrange
|
||||
$jsPath = $this->testModulePath.'/dist/js/module.iife.js';
|
||||
file_put_contents($jsPath, 'console.log("test");');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$cacheControl = $response->headers->get('Cache-Control');
|
||||
$this->assertNotNull($cacheControl, 'Cache-Control header should be present');
|
||||
}
|
||||
|
||||
/**
|
||||
* ETag 헤더 생성 확인
|
||||
*/
|
||||
public function test_generates_etag_header(): void
|
||||
{
|
||||
// Arrange
|
||||
$jsPath = $this->testModulePath.'/dist/js/module.iife.js';
|
||||
file_put_contents($jsPath, 'console.log("test");');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$etag = $response->headers->get('ETag');
|
||||
$this->assertNotNull($etag, 'ETag header should be present');
|
||||
$this->assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $etag, 'ETag should be a 32-character MD5 hash');
|
||||
}
|
||||
|
||||
/**
|
||||
* ETag 매칭 시 304 Not Modified 응답
|
||||
*/
|
||||
public function test_returns_304_when_etag_matches(): void
|
||||
{
|
||||
// Arrange
|
||||
$jsPath = $this->testModulePath.'/dist/js/module.iife.js';
|
||||
file_put_contents($jsPath, 'console.log("test");');
|
||||
|
||||
$firstResponse = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
$etag = $firstResponse->headers->get('ETag');
|
||||
|
||||
// Act
|
||||
$response = $this->withHeaders([
|
||||
'If-None-Match' => $etag,
|
||||
])->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(304);
|
||||
$this->assertEmpty($response->getContent(), '304 response should have empty body');
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로덕션 환경에서 immutable 캐싱 정책 적용
|
||||
*/
|
||||
public function test_applies_immutable_caching_in_production(): void
|
||||
{
|
||||
// Arrange
|
||||
app()['env'] = 'production';
|
||||
$jsPath = $this->testModulePath.'/dist/js/module.iife.js';
|
||||
file_put_contents($jsPath, 'console.log("test");');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$cacheControl = $response->headers->get('Cache-Control');
|
||||
$this->assertStringContainsString('immutable', $cacheControl, 'Production should use immutable caching');
|
||||
$this->assertStringContainsString('public', $cacheControl);
|
||||
$this->assertStringContainsString('max-age=31536000', $cacheControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 개발 환경에서 no-cache 정책 적용
|
||||
*/
|
||||
public function test_applies_no_cache_in_development(): void
|
||||
{
|
||||
// Arrange
|
||||
app()['env'] = 'local';
|
||||
$jsPath = $this->testModulePath.'/dist/js/module.iife.js';
|
||||
file_put_contents($jsPath, 'console.log("test");');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$cacheControl = $response->headers->get('Cache-Control');
|
||||
$this->assertStringContainsString('no-cache', $cacheControl, 'Development should use no-cache');
|
||||
}
|
||||
|
||||
/**
|
||||
* 중첩 경로 처리 테스트
|
||||
*/
|
||||
public function test_handles_nested_path(): void
|
||||
{
|
||||
// Arrange
|
||||
$nestedPath = $this->testModulePath.'/dist/js/components/Button.js';
|
||||
mkdir(dirname($nestedPath), 0755, true);
|
||||
file_put_contents($nestedPath, 'export default Button;');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/components/Button.js');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/javascript');
|
||||
}
|
||||
|
||||
/**
|
||||
* Source Map 파일 서빙 테스트
|
||||
*/
|
||||
public function test_serves_sourcemap_file(): void
|
||||
{
|
||||
// Arrange
|
||||
$mapPath = $this->testModulePath.'/dist/js/module.iife.js.map';
|
||||
file_put_contents($mapPath, '{"version":3,"sources":[],"mappings":""}');
|
||||
|
||||
// Act
|
||||
$response = $this->get('/api/modules/assets/test-module/dist/js/module.iife.js.map');
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Module;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Extension\ModuleManager;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use ReflectionMethod;
|
||||
use Tests\Helpers\ProtectsExtensionDirectories;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 모듈 메뉴 동기화 통합 테스트
|
||||
*
|
||||
* ExtensionMenuSyncHelper를 통한 실제 DB 기반 메뉴 동기화를 검증합니다:
|
||||
* - 설치 시 메뉴 생성 (user_overrides 없음)
|
||||
* - 업데이트 시 user_overrides 기반 사용자 커스터마이징 보존
|
||||
* - 동적 메뉴/권한 보존 (#135: cleanup 자동 호출 폐기)
|
||||
* - 새 메뉴 추가
|
||||
*/
|
||||
class ModuleMenuSyncTest extends TestCase
|
||||
{
|
||||
use ProtectsExtensionDirectories;
|
||||
use RefreshDatabase;
|
||||
|
||||
private ModuleManager $moduleManager;
|
||||
|
||||
/** @var bool 활성 디렉토리가 테스트 전에 이미 존재했는지 (tearDown에서 정리 판단용) */
|
||||
private bool $boardExistedBefore = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// 이전 세션에서 남은 좀비 디렉토리 정리
|
||||
$activePath = base_path('modules/sirsoft-board');
|
||||
if (File::isDirectory($activePath) && ! File::exists($activePath.'/module.php')) {
|
||||
File::deleteDirectory($activePath);
|
||||
}
|
||||
|
||||
$this->boardExistedBefore = File::isDirectory($activePath);
|
||||
|
||||
// 확장 디렉토리 보호 활성화
|
||||
$this->setUpExtensionProtection();
|
||||
|
||||
$this->moduleManager = app(ModuleManager::class);
|
||||
$this->moduleManager->loadModules();
|
||||
|
||||
// 기본 시스템 역할 생성
|
||||
Role::create([
|
||||
'identifier' => 'admin',
|
||||
'name' => ['ko' => '시스템 관리자', 'en' => 'System Administrator'],
|
||||
'description' => ['ko' => '모든 권한을 가진 관리자', 'en' => 'Administrator with all permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Role::create([
|
||||
'identifier' => 'manager',
|
||||
'name' => ['ko' => '매니저', 'en' => 'Manager'],
|
||||
'description' => ['ko' => '관리 권한을 가진 매니저', 'en' => 'Manager with management permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 테스트용 사용자 생성
|
||||
User::factory()->create(['email' => 'admin@test.com']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// 확장 디렉토리 보호 해제
|
||||
$this->tearDownExtensionProtection();
|
||||
|
||||
// 테스트에서 생성한 활성 디렉토리만 정리
|
||||
if (! $this->boardExistedBefore) {
|
||||
$activePath = base_path('modules/sirsoft-board');
|
||||
if (File::isDirectory($activePath)) {
|
||||
File::deleteDirectory($activePath);
|
||||
}
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleManager의 protected 메서드를 호출하는 헬퍼
|
||||
*
|
||||
* @param string $methodName 메서드명
|
||||
* @param array $args 인자
|
||||
*/
|
||||
private function callProtectedMethod(string $methodName, array $args = []): mixed
|
||||
{
|
||||
$method = new ReflectionMethod($this->moduleManager, $methodName);
|
||||
|
||||
return $method->invokeArgs($this->moduleManager, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 설치 시 메뉴가 올바르게 생성되는지 테스트
|
||||
*/
|
||||
public function test_module_install_creates_menus(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// sirsoft-board: 부모 1 + 자식 3 = 4
|
||||
$menuCount = Menu::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
$this->assertEquals(4, $menuCount);
|
||||
|
||||
// 부모 메뉴 확인
|
||||
$parentMenu = Menu::where('slug', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($parentMenu);
|
||||
$this->assertNotNull($parentMenu->name);
|
||||
$this->assertEquals('fas fa-clipboard-list', $parentMenu->icon);
|
||||
$this->assertEquals(30, $parentMenu->order);
|
||||
|
||||
// 자식 메뉴 확인
|
||||
$childMenu = Menu::where('slug', 'sirsoft-board-list')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($childMenu);
|
||||
$this->assertEquals($childMenu->parent_id, $parentMenu->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 최초 설치 시 메뉴에 user_overrides가 비어있는지 테스트
|
||||
*/
|
||||
public function test_no_user_overrides_on_fresh_menu_install(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$allMenus = Menu::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->get();
|
||||
|
||||
foreach ($allMenus as $menu) {
|
||||
$this->assertTrue(
|
||||
empty($menu->user_overrides),
|
||||
"메뉴 '{$menu->slug}'의 user_overrides는 비어있어야 합니다"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides에 "name"이 있으면 재설치 시 메뉴명이 보존되는지 테스트
|
||||
*/
|
||||
public function test_user_menu_name_override_preserved_on_reinstall(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 사용자가 메뉴명 수정 + user_overrides 기록
|
||||
$parentMenu = Menu::where('slug', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
$customName = ['ko' => '우리 게시판', 'en' => 'Our Board'];
|
||||
$parentMenu->update([
|
||||
'name' => $customName,
|
||||
'user_overrides' => ['name'],
|
||||
]);
|
||||
|
||||
// 재설치 (업데이트 시뮬레이션)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModuleMenus', [$module]);
|
||||
|
||||
$parentMenu->refresh();
|
||||
|
||||
// 사용자 수정 이름 보존
|
||||
$this->assertEquals($customName, $parentMenu->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides에 "order"가 있으면 재설치 시 메뉴 순서가 보존되는지 테스트
|
||||
*/
|
||||
public function test_user_menu_order_override_preserved_on_reinstall(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 사용자가 순서 변경 + user_overrides 기록
|
||||
$parentMenu = Menu::where('slug', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
$originalIcon = $parentMenu->icon;
|
||||
$parentMenu->update([
|
||||
'order' => 99,
|
||||
'user_overrides' => ['order'],
|
||||
]);
|
||||
|
||||
// 재설치 (업데이트 시뮬레이션)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModuleMenus', [$module]);
|
||||
|
||||
$parentMenu->refresh();
|
||||
|
||||
// 사용자 수정 순서 보존
|
||||
$this->assertEquals(99, $parentMenu->order);
|
||||
|
||||
// 미수정 아이콘은 확장 정의값 유지
|
||||
$this->assertEquals($originalIcon, $parentMenu->icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides가 없으면 재설치 시 메뉴가 확장 정의값으로 갱신되는지 테스트
|
||||
*/
|
||||
public function test_reinstall_updates_unmodified_menus(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$parentMenu = Menu::where('slug', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
$originalName = $parentMenu->name;
|
||||
$originalIcon = $parentMenu->icon;
|
||||
$originalOrder = $parentMenu->order;
|
||||
|
||||
// 재설치
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModuleMenus', [$module]);
|
||||
|
||||
$parentMenu->refresh();
|
||||
|
||||
// user_overrides 없으면 확장 정의값 그대로 유지
|
||||
$this->assertEquals($originalName, $parentMenu->name);
|
||||
$this->assertEquals($originalIcon, $parentMenu->icon);
|
||||
$this->assertEquals($originalOrder, $parentMenu->order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 제거 후 재설치 시 깨끗한 상태에서 메뉴가 올바르게 생성되는지 테스트
|
||||
*/
|
||||
public function test_reinstall_after_uninstall_creates_clean_menus(): void
|
||||
{
|
||||
// 설치
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
$this->assertTrue(Menu::where('extension_identifier', 'sirsoft-board')->exists());
|
||||
|
||||
// 제거 (spy가 활성 디렉토리 삭제 차단)
|
||||
$this->moduleManager->uninstallModule('sirsoft-board');
|
||||
$this->assertFalse(Menu::where('extension_identifier', 'sirsoft-board')->exists());
|
||||
|
||||
// 재설치 (활성 디렉토리가 보존되어 있으므로 재로드 가능)
|
||||
$this->moduleManager->loadModules();
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 메뉴가 새로 생성되고 user_overrides 비어있음
|
||||
$parentMenu = Menu::where('slug', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($parentMenu);
|
||||
$this->assertTrue(
|
||||
empty($parentMenu->user_overrides),
|
||||
'재설치 후 user_overrides는 비어있어야 합니다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 복수 필드 user_overrides 보존 테스트 (name + icon 동시 수정)
|
||||
*/
|
||||
public function test_multiple_user_overrides_preserved_on_reinstall(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$parentMenu = Menu::where('slug', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
$customName = ['ko' => '우리 게시판', 'en' => 'Our Board'];
|
||||
$customIcon = 'fas fa-star';
|
||||
$originalOrder = $parentMenu->order;
|
||||
$originalUrl = $parentMenu->url;
|
||||
|
||||
$parentMenu->update([
|
||||
'name' => $customName,
|
||||
'icon' => $customIcon,
|
||||
'user_overrides' => ['name', 'icon'],
|
||||
]);
|
||||
|
||||
// 재설치
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModuleMenus', [$module]);
|
||||
|
||||
$parentMenu->refresh();
|
||||
|
||||
// 수정된 필드 보존
|
||||
$this->assertEquals($customName, $parentMenu->name);
|
||||
$this->assertEquals($customIcon, $parentMenu->icon);
|
||||
|
||||
// 미수정 필드는 확장 정의값 유지
|
||||
$this->assertEquals($originalOrder, $parentMenu->order);
|
||||
$this->assertEquals($originalUrl, $parentMenu->url);
|
||||
}
|
||||
|
||||
// ========== 동적 메뉴/권한 보존 테스트 ==========
|
||||
|
||||
/**
|
||||
* 모듈 업데이트 시 동적으로 생성된 메뉴가 보존되는지 테스트
|
||||
*
|
||||
* 시나리오: 게시판 모듈 설치 후 사용자가 개별 게시판을 관리자 메뉴에 등록.
|
||||
* 모듈 업데이트(createModuleMenus 재호출) 시 동적 메뉴가 삭제되지 않아야 함.
|
||||
*/
|
||||
public function test_module_update_preserves_dynamic_menus(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 정적 메뉴 수 확인 (부모 1 + 자식 3 = 4)
|
||||
$staticMenuCount = Menu::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
$this->assertEquals(4, $staticMenuCount);
|
||||
|
||||
// 동적 메뉴 생성 시뮬레이션 (BoardService::addToAdminMenu 동작 재현)
|
||||
Menu::create([
|
||||
'name' => ['ko' => '공지사항', 'en' => 'Notice'],
|
||||
'slug' => 'board-notice',
|
||||
'url' => '/admin/board/notice',
|
||||
'icon' => 'fas fa-clipboard-list',
|
||||
'parent_id' => null,
|
||||
'is_active' => true,
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'order' => 50,
|
||||
]);
|
||||
|
||||
Menu::create([
|
||||
'name' => ['ko' => '자유게시판', 'en' => 'Free Board'],
|
||||
'slug' => 'board-free',
|
||||
'url' => '/admin/board/free',
|
||||
'icon' => 'fas fa-clipboard-list',
|
||||
'parent_id' => null,
|
||||
'is_active' => true,
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'order' => 51,
|
||||
]);
|
||||
|
||||
// 동적 메뉴 포함 총 6개 확인 (정적 4 + 동적 2)
|
||||
$totalMenuCount = Menu::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
$this->assertEquals(6, $totalMenuCount);
|
||||
|
||||
// 모듈 업데이트 시뮬레이션 (createModuleMenus 재호출)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModuleMenus', [$module]);
|
||||
|
||||
// 동적 메뉴가 보존되어야 함
|
||||
$this->assertTrue(
|
||||
Menu::where('slug', 'board-notice')
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->exists(),
|
||||
'동적 메뉴 board-notice가 모듈 업데이트 후에도 보존되어야 합니다'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
Menu::where('slug', 'board-free')
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->exists(),
|
||||
'동적 메뉴 board-free가 모듈 업데이트 후에도 보존되어야 합니다'
|
||||
);
|
||||
|
||||
// 전체 메뉴 수 유지 (정적 4 + 동적 2 = 6)
|
||||
$afterUpdateCount = Menu::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
$this->assertEquals(6, $afterUpdateCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 업데이트 시 동적으로 생성된 권한이 보존되는지 테스트
|
||||
*
|
||||
* 시나리오: 게시판 모듈 설치 후 BoardPermissionService가 동적 권한 생성.
|
||||
* 모듈 업데이트(createModulePermissions 재호출) 시 동적 권한이 삭제되지 않아야 함.
|
||||
*/
|
||||
public function test_module_update_preserves_dynamic_permissions(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 정적 권한 수 확인
|
||||
$staticPermCount = Permission::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
$this->assertGreaterThan(0, $staticPermCount);
|
||||
|
||||
// 동적 권한 생성 시뮬레이션 (BoardPermissionService::ensureBoardPermissions 동작 재현)
|
||||
$moduleRoot = Permission::where('identifier', 'sirsoft-board')
|
||||
->where('extension_type', ExtensionOwnerType::Module)
|
||||
->first();
|
||||
|
||||
// 모듈 루트 권한이 없으면 생성
|
||||
if (! $moduleRoot) {
|
||||
$moduleRoot = Permission::create([
|
||||
'identifier' => 'sirsoft-board',
|
||||
'name' => ['ko' => '게시판', 'en' => 'Board'],
|
||||
'description' => ['ko' => '게시판 모듈', 'en' => 'Board module'],
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'type' => 'admin',
|
||||
]);
|
||||
}
|
||||
|
||||
// 게시판별 동적 권한 생성
|
||||
$categoryPerm = Permission::create([
|
||||
'identifier' => 'sirsoft-board.notice',
|
||||
'name' => ['ko' => '공지사항 게시판', 'en' => 'Notice Board'],
|
||||
'description' => ['ko' => '공지사항 게시판 권한', 'en' => 'Notice board permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'parent_id' => $moduleRoot->id,
|
||||
'type' => 'admin',
|
||||
]);
|
||||
|
||||
Permission::create([
|
||||
'identifier' => 'sirsoft-board.notice.posts.list',
|
||||
'name' => ['ko' => '글 목록', 'en' => 'List Posts'],
|
||||
'description' => ['ko' => '글 목록 조회', 'en' => 'List posts'],
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'parent_id' => $categoryPerm->id,
|
||||
'type' => 'admin',
|
||||
]);
|
||||
|
||||
$totalPermCountBefore = Permission::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
|
||||
// 모듈 업데이트 시뮬레이션 (createModulePermissions 재호출)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModulePermissions', [$module]);
|
||||
|
||||
// 동적 권한이 보존되어야 함
|
||||
$this->assertTrue(
|
||||
Permission::where('identifier', 'sirsoft-board.notice')
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->exists(),
|
||||
'동적 권한 sirsoft-board.notice가 모듈 업데이트 후에도 보존되어야 합니다'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
Permission::where('identifier', 'sirsoft-board.notice.posts.list')
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->exists(),
|
||||
'동적 권한 sirsoft-board.notice.posts.list가 모듈 업데이트 후에도 보존되어야 합니다'
|
||||
);
|
||||
|
||||
// 전체 권한 수 유지 (정적 권한은 sync로 갱신, 동적 권한은 보존)
|
||||
$totalPermCountAfter = Permission::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->count();
|
||||
$this->assertEquals($totalPermCountBefore, $totalPermCountAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 제거 시 동적 메뉴를 포함한 모든 메뉴가 삭제되는지 테스트
|
||||
*
|
||||
* cleanup 자동 호출을 제거했지만, uninstall 시 deleteByExtension()은 정상 동작해야 함.
|
||||
*/
|
||||
public function test_module_uninstall_removes_all_menus_including_dynamic(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 동적 메뉴 생성 시뮬레이션
|
||||
Menu::create([
|
||||
'name' => ['ko' => '공지사항', 'en' => 'Notice'],
|
||||
'slug' => 'board-notice',
|
||||
'url' => '/admin/board/notice',
|
||||
'icon' => 'fas fa-clipboard-list',
|
||||
'parent_id' => null,
|
||||
'is_active' => true,
|
||||
'extension_type' => ExtensionOwnerType::Module,
|
||||
'extension_identifier' => 'sirsoft-board',
|
||||
'order' => 50,
|
||||
]);
|
||||
|
||||
// 정적 + 동적 메뉴 존재 확인 (정적 4 + 동적 1 = 5)
|
||||
$this->assertEquals(5, Menu::where('extension_identifier', 'sirsoft-board')->count());
|
||||
|
||||
// 모듈 제거
|
||||
$this->moduleManager->uninstallModule('sirsoft-board');
|
||||
|
||||
// 모든 메뉴 (정적 + 동적) 삭제 확인
|
||||
$this->assertFalse(
|
||||
Menu::where('extension_identifier', 'sirsoft-board')->exists(),
|
||||
'모듈 제거 시 동적 메뉴를 포함한 모든 메뉴가 삭제되어야 합니다'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Module;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Extension\ModuleManager;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use ReflectionMethod;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 모듈 역할/권한 동기화 통합 테스트
|
||||
*
|
||||
* ExtensionRoleSyncHelper를 통한 실제 DB 기반 동기화를 검증합니다:
|
||||
* - Permission은 항상 확장 정의값으로 덮어쓰기
|
||||
* - user_overrides 배열로 유저 커스터마이징 보존
|
||||
* - DB 기반 diff로 권한 attach/detach
|
||||
* - stale 권한 정리
|
||||
*/
|
||||
class ModuleRolePermissionSyncTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private ModuleManager $moduleManager;
|
||||
|
||||
/** @var bool 활성 디렉토리가 테스트 전에 이미 존재했는지 (tearDown에서 정리 판단용) */
|
||||
private bool $boardExistedBefore = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// 이전 테스트에서 남은 잔존 디렉토리 정리
|
||||
$activePath = base_path('modules/sirsoft-board');
|
||||
if (File::isDirectory($activePath) && ! File::exists($activePath.'/module.php')) {
|
||||
File::deleteDirectory($activePath);
|
||||
}
|
||||
|
||||
$this->boardExistedBefore = File::isDirectory($activePath);
|
||||
$this->moduleManager = app(ModuleManager::class);
|
||||
$this->moduleManager->loadModules();
|
||||
|
||||
// 기본 시스템 역할 생성
|
||||
Role::create([
|
||||
'identifier' => 'admin',
|
||||
'name' => ['ko' => '시스템 관리자', 'en' => 'System Administrator'],
|
||||
'description' => ['ko' => '모든 권한을 가진 관리자', 'en' => 'Administrator with all permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Role::create([
|
||||
'identifier' => 'manager',
|
||||
'name' => ['ko' => '매니저', 'en' => 'Manager'],
|
||||
'description' => ['ko' => '관리 권한을 가진 매니저', 'en' => 'Manager with management permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 테스트용 사용자 생성
|
||||
User::factory()->create(['email' => 'admin@test.com']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// 테스트에서 생성한 활성 디렉토리만 정리 (기존에 있었으면 건드리지 않음)
|
||||
if (! $this->boardExistedBefore) {
|
||||
$activePath = base_path('modules/sirsoft-board');
|
||||
if (File::isDirectory($activePath)) {
|
||||
File::deleteDirectory($activePath);
|
||||
}
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleManager의 protected 메서드를 호출하는 헬퍼
|
||||
*
|
||||
* @param string $methodName 메서드명
|
||||
* @param array $args 인자
|
||||
*/
|
||||
private function callProtectedMethod(string $methodName, array $args = []): mixed
|
||||
{
|
||||
$method = new ReflectionMethod($this->moduleManager, $methodName);
|
||||
|
||||
return $method->invokeArgs($this->moduleManager, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 설치 시 권한이 생성되고 역할에 할당되는지 테스트
|
||||
*/
|
||||
public function test_module_install_creates_permissions_and_assigns_to_roles(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 권한 생성 확인 (모듈 루트 1 + 카테고리 3 + 리프 8 = 12)
|
||||
$permissions = Permission::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')->get();
|
||||
$this->assertCount(12, $permissions);
|
||||
|
||||
// admin 역할에 할당된 sirsoft-board 권한 확인 (리프 8개)
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$adminPermissions = $adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get();
|
||||
$this->assertCount(8, $adminPermissions);
|
||||
|
||||
// manager 역할에 할당된 sirsoft-board 권한 확인
|
||||
$managerRole = Role::where('identifier', 'manager')->first();
|
||||
$managerPermissions = $managerRole->permissions()->where('extension_identifier', 'sirsoft-board')->get();
|
||||
$this->assertCount(4, $managerPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission은 항상 확장 정의값으로 덮어쓰기되는지 테스트
|
||||
*/
|
||||
public function test_reinstall_always_overwrites_permission(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$readPerm = Permission::where('identifier', 'sirsoft-board.boards.read')->first();
|
||||
$originalName = $readPerm->name;
|
||||
|
||||
// 재설치 (업데이트 시뮬레이션)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModulePermissions', [$module]);
|
||||
|
||||
$readPerm->refresh();
|
||||
|
||||
// 항상 확장 정의값으로 덮어씀
|
||||
$this->assertEquals($originalName, $readPerm->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 최초 설치 시 역할에 user_overrides가 비어있는지 테스트
|
||||
*/
|
||||
public function test_no_user_overrides_on_fresh_install(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$managerRole = Role::where('identifier', 'manager')->first();
|
||||
|
||||
// 최초 설치 시 user_overrides는 비어있거나 null
|
||||
$this->assertTrue(
|
||||
empty($adminRole->user_overrides),
|
||||
'admin 역할의 user_overrides는 비어있어야 합니다'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
empty($managerRole->user_overrides),
|
||||
'manager 역할의 user_overrides는 비어있어야 합니다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides에 개별 권한 식별자가 있으면 해당 권한만 보호되는지 테스트
|
||||
*
|
||||
* 시나리오:
|
||||
* 1. 모듈 설치 → admin에 8개 권한 할당
|
||||
* 2. 유저가 admin에서 boards.delete 해제 + user_overrides에 "sirsoft-board.boards.delete" 기록
|
||||
* 3. 재설치 (업데이트 시뮬레이션)
|
||||
* 4. boards.delete만 보호 (해제 상태 유지), 나머지 권한은 정상 동기화
|
||||
*/
|
||||
public function test_user_permission_override_preserved_on_reinstall(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 초기 상태 확인 (admin에 리프 8개 할당)
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$this->assertCount(8, $adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get());
|
||||
|
||||
// 유저가 admin에서 boards.delete 권한 해제
|
||||
$deletePerm = Permission::where('identifier', 'sirsoft-board.boards.delete')->first();
|
||||
$adminRole->permissions()->detach($deletePerm->id);
|
||||
$this->assertCount(7, $adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get());
|
||||
|
||||
// user_overrides에 개별 권한 식별자 기록 (실제로는 Listener가 수행)
|
||||
$adminRole->update(['user_overrides' => ['sirsoft-board.boards.delete']]);
|
||||
|
||||
// 재설치 (업데이트 시뮬레이션)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('assignPermissionsToRoles', [$module]);
|
||||
|
||||
// sirsoft-board.boards.delete만 보호 → 해제 상태 유지 → 7개 유지
|
||||
$adminRole->refresh();
|
||||
$this->assertCount(
|
||||
7,
|
||||
$adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get(),
|
||||
'user_overrides에 개별 권한 식별자가 있으면 해당 권한만 보호되어야 합니다'
|
||||
);
|
||||
|
||||
// boards.delete는 여전히 해제 상태
|
||||
$this->assertFalse(
|
||||
$adminRole->permissions()->where('identifier', 'sirsoft-board.boards.delete')->exists(),
|
||||
'보호된 권한(boards.delete)은 해제 상태가 유지되어야 합니다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides에 없는 권한은 재설치 시 정상 동기화되는지 테스트
|
||||
*
|
||||
* 시나리오:
|
||||
* 1. 모듈 설치 → admin에 8개 권한 할당
|
||||
* 2. 유저가 boards.delete 해제 + user_overrides에 기록
|
||||
* 3. 유저가 boards.read도 해제 (하지만 user_overrides에 미기록 — 가상 시나리오)
|
||||
* 4. 재설치 → boards.delete는 보호, boards.read는 비보호이므로 다시 attach
|
||||
*/
|
||||
public function test_non_overridden_permissions_synced_normally(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
|
||||
// 유저가 boards.delete 해제 + user_overrides 기록
|
||||
$deletePerm = Permission::where('identifier', 'sirsoft-board.boards.delete')->first();
|
||||
$adminRole->permissions()->detach($deletePerm->id);
|
||||
$adminRole->update(['user_overrides' => ['sirsoft-board.boards.delete']]);
|
||||
|
||||
// boards.read도 해제 (user_overrides 미기록)
|
||||
$readPerm = Permission::where('identifier', 'sirsoft-board.boards.read')->first();
|
||||
$adminRole->permissions()->detach($readPerm->id);
|
||||
|
||||
$this->assertCount(6, $adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get());
|
||||
|
||||
// 재설치
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('assignPermissionsToRoles', [$module]);
|
||||
|
||||
$adminRole->refresh();
|
||||
|
||||
// boards.read는 비보호 → 다시 attach → 7개
|
||||
$this->assertTrue(
|
||||
$adminRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists(),
|
||||
'비보호 권한(boards.read)은 재설치 시 다시 attach되어야 합니다'
|
||||
);
|
||||
|
||||
// boards.delete는 보호 → 해제 상태 유지
|
||||
$this->assertFalse(
|
||||
$adminRole->permissions()->where('identifier', 'sirsoft-board.boards.delete')->exists(),
|
||||
'보호된 권한(boards.delete)은 해제 상태가 유지되어야 합니다'
|
||||
);
|
||||
|
||||
$this->assertCount(
|
||||
7,
|
||||
$adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get(),
|
||||
'비보호 권한만 동기화되고 보호 권한은 유지되어야 합니다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* user_overrides가 없는 역할은 DB 기반 diff로 새 권한만 attach되는지 테스트
|
||||
*
|
||||
* 시나리오:
|
||||
* 1. 모듈 설치 → manager에 4개 권한 할당
|
||||
* 2. 유저가 manager에서 boards.read 해제 (user_overrides 미기록 — Listener 미동작 가정)
|
||||
* 3. 재설치
|
||||
* 4. DB 기반 diff → boards.read가 이미 없으므로 다시 attach됨
|
||||
*/
|
||||
public function test_db_based_diff_reattaches_removed_permission_without_user_overrides(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$managerRole = Role::where('identifier', 'manager')->first();
|
||||
$readPerm = Permission::where('identifier', 'sirsoft-board.boards.read')->first();
|
||||
|
||||
// 유저가 해제 (user_overrides 미기록)
|
||||
$managerRole->permissions()->detach($readPerm->id);
|
||||
$this->assertFalse($managerRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists());
|
||||
|
||||
// 재설치 (업데이트 시뮬레이션)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('assignPermissionsToRoles', [$module]);
|
||||
|
||||
// user_overrides 없음 → DB 기반 diff → boards.read가 다시 attach됨
|
||||
$this->assertTrue(
|
||||
$managerRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists(),
|
||||
'user_overrides가 없으면 DB 기반 diff로 누락된 권한이 다시 attach되어야 합니다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 수동으로 추가한 역할 할당이 보존되는지 테스트
|
||||
*/
|
||||
public function test_user_added_role_assignments_are_preserved(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
$customRole = Role::create([
|
||||
'identifier' => 'custom-editor',
|
||||
'name' => ['ko' => '커스텀 편집자', 'en' => 'Custom Editor'],
|
||||
'description' => ['ko' => '사용자 정의 편집자 역할', 'en' => 'Custom editor role'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$readPerm = Permission::where('identifier', 'sirsoft-board.boards.read')->first();
|
||||
$customRole->permissions()->attach($readPerm->id, [
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
|
||||
// 재설치 (업데이트 시뮬레이션)
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$this->callProtectedMethod('createModulePermissions', [$module]);
|
||||
$this->callProtectedMethod('assignPermissionsToRoles', [$module]);
|
||||
|
||||
// 사용자가 수동 추가한 할당이 보존됨
|
||||
$this->assertTrue(
|
||||
$customRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists(),
|
||||
'사용자가 수동으로 추가한 역할 할당이 보존되어야 합니다'
|
||||
);
|
||||
|
||||
// 기존 admin/manager 할당도 유지됨
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$this->assertTrue($adminRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists());
|
||||
|
||||
$managerRole = Role::where('identifier', 'manager')->first();
|
||||
$this->assertTrue($managerRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Module;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Enums\PermissionType;
|
||||
use App\Extension\ModuleManager;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\Helpers\ProtectsExtensionDirectories;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ModuleRolePermissionTest extends TestCase
|
||||
{
|
||||
use ProtectsExtensionDirectories;
|
||||
use RefreshDatabase;
|
||||
|
||||
private ModuleManager $moduleManager;
|
||||
|
||||
/** @var bool 테스트 전에 활성 디렉토리가 이미 존재했는지 */
|
||||
private bool $boardExistedBefore = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$activePath = base_path('modules/sirsoft-board');
|
||||
$bundledPath = base_path('modules/_bundled/sirsoft-board');
|
||||
|
||||
// 이전 세션에서 남은 좀비 디렉토리 정리 (module.php 없는 잔존 디렉토리)
|
||||
if (File::isDirectory($activePath) && ! File::exists($activePath.'/module.php')) {
|
||||
File::deleteDirectory($activePath);
|
||||
}
|
||||
|
||||
// 활성 디렉토리 존재 여부 판정
|
||||
$this->boardExistedBefore = File::isDirectory($activePath)
|
||||
&& File::exists($activePath.'/module.php');
|
||||
|
||||
// 미설치 시 _bundled에서 복사 (테스트 실행을 위한 준비)
|
||||
if (! $this->boardExistedBefore && File::isDirectory($bundledPath)) {
|
||||
File::copyDirectory($bundledPath, $activePath);
|
||||
}
|
||||
|
||||
// 확장 디렉토리 보호 활성화 (이후 모든 deleteDirectory 호출 차단)
|
||||
$this->setUpExtensionProtection();
|
||||
|
||||
$this->moduleManager = app(ModuleManager::class);
|
||||
$this->moduleManager->loadModules();
|
||||
|
||||
// 기본 admin role 생성
|
||||
Role::create([
|
||||
'identifier' => 'admin',
|
||||
'name' => ['ko' => '시스템 관리자', 'en' => 'System Administrator'],
|
||||
'description' => ['ko' => '모든 권한을 가진 관리자', 'en' => 'Administrator with all permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// manager role 생성 (sirsoft-board 권한에서 참조)
|
||||
Role::create([
|
||||
'identifier' => 'manager',
|
||||
'name' => ['ko' => '매니저', 'en' => 'Manager'],
|
||||
'description' => ['ko' => '관리 권한을 가진 매니저', 'en' => 'Manager with management permissions'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 테스트용 사용자 생성
|
||||
User::factory()->create(['email' => 'admin@test.com']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// 확장 디렉토리 보호 해제 (원래 File 파사드 복원)
|
||||
$this->tearDownExtensionProtection();
|
||||
|
||||
// 테스트에서 생성한 활성 디렉토리만 정리 (기존 설치분은 건드리지 않음)
|
||||
if (! $this->boardExistedBefore) {
|
||||
$activePath = base_path('modules/sirsoft-board');
|
||||
if (File::isDirectory($activePath)) {
|
||||
File::deleteDirectory($activePath);
|
||||
}
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 설치 시 권한이 생성되는지 테스트
|
||||
*
|
||||
* 계층형 구조로 권한이 생성됨:
|
||||
* - 1레벨: 모듈 노드 (sirsoft-board)
|
||||
* - 2레벨: 카테고리 노드 (sirsoft-board.boards, sirsoft-board.reports)
|
||||
* - 3레벨: 개별 권한 (sirsoft-board.boards.read, create, update, delete, sirsoft-board.reports.view, manage)
|
||||
*/
|
||||
public function test_module_installation_creates_permissions(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 권한이 생성되었는지 확인 (계층형 구조: 모듈 1 + 카테고리 2 + 개별 권한 6 = 9개)
|
||||
$permissions = Permission::where('extension_type', ExtensionOwnerType::Module)
|
||||
->where('extension_identifier', 'sirsoft-board')->get();
|
||||
$this->assertCount(9, $permissions);
|
||||
|
||||
$identifiers = $permissions->pluck('identifier')->toArray();
|
||||
// 1레벨: 모듈 노드
|
||||
$this->assertContains('sirsoft-board', $identifiers);
|
||||
// 2레벨: 카테고리 노드
|
||||
$this->assertContains('sirsoft-board.boards', $identifiers);
|
||||
$this->assertContains('sirsoft-board.reports', $identifiers);
|
||||
// 3레벨: 개별 권한 - boards
|
||||
$this->assertContains('sirsoft-board.boards.read', $identifiers);
|
||||
$this->assertContains('sirsoft-board.boards.create', $identifiers);
|
||||
$this->assertContains('sirsoft-board.boards.update', $identifiers);
|
||||
$this->assertContains('sirsoft-board.boards.delete', $identifiers);
|
||||
// 3레벨: 개별 권한 - reports
|
||||
$this->assertContains('sirsoft-board.reports.view', $identifiers);
|
||||
$this->assertContains('sirsoft-board.reports.manage', $identifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 설치 시 권한이 지정된 role에 할당되는지 테스트
|
||||
*/
|
||||
public function test_module_installation_assigns_permissions_to_roles(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// admin role에 모든 개별 권한(6개)이 할당되었는지 확인
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$adminPermissions = $adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->get();
|
||||
$this->assertCount(6, $adminPermissions);
|
||||
|
||||
// manager role에 할당된 권한 확인 (boards: read, create, update + reports: view = 4개)
|
||||
$managerRole = Role::where('identifier', 'manager')->first();
|
||||
$managerPermissions = $managerRole->permissions()->where('extension_identifier', 'sirsoft-board')->get();
|
||||
$this->assertCount(4, $managerPermissions);
|
||||
|
||||
$managerIdentifiers = $managerPermissions->pluck('identifier')->toArray();
|
||||
$this->assertContains('sirsoft-board.boards.read', $managerIdentifiers);
|
||||
$this->assertContains('sirsoft-board.boards.create', $managerIdentifiers);
|
||||
$this->assertContains('sirsoft-board.boards.update', $managerIdentifiers);
|
||||
$this->assertContains('sirsoft-board.reports.view', $managerIdentifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 role에 새 권한 추가 시 기존 권한이 유지되는지 테스트
|
||||
*/
|
||||
public function test_existing_role_permissions_are_preserved(): void
|
||||
{
|
||||
// admin role에 기존 권한 추가
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$existingPermission = Permission::create([
|
||||
'identifier' => 'core.test.permission',
|
||||
'name' => ['ko' => '테스트 권한', 'en' => 'Test Permission'],
|
||||
'description' => ['ko' => '테스트용 권한', 'en' => 'Test permission'],
|
||||
'extension_type' => ExtensionOwnerType::Core,
|
||||
'extension_identifier' => 'core',
|
||||
'type' => PermissionType::Admin,
|
||||
]);
|
||||
$adminRole->permissions()->attach($existingPermission->id);
|
||||
|
||||
// 모듈 설치
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 기존 권한이 유지되는지 확인
|
||||
$adminRole->refresh();
|
||||
$this->assertTrue($adminRole->permissions()->where('identifier', 'core.test.permission')->exists());
|
||||
|
||||
// 새 권한도 추가되었는지 확인
|
||||
$this->assertTrue($adminRole->permissions()->where('identifier', 'sirsoft-board.boards.read')->exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 제거 시 권한이 role에서 분리되는지 테스트
|
||||
*/
|
||||
public function test_module_uninstallation_detaches_permissions_from_roles(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// admin role에 권한이 있는지 확인
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
$this->assertTrue($adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->exists());
|
||||
|
||||
// 모듈 제거
|
||||
$this->moduleManager->uninstallModule('sirsoft-board');
|
||||
|
||||
// 권한이 분리되었는지 확인
|
||||
$adminRole->refresh();
|
||||
$this->assertFalse($adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 제거 시 권한이 삭제되는지 테스트
|
||||
*/
|
||||
public function test_module_uninstallation_deletes_permissions(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 권한이 존재하는지 확인
|
||||
$this->assertTrue(Permission::where('extension_identifier', 'sirsoft-board')->exists());
|
||||
|
||||
// 모듈 제거
|
||||
$this->moduleManager->uninstallModule('sirsoft-board');
|
||||
|
||||
// 권한이 삭제되었는지 확인
|
||||
$this->assertFalse(Permission::where('extension_identifier', 'sirsoft-board')->exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 role은 모듈 제거 시에도 삭제되지 않는지 테스트
|
||||
*/
|
||||
public function test_system_roles_are_not_deleted_on_uninstall(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
$this->moduleManager->uninstallModule('sirsoft-board');
|
||||
|
||||
// 시스템 admin role은 삭제되지 않음
|
||||
$this->assertTrue(Role::where('identifier', 'admin')->exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* getPermissions() 메서드가 roles 필드를 포함하는지 테스트
|
||||
*
|
||||
* 계층형 구조에서 개별 권한(3레벨)에 roles 필드가 있는지 확인
|
||||
*/
|
||||
public function test_get_permissions_includes_roles_field(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
$module = $this->moduleManager->getModule('sirsoft-board');
|
||||
$permissionConfig = $module->getPermissions();
|
||||
|
||||
$this->assertIsArray($permissionConfig);
|
||||
$this->assertArrayHasKey('categories', $permissionConfig);
|
||||
$this->assertNotEmpty($permissionConfig['categories']);
|
||||
|
||||
// 계층형 구조에서 개별 권한 검증
|
||||
foreach ($permissionConfig['categories'] as $category) {
|
||||
$this->assertArrayHasKey('identifier', $category);
|
||||
$this->assertArrayHasKey('name', $category);
|
||||
$this->assertArrayHasKey('description', $category);
|
||||
$this->assertArrayHasKey('permissions', $category);
|
||||
|
||||
foreach ($category['permissions'] as $permission) {
|
||||
$this->assertArrayHasKey('action', $permission);
|
||||
$this->assertArrayHasKey('name', $permission);
|
||||
$this->assertArrayHasKey('description', $permission);
|
||||
$this->assertArrayHasKey('roles', $permission);
|
||||
$this->assertIsArray($permission['roles']);
|
||||
}
|
||||
}
|
||||
|
||||
// boards 카테고리의 권한 확인
|
||||
$boardsCategory = collect($permissionConfig['categories'])->firstWhere('identifier', 'boards');
|
||||
$this->assertNotNull($boardsCategory);
|
||||
|
||||
// read 권한은 admin과 manager 둘 다에게 할당
|
||||
$readPermission = collect($boardsCategory['permissions'])->firstWhere('action', 'read');
|
||||
$this->assertContains('admin', $readPermission['roles']);
|
||||
$this->assertContains('manager', $readPermission['roles']);
|
||||
|
||||
// delete 권한은 admin에게만 할당
|
||||
$deletePermission = collect($boardsCategory['permissions'])->firstWhere('action', 'delete');
|
||||
$this->assertContains('admin', $deletePermission['roles']);
|
||||
$this->assertNotContains('manager', $deletePermission['roles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* roles 필드가 없는 권한은 role에 할당되지 않는지 테스트
|
||||
*
|
||||
* 계층형 구조에서 개별 권한(3레벨)만 role에 할당됨.
|
||||
* 모듈 노드(1레벨)와 카테고리 노드(2레벨)는 할당되지 않음.
|
||||
*/
|
||||
public function test_permissions_without_roles_field_are_not_assigned(): void
|
||||
{
|
||||
$this->moduleManager->installModule('sirsoft-board');
|
||||
|
||||
// 계층형 구조에서 개별 권한(3레벨)만 role에 할당됨
|
||||
// 모듈의 getPermissions()에서 정의된 개별 권한 6개만 할당되어야 함
|
||||
$adminRole = Role::where('identifier', 'admin')->first();
|
||||
|
||||
// admin role에 할당된 모듈 권한 개수 확인 (개별 권한 6개)
|
||||
$assignedPermissions = $adminRole->permissions()->where('extension_identifier', 'sirsoft-board')->count();
|
||||
$this->assertEquals(6, $assignedPermissions);
|
||||
|
||||
// 전체 모듈 권한 개수 확인 (모듈 1 + 카테고리 2 + 개별 6 = 9개)
|
||||
$totalModulePermissions = Permission::where('extension_identifier', 'sirsoft-board')->count();
|
||||
$this->assertEquals(9, $totalModulePermissions);
|
||||
|
||||
// 개별 권한만 role에 할당되었는지 확인
|
||||
$assignedIdentifiers = $adminRole->permissions()
|
||||
->where('extension_identifier', 'sirsoft-board')
|
||||
->pluck('identifier')
|
||||
->toArray();
|
||||
|
||||
$this->assertContains('sirsoft-board.boards.read', $assignedIdentifiers);
|
||||
$this->assertContains('sirsoft-board.boards.create', $assignedIdentifiers);
|
||||
$this->assertContains('sirsoft-board.boards.update', $assignedIdentifiers);
|
||||
$this->assertContains('sirsoft-board.boards.delete', $assignedIdentifiers);
|
||||
$this->assertContains('sirsoft-board.reports.view', $assignedIdentifiers);
|
||||
$this->assertContains('sirsoft-board.reports.manage', $assignedIdentifiers);
|
||||
|
||||
// 모듈 노드와 카테고리 노드는 할당되지 않음
|
||||
$this->assertNotContains('sirsoft-board', $assignedIdentifiers);
|
||||
$this->assertNotContains('sirsoft-board.boards', $assignedIdentifiers);
|
||||
$this->assertNotContains('sirsoft-board.reports', $assignedIdentifiers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Module;
|
||||
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Http\Requests\Module\UpdateModuleRequest;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* UpdateModuleRequest 검증 테스트
|
||||
*
|
||||
* TranslatableField 규칙과 역호환성 처리를 테스트합니다.
|
||||
*/
|
||||
class UpdateModuleRequestTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// 한국어 로케일 설정
|
||||
App::setLocale('ko');
|
||||
}
|
||||
|
||||
/**
|
||||
* 다국어 배열로 업데이트 요청 시 성공
|
||||
*/
|
||||
public function test_passes_with_translatable_array(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => [
|
||||
'ko' => '테스트 모듈',
|
||||
'en' => 'Test Module',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '테스트 모듈 설명',
|
||||
'en' => 'Test module description',
|
||||
],
|
||||
'status' => ExtensionStatus::Active->value,
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열로 업데이트 요청 시 배열로 변환되어 통과
|
||||
*/
|
||||
public function test_converts_string_to_array(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => '테스트 모듈',
|
||||
'description' => '테스트 설명',
|
||||
];
|
||||
|
||||
// UpdateModuleRequest 인스턴스 생성 및 데이터 주입
|
||||
$request = UpdateModuleRequest::create('/test', 'POST', $data);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
|
||||
// Act - prepareForValidation이 실행된 후 데이터 확인
|
||||
$validatedData = $request->validated();
|
||||
|
||||
// Assert
|
||||
$this->assertIsArray($validatedData['name']);
|
||||
$this->assertIsArray($validatedData['description']);
|
||||
$this->assertEquals('테스트 모듈', $validatedData['name']['ko']);
|
||||
$this->assertEquals('테스트 모듈', $validatedData['name']['en']);
|
||||
}
|
||||
|
||||
/**
|
||||
* name이 maxLength를 초과하면 실패
|
||||
*/
|
||||
public function test_fails_when_name_exceeds_max_length(): void
|
||||
{
|
||||
// Arrange
|
||||
$longString = str_repeat('가', 256); // 255자 초과
|
||||
$data = [
|
||||
'name' => [
|
||||
'ko' => $longString,
|
||||
'en' => 'Test',
|
||||
],
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($validator->fails());
|
||||
$this->assertArrayHasKey('name', $validator->errors()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* description이 maxLength를 초과하면 실패
|
||||
*/
|
||||
public function test_fails_when_description_exceeds_max_length(): void
|
||||
{
|
||||
// Arrange
|
||||
$longString = str_repeat('가', 1001); // 1000자 초과
|
||||
$data = [
|
||||
'description' => [
|
||||
'ko' => $longString,
|
||||
'en' => 'Test',
|
||||
],
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($validator->fails());
|
||||
$this->assertArrayHasKey('description', $validator->errors()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* status가 허용되지 않은 값이면 실패
|
||||
*/
|
||||
public function test_fails_with_invalid_status(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'status' => 'invalid_status',
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($validator->fails());
|
||||
$this->assertArrayHasKey('status', $validator->errors()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* status가 ExtensionStatus enum 값이면 성공
|
||||
*/
|
||||
public function test_passes_with_valid_status_values(): void
|
||||
{
|
||||
// Arrange
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act & Assert - 모든 유효한 status 값 테스트
|
||||
foreach (ExtensionStatus::values() as $statusValue) {
|
||||
$data = ['status' => $statusValue];
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
$this->assertFalse($validator->fails(), "Failed with status: {$statusValue}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 필드가 nullable이므로 빈 데이터도 성공
|
||||
*/
|
||||
public function test_passes_with_empty_data(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* name만 포함된 업데이트 요청 성공
|
||||
*/
|
||||
public function test_passes_with_only_name(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => [
|
||||
'ko' => '새 이름',
|
||||
'en' => 'New Name',
|
||||
],
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* description만 포함된 업데이트 요청 성공
|
||||
*/
|
||||
public function test_passes_with_only_description(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'description' => [
|
||||
'ko' => '새 설명',
|
||||
'en' => 'New Description',
|
||||
],
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* status만 포함된 업데이트 요청 성공
|
||||
*/
|
||||
public function test_passes_with_only_status(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'status' => ExtensionStatus::Inactive->value,
|
||||
];
|
||||
$request = new UpdateModuleRequest;
|
||||
|
||||
// Act
|
||||
$validator = Validator::make($data, $request->rules());
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user