permissionService = app(PermissionService::class); } // ======================================================================== // getPermissionTree() 타입별 그룹화 테스트 // ======================================================================== /** * getPermissionTree()가 올바른 구조를 반환하는지 확인 */ public function test_get_permission_tree_returns_correct_structure(): void { $result = $this->permissionService->getPermissionTree(); // 필수 키 확인 $this->assertArrayHasKey('permissions', $result); $this->assertArrayHasKey('types', $result); $this->assertArrayHasKey('default_type', $result); } /** * types 배열에 admin과 user가 포함되어 있는지 확인 */ public function test_get_permission_tree_includes_all_permission_types(): void { $result = $this->permissionService->getPermissionTree(); $this->assertContains('admin', $result['types']); $this->assertContains('user', $result['types']); } /** * default_type이 admin인지 확인 */ public function test_get_permission_tree_default_type_is_admin(): void { $result = $this->permissionService->getPermissionTree(); $this->assertEquals('admin', $result['default_type']); } /** * permissions 배열이 타입별로 그룹화되어 있는지 확인 */ public function test_get_permission_tree_groups_by_type(): void { $result = $this->permissionService->getPermissionTree(); // 각 타입별 그룹 존재 확인 $this->assertArrayHasKey('admin', $result['permissions']); $this->assertArrayHasKey('user', $result['permissions']); // 각 그룹에 label과 permissions 키 존재 확인 $this->assertArrayHasKey('label', $result['permissions']['admin']); $this->assertArrayHasKey('permissions', $result['permissions']['admin']); $this->assertArrayHasKey('label', $result['permissions']['user']); $this->assertArrayHasKey('permissions', $result['permissions']['user']); } /** * 각 타입 그룹에 icon 필드가 포함되어 있는지 확인 */ public function test_get_permission_tree_includes_icon_for_each_type(): void { $result = $this->permissionService->getPermissionTree(); $this->assertArrayHasKey('icon', $result['permissions']['admin']); $this->assertArrayHasKey('icon', $result['permissions']['user']); $this->assertEquals('cog', $result['permissions']['admin']['icon']); $this->assertEquals('user', $result['permissions']['user']['icon']); } /** * admin 타입 권한이 admin 그룹에 포함되는지 확인 */ public function test_admin_permissions_are_grouped_under_admin(): void { // admin 타입 루트 권한 생성 Permission::create([ 'identifier' => 'test-module', 'name' => ['ko' => '테스트 모듈', 'en' => 'Test Module'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); $result = $this->permissionService->getPermissionTree(); // admin 그룹에 권한이 있는지 확인 $adminPermissions = $result['permissions']['admin']['permissions']; $identifiers = array_column($adminPermissions, 'identifier'); $this->assertContains('test-module', $identifiers); } /** * user 타입 권한이 user 그룹에 포함되는지 확인 */ public function test_user_permissions_are_grouped_under_user(): void { // user 타입 루트 권한 생성 Permission::create([ 'identifier' => 'user-module', 'name' => ['ko' => '사용자 모듈', 'en' => 'User Module'], 'type' => PermissionType::User, 'parent_id' => null, ]); $result = $this->permissionService->getPermissionTree(); // user 그룹에 권한이 있는지 확인 $userPermissions = $result['permissions']['user']['permissions']; $identifiers = array_column($userPermissions, 'identifier'); $this->assertContains('user-module', $identifiers); } /** * 권한 노드에 필수 필드가 포함되어 있는지 확인 */ public function test_permission_node_has_required_fields(): void { // 권한 생성 Permission::create([ 'identifier' => 'test-node', 'name' => ['ko' => '테스트 노드', 'en' => 'Test Node'], 'description' => ['ko' => '테스트 설명', 'en' => 'Test Description'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); $result = $this->permissionService->getPermissionTree(); $adminPermissions = $result['permissions']['admin']['permissions']; // test-node 찾기 $testNode = collect($adminPermissions)->firstWhere('identifier', 'test-node'); $this->assertNotNull($testNode); $this->assertArrayHasKey('id', $testNode); $this->assertArrayHasKey('identifier', $testNode); $this->assertArrayHasKey('name', $testNode); $this->assertArrayHasKey('name_raw', $testNode); $this->assertArrayHasKey('description', $testNode); $this->assertArrayHasKey('description_raw', $testNode); $this->assertArrayHasKey('extension_type', $testNode); $this->assertArrayHasKey('type', $testNode); $this->assertArrayHasKey('is_assignable', $testNode); $this->assertArrayHasKey('children', $testNode); $this->assertArrayHasKey('leaf_count', $testNode); } /** * 계층 구조가 올바르게 포함되는지 확인 */ public function test_permission_tree_includes_children(): void { // 부모 권한 생성 $parent = Permission::create([ 'identifier' => 'parent-module', 'name' => ['ko' => '부모 모듈', 'en' => 'Parent Module'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); // 자식 권한 생성 Permission::create([ 'identifier' => 'parent-module.child', 'name' => ['ko' => '자식 권한', 'en' => 'Child Permission'], 'type' => PermissionType::Admin, 'parent_id' => $parent->id, ]); $result = $this->permissionService->getPermissionTree(); $adminPermissions = $result['permissions']['admin']['permissions']; // 부모 노드 찾기 $parentNode = collect($adminPermissions)->firstWhere('identifier', 'parent-module'); $this->assertNotNull($parentNode); $this->assertNotEmpty($parentNode['children']); $this->assertEquals('parent-module.child', $parentNode['children'][0]['identifier']); } /** * 리프 노드 개수가 올바르게 계산되는지 확인 */ public function test_leaf_count_is_calculated_correctly(): void { // 부모 권한 생성 $parent = Permission::create([ 'identifier' => 'count-test', 'name' => ['ko' => '카운트 테스트', 'en' => 'Count Test'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); // 자식 권한 3개 생성 (리프 노드) for ($i = 1; $i <= 3; $i++) { Permission::create([ 'identifier' => "count-test.child{$i}", 'name' => ['ko' => "자식 {$i}", 'en' => "Child {$i}"], 'type' => PermissionType::Admin, 'parent_id' => $parent->id, ]); } $result = $this->permissionService->getPermissionTree(); $adminPermissions = $result['permissions']['admin']['permissions']; // 부모 노드 찾기 $parentNode = collect($adminPermissions)->firstWhere('identifier', 'count-test'); $this->assertEquals(3, $parentNode['leaf_count']); } /** * 코어 권한이 항상 먼저 정렬되는지 확인 */ public function test_core_permissions_are_sorted_first(): void { // 비코어 모듈 권한 먼저 생성 Permission::create([ 'identifier' => 'zzz-module', 'name' => ['ko' => 'ZZZ 모듈', 'en' => 'ZZZ Module'], 'type' => PermissionType::Admin, 'parent_id' => null, 'order' => 1, ]); // 코어 모듈 권한 생성 Permission::create([ 'identifier' => 'core', 'name' => ['ko' => '코어', 'en' => 'Core'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, 'parent_id' => null, 'order' => 1, ]); $result = $this->permissionService->getPermissionTree(); $adminPermissions = $result['permissions']['admin']['permissions']; // 첫 번째 권한이 코어인지 확인 $this->assertEquals('core', $adminPermissions[0]['identifier']); } /** * 빈 권한 목록에서도 올바른 구조를 반환하는지 확인 */ public function test_get_permission_tree_with_no_permissions(): void { // 권한 없이 호출 $result = $this->permissionService->getPermissionTree(); $this->assertArrayHasKey('permissions', $result); $this->assertArrayHasKey('types', $result); $this->assertArrayHasKey('default_type', $result); // 빈 permissions 배열 확인 $this->assertEmpty($result['permissions']['admin']['permissions']); $this->assertEmpty($result['permissions']['user']['permissions']); } /** * is_assignable이 자식이 없는 노드에서 true인지 확인 */ public function test_is_assignable_is_true_for_leaf_nodes(): void { // 리프 노드 (자식 없음) Permission::create([ 'identifier' => 'leaf-permission', 'name' => ['ko' => '리프 권한', 'en' => 'Leaf Permission'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); $result = $this->permissionService->getPermissionTree(); $adminPermissions = $result['permissions']['admin']['permissions']; $leafNode = collect($adminPermissions)->firstWhere('identifier', 'leaf-permission'); $this->assertTrue($leafNode['is_assignable']); } /** * 부모가 admin 타입이고 자식이 user 타입일 때 user 그룹에 올바르게 표시되는지 확인 */ public function test_mixed_type_tree_user_children_appear_under_user_group(): void { // admin 타입 루트 생성 $root = Permission::create([ 'identifier' => 'test-module', 'name' => ['ko' => '테스트 모듈', 'en' => 'Test Module'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); // admin 타입 중간 노드 생성 $middle = Permission::create([ 'identifier' => 'test-module.board', 'name' => ['ko' => '게시판', 'en' => 'Board'], 'type' => PermissionType::Admin, 'parent_id' => $root->id, ]); // user 타입 리프 노드 생성 Permission::create([ 'identifier' => 'test-module.board.read', 'name' => ['ko' => '읽기', 'en' => 'Read'], 'type' => PermissionType::User, 'parent_id' => $middle->id, ]); Permission::create([ 'identifier' => 'test-module.board.write', 'name' => ['ko' => '쓰기', 'en' => 'Write'], 'type' => PermissionType::User, 'parent_id' => $middle->id, ]); $result = $this->permissionService->getPermissionTree(); // user 그룹에 부모 구조가 포함되어야 함 $userPermissions = $result['permissions']['user']['permissions']; $this->assertNotEmpty($userPermissions, 'user 그룹에 권한이 있어야 합니다'); // 루트 노드가 user 그룹에 포함 $rootNode = collect($userPermissions)->firstWhere('identifier', 'test-module'); $this->assertNotNull($rootNode, 'user 그룹에 루트 노드가 있어야 합니다'); // 중간 노드 확인 $this->assertNotEmpty($rootNode['children']); $middleNode = collect($rootNode['children'])->firstWhere('identifier', 'test-module.board'); $this->assertNotNull($middleNode); // user 타입 리프 노드 확인 $leafIdentifiers = array_column($middleNode['children'], 'identifier'); $this->assertContains('test-module.board.read', $leafIdentifiers); $this->assertContains('test-module.board.write', $leafIdentifiers); // leaf_count가 user 타입 리프만 카운트 $this->assertEquals(2, $rootNode['leaf_count']); } /** * 혼합 타입 트리에서 admin 그룹에 user 타입 리프가 포함되지 않는지 확인 */ public function test_mixed_type_tree_admin_group_excludes_user_leaves(): void { // admin 타입 루트 생성 $root = Permission::create([ 'identifier' => 'mixed-module', 'name' => ['ko' => '혼합 모듈', 'en' => 'Mixed Module'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); // admin 타입 리프 생성 Permission::create([ 'identifier' => 'mixed-module.admin-leaf', 'name' => ['ko' => '관리자 리프', 'en' => 'Admin Leaf'], 'type' => PermissionType::Admin, 'parent_id' => $root->id, ]); // user 타입 리프 생성 Permission::create([ 'identifier' => 'mixed-module.user-leaf', 'name' => ['ko' => '사용자 리프', 'en' => 'User Leaf'], 'type' => PermissionType::User, 'parent_id' => $root->id, ]); $result = $this->permissionService->getPermissionTree(); // admin 그룹: admin 리프만 포함 $adminPermissions = $result['permissions']['admin']['permissions']; $adminRoot = collect($adminPermissions)->firstWhere('identifier', 'mixed-module'); $this->assertNotNull($adminRoot); $adminLeafIds = array_column($adminRoot['children'], 'identifier'); $this->assertContains('mixed-module.admin-leaf', $adminLeafIds); $this->assertNotContains('mixed-module.user-leaf', $adminLeafIds); $this->assertEquals(1, $adminRoot['leaf_count']); // user 그룹: user 리프만 포함 $userPermissions = $result['permissions']['user']['permissions']; $userRoot = collect($userPermissions)->firstWhere('identifier', 'mixed-module'); $this->assertNotNull($userRoot); $userLeafIds = array_column($userRoot['children'], 'identifier'); $this->assertContains('mixed-module.user-leaf', $userLeafIds); $this->assertNotContains('mixed-module.admin-leaf', $userLeafIds); $this->assertEquals(1, $userRoot['leaf_count']); } /** * 특정 타입의 리프가 없는 브랜치는 해당 타입 그룹에서 제외되는지 확인 */ public function test_branch_without_matching_type_is_excluded(): void { // admin 전용 브랜치 $adminRoot = Permission::create([ 'identifier' => 'admin-only', 'name' => ['ko' => '관리자 전용', 'en' => 'Admin Only'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); Permission::create([ 'identifier' => 'admin-only.manage', 'name' => ['ko' => '관리', 'en' => 'Manage'], 'type' => PermissionType::Admin, 'parent_id' => $adminRoot->id, ]); $result = $this->permissionService->getPermissionTree(); // user 그룹에 이 브랜치가 없어야 함 $userPermissions = $result['permissions']['user']['permissions']; $userRoot = collect($userPermissions)->firstWhere('identifier', 'admin-only'); $this->assertNull($userRoot, 'admin 전용 브랜치는 user 그룹에 없어야 합니다'); // admin 그룹에는 존재 $adminPermissions = $result['permissions']['admin']['permissions']; $adminRootNode = collect($adminPermissions)->firstWhere('identifier', 'admin-only'); $this->assertNotNull($adminRootNode); } /** * is_assignable이 자식이 있는 노드에서 false인지 확인 */ public function test_is_assignable_is_false_for_parent_nodes(): void { // 부모 노드 $parent = Permission::create([ 'identifier' => 'parent-permission', 'name' => ['ko' => '부모 권한', 'en' => 'Parent Permission'], 'type' => PermissionType::Admin, 'parent_id' => null, ]); // 자식 노드 Permission::create([ 'identifier' => 'parent-permission.child', 'name' => ['ko' => '자식 권한', 'en' => 'Child Permission'], 'type' => PermissionType::Admin, 'parent_id' => $parent->id, ]); $result = $this->permissionService->getPermissionTree(); $adminPermissions = $result['permissions']['admin']['permissions']; $parentNode = collect($adminPermissions)->firstWhere('identifier', 'parent-permission'); $this->assertFalse($parentNode['is_assignable']); } // ======================================================================== // getPermissionCandidates — 레이아웃 편집기 표시 권한 후보 // ======================================================================== /** * 후보 목록은 리프(할당 가능) 권한만 포함하고 카테고리(자식 보유)는 제외한다. */ public function test_get_permission_candidates_excludes_category_parents(): void { $parent = Permission::factory()->create([ 'identifier' => 'cat.users', 'name' => ['ko' => '회원 관리', 'en' => 'Users'], 'parent_id' => null, ]); Permission::factory()->create([ 'identifier' => 'cat.users.read', 'name' => ['ko' => '회원 조회', 'en' => 'Read users'], 'parent_id' => $parent->id, ]); $candidates = $this->permissionService->getPermissionCandidates('ko'); $keys = array_column($candidates, 'key'); $this->assertContains('cat.users.read', $keys, '리프 권한은 후보에 포함되어야 한다'); $this->assertNotContains('cat.users', $keys, '카테고리(부모) 권한은 후보에서 제외되어야 한다'); } /** * 각 후보는 식별자(key)와 로케일 표시명(name)을 함께 반환한다. */ public function test_get_permission_candidates_returns_identifier_and_localized_name(): void { Permission::factory()->create([ 'identifier' => 'shop.order.refund', 'name' => ['ko' => '주문 환불', 'en' => 'Refund order'], 'parent_id' => null, ]); $candidates = $this->permissionService->getPermissionCandidates('ko'); $row = collect($candidates)->firstWhere('key', 'shop.order.refund'); $this->assertNotNull($row); $this->assertSame('shop.order.refund', $row['key']); $this->assertSame('주문 환불', $row['name']); } /** * 자식이 없는 단독 권한(루트지만 리프)도 후보에 포함된다. */ public function test_get_permission_candidates_includes_leaf_root_permission(): void { Permission::factory()->create([ 'identifier' => 'standalone.permission', 'name' => ['ko' => '단독 권한', 'en' => 'Standalone'], 'parent_id' => null, ]); $keys = array_column($this->permissionService->getPermissionCandidates('ko'), 'key'); $this->assertContains('standalone.permission', $keys); } }