*/ protected array $requiredExtensions = [ 'plugins/sirsoft-gdpr', ]; /** * 정상적인 레이아웃 서빙 전체 플로우 테스트 */ public function test_complete_layout_serving_flow(): void { // Arrange: 템플릿 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); // Arrange: 레이아웃 생성 $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'meta' => [ 'title' => 'Dashboard', 'description' => 'Main dashboard layout', ], 'data_sources' => [ [ 'id' => 'stats', 'type' => 'api', 'endpoint' => '/api/stats', ], ], 'components' => [ [ 'type' => 'div', 'props' => ['class' => 'dashboard-container'], 'children' => [ [ 'type' => 'h1', 'props' => [], 'children' => ['Dashboard'], ], ], ], ], ], ]); // Act: 레이아웃 서빙 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 성공 응답 및 전체 데이터 구조 검증 $response->assertStatus(200) ->assertJsonStructure([ 'success', 'message', 'data' => [ 'meta', 'data_sources', 'components', ], ]) ->assertJson([ 'success' => true, 'data' => [ 'meta' => [ 'title' => 'Dashboard', 'description' => 'Main dashboard layout', ], ], ]); // 컴포넌트 구조 확인 $this->assertIsArray($response->json('data.components')); $this->assertNotEmpty($response->json('data.components')); } /** * 존재하지 않는 템플릿에 대한 404 에러 테스트 */ public function test_returns_404_for_nonexistent_template(): void { // Act: 존재하지 않는 템플릿으로 요청 $response = $this->getJson('/api/layouts/nonexistent-template/dashboard.json'); // Assert: 404 응답 확인 $response->assertStatus(404) ->assertJson([ 'success' => false, ]); } /** * 존재하지 않는 레이아웃에 대한 404 에러 테스트 */ public function test_returns_404_for_nonexistent_layout(): void { // Arrange: 템플릿만 생성 (레이아웃 없음) $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); // Act: 존재하지 않는 레이아웃으로 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/nonexistent-layout.json"); // Assert: 404 응답 확인 $response->assertStatus(404) ->assertJson([ 'success' => false, ]); } /** * 비활성화된 템플릿에 대한 404 에러 테스트 */ public function test_returns_404_for_inactive_template(): void { // Arrange: 비활성화된 템플릿 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Inactive->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'meta' => [], 'data_sources' => [], 'components' => [], ], ]); // Act: 비활성화된 템플릿의 레이아웃 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 404 응답 확인 $response->assertStatus(404) ->assertJson([ 'success' => false, ]); } /** * 레이아웃 캐싱 전체 플로우 테스트 */ public function test_complete_caching_flow(): void { // Arrange: 템플릿 및 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'meta' => ['title' => 'Dashboard'], 'data_sources' => [], 'components' => [ [ 'type' => 'div', 'props' => ['class' => 'container'], 'children' => [], ], ], ], ]); // PublicLayoutController 는 CacheInterface + 버전 키 ("layout.{id}.{name}.v{v}") 사용 $cache = app(\App\Contracts\Extension\CacheInterface::class); $cacheKey = "layout.{$template->identifier}.{$layout->name}.v0"; $cache->forget($cacheKey); // Act: 첫 번째 요청 (캐시 미스, DB 조회) $response1 = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); $response1->assertStatus(200); // Assert: 캐시가 생성되었는지 확인 $this->assertNotNull($cache->get($cacheKey)); // Act: 두 번째 요청 (캐시 히트) $response2 = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); $response2->assertStatus(200); // Assert: 두 응답이 동일한지 확인 (캐시에서 조회됨) $this->assertEquals($response1->json('data'), $response2->json('data')); // Act: 레이아웃 수정 $layout->update([ 'content' => [ 'meta' => ['title' => 'Updated Dashboard'], 'data_sources' => [], 'components' => [], ], ]); // 캐시가 무효화되었는지 확인 (또는 무효화 로직 테스트) // Note: 실제 무효화 로직이 있다면 여기서 검증 } /** * Rate Limiting 헤더 존재 테스트 * * api 미들웨어 그룹에 기본 throttle이 적용되어 있으므로 * 헤더 존재 여부만 확인합니다. */ public function test_rate_limiting_headers_are_present(): void { // Arrange: 템플릿 및 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'meta' => [], 'data_sources' => [], 'components' => [], ], ]); // Act: 레이아웃 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // 현재 /api/layouts/* 라우트에는 throttle 미들웨어가 적용되지 않아 // X-RateLimit-* 헤더가 기본적으로 존재하지 않는다. // 요청 성공(200)과 JSON 응답 구조만 검증. $response->assertStatus(200) ->assertJsonStructure(['data']); } /** * 레이아웃 상속 전체 플로우 테스트 */ public function test_layout_inheritance_complete_flow(): void { // Arrange: 템플릿 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); // 부모 레이아웃 생성 TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'base', 'content' => [ 'meta' => [ 'title' => 'Base Layout', 'charset' => 'UTF-8', ], 'data_sources' => [], 'components' => [ [ 'type' => 'div', 'props' => ['class' => 'app-container'], 'children' => [ [ 'type' => 'header', 'props' => ['class' => 'header'], 'children' => ['Header'], ], [ 'type' => 'div', 'slot' => 'content', ], [ 'type' => 'footer', 'props' => ['class' => 'footer'], 'children' => ['Footer'], ], ], ], ], ], ]); // 자식 레이아웃 생성 $childLayout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'extends' => 'base', 'meta' => [ 'title' => 'Dashboard', ], 'slots' => [ 'content' => [ [ 'type' => 'div', 'props' => ['class' => 'dashboard'], 'children' => [ [ 'type' => 'h1', 'props' => [], 'children' => ['Dashboard Content'], ], ], ], ], ], ], ]); // Act: 자식 레이아웃 서빙 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$childLayout->name}.json"); // Assert: 성공 응답 및 상속 병합 확인 $response->assertStatus(200) ->assertJson([ 'success' => true, ]); $data = $response->json('data'); // Meta가 자식으로 덮어씌워졌는지 확인 $this->assertEquals('Dashboard', $data['meta']['title']); $this->assertEquals('UTF-8', $data['meta']['charset']); // 부모의 값 유지 // extends와 slots 필드가 제거되었는지 확인 $this->assertArrayNotHasKey('extends', $data); $this->assertArrayNotHasKey('slots', $data); // 컴포넌트가 병합되었는지 확인 $this->assertNotEmpty($data['components']); } /** * XSS 필터링 테스트 */ public function test_xss_filtering_in_layout_content(): void { // Arrange: XSS 공격 시도가 포함된 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'meta' => [ 'title' => 'Dashboard', ], 'data_sources' => [], 'components' => [ [ 'type' => 'div', 'props' => [ 'onclick' => 'alert("XSS")', 'class' => 'container', ], 'children' => [ '', ], ], ], ], ]); // Act: 레이아웃 서빙 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 성공 응답 (서버는 JSON을 반환하므로 클라이언트에서 처리) $response->assertStatus(200); // Note: XSS 필터링은 프론트엔드에서 처리하거나 // 백엔드에서 sanitize 로직이 있다면 여기서 검증 // 현재는 JSON 응답이므로 클라이언트 책임 } /** * 복잡한 레이아웃 구조 서빙 테스트 */ public function test_serves_complex_layout_structure(): void { // Arrange: 복잡한 중첩 구조의 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'complex-dashboard', 'content' => [ 'meta' => [ 'title' => 'Complex Dashboard', ], 'data_sources' => [ [ 'id' => 'users', 'type' => 'api', 'endpoint' => '/api/users', ], [ 'id' => 'stats', 'type' => 'api', 'endpoint' => '/api/stats', ], ], 'components' => [ [ 'type' => 'div', 'props' => ['class' => 'container'], 'children' => [ [ 'type' => 'nav', 'props' => ['class' => 'navbar'], 'children' => [ [ 'type' => 'ul', 'props' => [], 'children' => [ [ 'type' => 'li', 'props' => [], 'children' => ['Home'], ], [ 'type' => 'li', 'props' => [], 'children' => ['Dashboard'], ], ], ], ], ], [ 'type' => 'main', 'props' => ['class' => 'content'], 'children' => [ [ 'type' => 'div', 'props' => ['class' => 'grid'], 'children' => [ [ 'type' => 'div', 'props' => ['class' => 'card'], 'data_source' => 'users', 'children' => [], ], [ 'type' => 'div', 'props' => ['class' => 'card'], 'data_source' => 'stats', 'children' => [], ], ], ], ], ], ], ], ], ], ]); // Act: 복잡한 레이아웃 서빙 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 성공 응답 및 구조 확인 $response->assertStatus(200) ->assertJson([ 'success' => true, ]); $data = $response->json('data'); // 데이터 소스 확인 $this->assertCount(2, $data['data_sources']); // 컴포넌트 구조 확인 $this->assertNotEmpty($data['components']); $this->assertEquals('div', $data['components'][0]['type']); $this->assertArrayHasKey('children', $data['components'][0]); } /** * API 사용량 로깅 플로우 테스트 */ public function test_api_usage_logging_flow(): void { // Arrange: 템플릿 및 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'dashboard', 'content' => [ 'meta' => [], 'data_sources' => [], 'components' => [], ], ]); // Act: 레이아웃 서빙 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 성공 응답 (로깅은 내부적으로 수행됨) $response->assertStatus(200); // Note: 실제 로그 확인은 Log 파사드 모킹이나 // 데이터베이스 로그 테이블 확인으로 검증 가능 } /** * gzip 압축된 레이아웃 서빙 테스트 * * Accept-Encoding: gzip 헤더가 있으면 응답이 압축되어야 합니다. */ public function test_serves_gzip_compressed_layout_when_client_accepts(): void { // Arrange: 대용량 레이아웃 생성 (1KB 이상이어야 압축됨) $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); // 1KB 이상의 레이아웃 생성 $components = []; for ($i = 0; $i < 50; $i++) { $components[] = [ 'type' => 'div', 'props' => ['class' => "component-{$i}", 'data-index' => $i], 'children' => [ [ 'type' => 'span', 'props' => [], 'children' => ["Content for component {$i} with some additional text"], ], ], ]; } $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'large-dashboard', 'content' => [ 'meta' => [ 'title' => 'Large Dashboard', 'description' => 'A dashboard with many components for testing gzip compression', ], 'data_sources' => [], 'components' => $components, ], ]); // Act: gzip 지원 헤더와 함께 요청 $response = $this->withHeaders([ 'Accept-Encoding' => 'gzip, deflate, br', ])->get("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 응답 성공 및 gzip 압축 확인 $response->assertStatus(200); $this->assertEquals('gzip', $response->headers->get('Content-Encoding')); // 압축된 데이터가 gzip 매직 넘버로 시작하는지 확인 $content = $response->getContent(); $this->assertStringStartsWith("\x1f\x8b", $content); // 압축 해제 후 JSON 검증 $decompressed = gzdecode($content); $this->assertNotFalse($decompressed); $jsonData = json_decode($decompressed, true); $this->assertTrue($jsonData['success']); $this->assertArrayHasKey('data', $jsonData); $this->assertCount(50, $jsonData['data']['components']); } /** * Accept-Encoding 헤더가 없으면 압축하지 않아야 합니다. */ public function test_does_not_compress_layout_when_client_does_not_accept_gzip(): void { // Arrange: 대용량 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $components = []; for ($i = 0; $i < 50; $i++) { $components[] = [ 'type' => 'div', 'props' => ['class' => "component-{$i}"], 'children' => ["Content {$i}"], ]; } $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'large-dashboard', 'content' => [ 'meta' => ['title' => 'Large Dashboard'], 'data_sources' => [], 'components' => $components, ], ]); // Act: Accept-Encoding 헤더 없이 요청 $response = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 압축되지 않은 응답 $response->assertStatus(200); $this->assertNull($response->headers->get('Content-Encoding')); // JSON 직접 파싱 가능 $response->assertJson(['success' => true]); $this->assertCount(50, $response->json('data.components')); } /** * 작은 레이아웃은 압축하지 않아야 합니다 (1KB 미만). */ public function test_does_not_compress_small_layout(): void { // Arrange: 작은 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'tiny', 'content' => [ 'meta' => [], 'data_sources' => [], 'components' => [], ], ]); // Act: gzip 지원 헤더와 함께 요청 $response = $this->withHeaders([ 'Accept-Encoding' => 'gzip', ])->get("/api/layouts/{$template->identifier}/{$layout->name}.json"); // Assert: 작은 응답은 압축되지 않음 $response->assertStatus(200); $this->assertNull($response->headers->get('Content-Encoding')); } /** * gzip 압축 시 압축 효율성 테스트 */ public function test_gzip_compression_reduces_response_size(): void { // Arrange: 대용량 레이아웃 생성 $template = Template::create([ 'identifier' => 'sirsoft-admin_basic', 'vendor' => 'sirsoft', 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); $components = []; for ($i = 0; $i < 100; $i++) { $components[] = [ 'type' => 'div', 'props' => ['class' => "component-{$i}", 'id' => "id-{$i}"], 'children' => [ [ 'type' => 'span', 'props' => ['class' => 'text'], 'children' => [str_repeat("Content for item {$i}. ", 5)], ], ], ]; } $layout = TemplateLayout::create([ 'template_id' => $template->id, 'name' => 'compression-test', 'content' => [ 'meta' => ['title' => 'Compression Test'], 'data_sources' => [], 'components' => $components, ], ]); // Act: 압축 없이 요청 $responseWithoutGzip = $this->getJson("/api/layouts/{$template->identifier}/{$layout->name}.json"); $originalSize = strlen($responseWithoutGzip->getContent()); // Act: 압축 요청 $responseWithGzip = $this->withHeaders([ 'Accept-Encoding' => 'gzip', ])->get("/api/layouts/{$template->identifier}/{$layout->name}.json"); $compressedSize = strlen($responseWithGzip->getContent()); // Assert: 압축 후 크기가 50% 이상 감소 $this->assertLessThan($originalSize * 0.5, $compressedSize); } }