userService = app(UserService::class); } // ======================================================================== // deleteUser() - 슈퍼 관리자 삭제 방지 테스트 // ======================================================================== /** * 슈퍼 관리자 삭제 시 CannotDeleteSuperAdminException 발생 확인 */ public function test_delete_super_admin_throws_cannot_delete_super_admin_exception(): void { $superAdmin = User::factory()->create(['is_super' => true]); $this->expectException(CannotDeleteSuperAdminException::class); $this->userService->deleteUser($superAdmin); } /** * 슈퍼 관리자 삭제 시 예외 메시지가 올바른지 확인 */ public function test_cannot_delete_super_admin_exception_has_correct_message(): void { $superAdmin = User::factory()->create(['is_super' => true]); try { $this->userService->deleteUser($superAdmin); $this->fail('Expected CannotDeleteSuperAdminException was not thrown'); } catch (CannotDeleteSuperAdminException $e) { // 예외 메시지가 다국어 키로 번역되는지 확인 $this->assertNotEmpty($e->getMessage()); } } // ======================================================================== // deleteUser() - 관리자 삭제: 권한/스코프 기반 위임 (하드코딩 차단 제거) // ======================================================================== /** * 관리자 권한을 가진 사용자(target=admin) 도 UserService::deleteUser 단독으로는 삭제 가능해야 한다. * * 회귀: "target.isAdmin() → CannotDeleteAdminException" 하드코딩이 G7 의 역할/퍼미션/스코프 시스템을 * 우회해 슈퍼관리자도 다른 관리자를 삭제하지 못하도록 막던 결함. 권한/스코프 검증은 미들웨어 * (PermissionMiddleware) 가 담당하고, 시스템 불변식인 슈퍼관리자 보호만 Service 가 보장한다. */ public function test_delete_admin_user_succeeds_when_actor_authorized(): void { $adminPermission = Permission::create([ 'identifier' => 'core.admin.test', 'name' => ['ko' => '관리자 권한', 'en' => 'Admin Permission'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $role = Role::create([ 'identifier' => 'test-admin', 'name' => ['ko' => '테스트 관리자', 'en' => 'Test Admin'], ]); $role->permissions()->attach($adminPermission->id); $adminUser = User::factory()->create(['is_super' => false]); $adminUser->roles()->attach($role->id); $result = $this->userService->deleteUser($adminUser); $this->assertTrue($result); $this->assertDatabaseMissing('users', ['id' => $adminUser->id]); } // ======================================================================== // deleteUser() - 일반 사용자 삭제 테스트 // ======================================================================== /** * 일반 사용자는 정상적으로 삭제 가능 */ public function test_delete_regular_user_succeeds(): void { $regularUser = User::factory()->create(['is_super' => false]); $result = $this->userService->deleteUser($regularUser); $this->assertTrue($result); $this->assertDatabaseMissing('users', ['id' => $regularUser->id]); } /** * user 타입 권한만 가진 사용자는 삭제 가능 */ public function test_delete_user_with_only_user_type_permissions_succeeds(): void { // user 타입 권한 생성 $userPermission = Permission::create([ 'identifier' => 'frontend.user.test', 'name' => ['ko' => '사용자 권한', 'en' => 'User Permission'], 'type' => PermissionType::User, ]); // 역할 생성 및 권한 연결 $role = Role::create([ 'identifier' => 'test-user', 'name' => ['ko' => '테스트 사용자', 'en' => 'Test User'], ]); $role->permissions()->attach($userPermission->id); // 사용자 생성 및 역할 연결 $user = User::factory()->create(['is_super' => false]); $user->roles()->attach($role->id); $result = $this->userService->deleteUser($user); $this->assertTrue($result); $this->assertDatabaseMissing('users', ['id' => $user->id]); } /** * 역할이 없는 사용자도 삭제 가능 */ public function test_delete_user_without_roles_succeeds(): void { $userWithoutRoles = User::factory()->create(['is_super' => false]); // 역할이 없는지 확인 $this->assertEquals(0, $userWithoutRoles->roles()->count()); $result = $this->userService->deleteUser($userWithoutRoles); $this->assertTrue($result); $this->assertDatabaseMissing('users', ['id' => $userWithoutRoles->id]); } // ======================================================================== // 삭제 우선순위 테스트 (슈퍼 관리자 > 관리자) // ======================================================================== /** * 슈퍼 관리자이면서 관리자 권한도 있는 경우, 슈퍼 관리자 예외가 우선 */ public function test_super_admin_exception_takes_priority_over_admin_exception(): void { // admin 타입 권한 생성 $adminPermission = Permission::create([ 'identifier' => 'core.priority.test', 'name' => ['ko' => '우선순위 테스트', 'en' => 'Priority Test'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); // 역할 생성 및 권한 연결 $role = Role::create([ 'identifier' => 'super-admin-role', 'name' => ['ko' => '슈퍼관리자 역할', 'en' => 'Super Admin Role'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', ]); $role->permissions()->attach($adminPermission->id); // 슈퍼 관리자이면서 관리자 역할도 가진 사용자 $superAdminWithRole = User::factory()->create(['is_super' => true]); $superAdminWithRole->roles()->attach($role->id); // 슈퍼 관리자 예외가 먼저 발생해야 함 $this->expectException(CannotDeleteSuperAdminException::class); $this->userService->deleteUser($superAdminWithRole); } // ======================================================================== // deleteUser() - 관계 레코드 명시적 삭제 검증 (CASCADE 의존 금지) // ======================================================================== /** * 사용자 삭제 시 역할 연결이 해제되는지 확인 */ public function test_delete_user_detaches_roles(): void { $role = Role::create([ 'identifier' => 'test-detach-role', 'name' => ['ko' => '역할 해제 테스트', 'en' => 'Detach Test'], ]); $user = User::factory()->create(['is_super' => false]); $user->roles()->attach($role->id); $this->assertDatabaseHas('user_roles', ['user_id' => $user->id, 'role_id' => $role->id]); $this->userService->deleteUser($user); $this->assertDatabaseMissing('user_roles', ['user_id' => $user->id]); } /** * 사용자 삭제 시 약관 동의 이력이 삭제되는지 확인 */ public function test_delete_user_deletes_consents(): void { $user = User::factory()->create(['is_super' => false]); UserConsent::create([ 'user_id' => $user->id, 'consent_type' => 'terms', 'agreed_at' => now(), ]); $this->assertDatabaseHas('user_consents', ['user_id' => $user->id]); $this->userService->deleteUser($user); $this->assertDatabaseMissing('user_consents', ['user_id' => $user->id]); } /** * 사용자 삭제 시 API 토큰이 삭제되는지 확인 */ public function test_delete_user_deletes_tokens(): void { $user = User::factory()->create(['is_super' => false]); $user->createToken('test-token'); $this->assertDatabaseHas('personal_access_tokens', [ 'tokenable_id' => $user->id, 'tokenable_type' => User::class, ]); $this->userService->deleteUser($user); $this->assertDatabaseMissing('personal_access_tokens', [ 'tokenable_id' => $user->id, 'tokenable_type' => User::class, ]); } // ======================================================================== // createUser() 테스트 (is_super 필드 처리) // ======================================================================== /** * 사용자 생성 시 is_super 필드가 정상적으로 저장되는지 확인 */ public function test_create_user_with_is_super_true(): void { $userData = [ 'name' => 'Super Admin Test', 'email' => 'superadmin@test.com', 'password' => 'password123', 'is_super' => true, ]; $user = $this->userService->createUser($userData); $this->assertTrue($user->is_super); $this->assertTrue($user->isSuperAdmin()); } /** * 사용자 생성 시 is_super가 기본값 false인지 확인 */ #[Test] public function test_create_user_is_super_defaults_to_false(): void { $userData = [ 'name' => 'Regular User Test', 'email' => 'regularuser@test.com', 'password' => 'password123', ]; $user = $this->userService->createUser($userData); $this->assertFalse($user->is_super); $this->assertFalse($user->isSuperAdmin()); } // ======================================================================== // updateUser() 테스트 (is_super 필드 처리) // ======================================================================== /** * 사용자 업데이트 시 is_super 필드 변경 가능 */ public function test_update_user_can_change_is_super(): void { $user = User::factory()->create(['is_super' => false]); $this->assertFalse($user->is_super); $updatedUser = $this->userService->updateUser($user, ['is_super' => true]); $this->assertTrue($updatedUser->is_super); $this->assertTrue($updatedUser->isSuperAdmin()); } /** * 슈퍼 관리자를 일반 사용자로 강등 가능 */ public function test_update_user_can_demote_super_admin(): void { $superAdmin = User::factory()->create(['is_super' => true]); $this->assertTrue($superAdmin->isSuperAdmin()); $updatedUser = $this->userService->updateUser($superAdmin, ['is_super' => false]); $this->assertFalse($updatedUser->is_super); $this->assertFalse($updatedUser->isSuperAdmin()); } // ======================================================================== // updateUser() — 역할 할당 권한 체크 테스트 // ======================================================================== /** * core.permissions.update 권한이 있으면 자기 자신의 역할도 변경 가능 */ public function test_update_user_can_change_own_roles_with_permission(): void { // core.permissions.update 권한을 가진 사용자 $permUpdatePermission = Permission::create([ 'identifier' => 'core.permissions.update', 'name' => ['ko' => '권한 수정', 'en' => 'Update Permissions'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $adminRole = Role::create([ 'identifier' => 'test-perm-admin', 'name' => ['ko' => '권한 관리자', 'en' => 'Permission Admin'], ]); $adminRole->permissions()->attach($permUpdatePermission->id); $newRole = Role::create([ 'identifier' => 'new-role', 'name' => ['ko' => '새 역할', 'en' => 'New Role'], ]); $user = User::factory()->create(); $user->roles()->attach($adminRole->id); // 자기 자신으로 인증 Auth::login($user); // 자기 자신의 역할 변경 시도 (admin 역할이 아니므로 자기잠금 방지 해당 없음) $updatedUser = $this->userService->updateUser($user, [ 'name' => 'Updated Name', 'role_ids' => [$adminRole->id, $newRole->id], ]); // 이름과 역할 모두 변경되어야 함 $this->assertEquals('Updated Name', $updatedUser->name); $this->assertTrue($updatedUser->roles->contains('id', $adminRole->id)); $this->assertTrue($updatedUser->roles->contains('id', $newRole->id)); } /** * 마지막 admin 역할 사용자가 자기 admin 역할을 제거하려는 경우 차단 */ public function test_update_user_prevents_last_admin_role_removal(): void { $permUpdatePermission = Permission::create([ 'identifier' => 'core.permissions.update', 'name' => ['ko' => '권한 수정', 'en' => 'Update Permissions'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $adminRole = Role::create([ 'identifier' => 'admin', 'name' => ['ko' => '관리자', 'en' => 'Admin'], ]); $adminRole->permissions()->attach($permUpdatePermission->id); $otherRole = Role::create([ 'identifier' => 'other-role', 'name' => ['ko' => '다른 역할', 'en' => 'Other Role'], ]); // 유일한 admin 사용자 $user = User::factory()->create(); $user->roles()->attach($adminRole->id); Auth::login($user); // admin 역할을 제거하고 다른 역할만 남기려는 시도 $this->expectException(ValidationException::class); $this->userService->updateUser($user, [ 'role_ids' => [$otherRole->id], ]); } /** * admin 역할을 가진 다른 사용자가 있으면 자기 admin 역할 제거 가능 */ public function test_update_user_allows_admin_role_removal_when_other_admins_exist(): void { $permUpdatePermission = Permission::create([ 'identifier' => 'core.permissions.update', 'name' => ['ko' => '권한 수정', 'en' => 'Update Permissions'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $adminRole = Role::create([ 'identifier' => 'admin', 'name' => ['ko' => '관리자', 'en' => 'Admin'], ]); $adminRole->permissions()->attach($permUpdatePermission->id); $otherRole = Role::create([ 'identifier' => 'other-role-2', 'name' => ['ko' => '다른 역할', 'en' => 'Other Role'], ]); // admin 역할을 가진 사용자 2명 $user = User::factory()->create(); $user->roles()->attach($adminRole->id); $otherAdmin = User::factory()->create(); $otherAdmin->roles()->attach($adminRole->id); Auth::login($user); // 다른 admin이 있으므로 자기 admin 역할 제거 가능 $updatedUser = $this->userService->updateUser($user, [ 'role_ids' => [$otherRole->id], ]); $this->assertFalse($updatedUser->roles->contains('id', $adminRole->id)); $this->assertTrue($updatedUser->roles->contains('id', $otherRole->id)); } /** * 마지막 admin 역할 제거 시 ValidationException에 role_ids 에러가 포함되는지 확인 */ public function test_last_admin_role_removal_has_role_ids_error_key(): void { $permUpdatePermission = Permission::create([ 'identifier' => 'core.permissions.update', 'name' => ['ko' => '권한 수정', 'en' => 'Update Permissions'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $adminRole = Role::create([ 'identifier' => 'admin', 'name' => ['ko' => '관리자', 'en' => 'Admin'], ]); $adminRole->permissions()->attach($permUpdatePermission->id); $otherRole = Role::create([ 'identifier' => 'user-role', 'name' => ['ko' => '사용자', 'en' => 'User'], ]); $user = User::factory()->create(); $user->roles()->attach($adminRole->id); Auth::login($user); try { $this->userService->updateUser($user, [ 'role_ids' => [$otherRole->id], ]); $this->fail('Expected ValidationException was not thrown'); } catch (ValidationException $e) { $this->assertArrayHasKey('role_ids', $e->errors()); $this->assertNotEmpty($e->errors()['role_ids']); } } /** * 역할 부여는 core.permissions.update(역할 정의 수정)를 요구하지 않는다 — 상한(ceiling) * 이내라면 그 권한이 없어도 역할을 부여할 수 있다. (역할 부여 = 사용자 관리의 일부) * * 회귀 방지: 과거에는 core.permissions.update 가 없으면 role_ids 를 조용히 버려(silent * no-op) 200 성공을 반환하면서도 역할이 바뀌지 않았다. 이제는 상한만 통과하면 부여된다. */ public function test_update_user_assigns_role_within_ceiling_without_permissions_update(): void { $originalRole = Role::create([ 'identifier' => 'original-role', 'name' => ['ko' => '원래 역할', 'en' => 'Original Role'], ]); // 권한이 전혀 없는 역할 → 어떤 액터의 상한도 넘지 않는다. $newRole = Role::create([ 'identifier' => 'new-role-test', 'name' => ['ko' => '새 역할', 'en' => 'New Role'], ]); $targetUser = User::factory()->create(); $targetUser->roles()->attach($originalRole->id); // core.permissions.update 가 없는 액터로 인증 (사용자 관리 권한은 라우트 단계에서 // 강제되며, 서비스 계층 테스트에서는 상한만 검사된다) $authUser = User::factory()->create(); Auth::login($authUser); $updatedUser = $this->userService->updateUser($targetUser, [ 'name' => 'Changed Name', 'role_ids' => [$newRole->id], ]); // 이름과 역할 모두 변경되어야 한다 (silent no-op 아님) $this->assertEquals('Changed Name', $updatedUser->name); $this->assertTrue($updatedUser->roles->contains('id', $newRole->id)); $this->assertFalse($updatedUser->roles->contains('id', $originalRole->id)); } /** * 상한을 넘는 역할(액터가 보유하지 않은 권한을 담은 역할)은 부여 시 명시적으로 거부된다. * * 회귀 방지: 조용히 무시(silent no-op)하는 대신 PermissionEscalationException 을 던져 * 컨트롤러가 403 으로 매핑하도록 한다. */ public function test_update_user_rejects_role_beyond_actor_ceiling(): void { $elevatedPermission = Permission::create([ 'identifier' => 'core.templates.uninstall', 'name' => ['ko' => '템플릿 삭제', 'en' => 'Uninstall Templates'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); // 액터가 보유하지 않은 권한을 담은 역할 → 상한 초과 $elevatedRole = Role::create([ 'identifier' => 'elevated-role', 'name' => ['ko' => '상위 역할', 'en' => 'Elevated Role'], ]); $elevatedRole->permissions()->attach($elevatedPermission->id); $targetUser = User::factory()->create(); // 그 권한을 보유하지 않은 액터 $authUser = User::factory()->create(); Auth::login($authUser); $this->expectException(PermissionEscalationException::class); $this->userService->updateUser($targetUser, [ 'role_ids' => [$elevatedRole->id], ]); } /** * core.permissions.update 권한이 있으면 타인의 역할 변경 가능 */ public function test_update_user_allows_role_change_with_permission(): void { $permUpdatePermission = Permission::create([ 'identifier' => 'core.permissions.update', 'name' => ['ko' => '권한 수정', 'en' => 'Update Permissions'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $adminRole = Role::create([ 'identifier' => 'perm-admin-role', 'name' => ['ko' => '권한 관리 역할', 'en' => 'Permission Admin Role'], ]); $adminRole->permissions()->attach($permUpdatePermission->id); $originalRole = Role::create([ 'identifier' => 'original-role-2', 'name' => ['ko' => '원래 역할', 'en' => 'Original Role'], ]); $newRole = Role::create([ 'identifier' => 'target-role', 'name' => ['ko' => '대상 역할', 'en' => 'Target Role'], ]); $targetUser = User::factory()->create(); $targetUser->roles()->attach($originalRole->id); // 권한 있는 사용자로 인증 $authUser = User::factory()->create(); $authUser->roles()->attach($adminRole->id); Auth::login($authUser); // 타인의 역할 변경 $updatedUser = $this->userService->updateUser($targetUser, [ 'role_ids' => [$newRole->id], ]); // 역할이 새 역할로 변경되어야 함 $this->assertTrue($updatedUser->roles->contains('id', $newRole->id)); $this->assertFalse($updatedUser->roles->contains('id', $originalRole->id)); } // ======================================================================== // createUser() — 역할 할당 권한 체크 테스트 // ======================================================================== /** * 역할을 지정하지 않고 생성하면 기본 역할('user')이 자동 배정된다. * * 회귀 방지: 기본 역할 배정은 권한 게이트가 아니라 "요청 역할 없음" 조건에 묶인다. * (과거에는 core.permissions.update 미보유 조건에 묶여 있어 지정 역할을 덮어썼다) */ public function test_create_user_assigns_default_role_when_none_requested(): void { $userRole = Role::create([ 'identifier' => 'user', 'name' => ['ko' => '사용자', 'en' => 'User'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'is_active' => true, ]); $authUser = User::factory()->create(); Auth::login($authUser); $user = $this->userService->createUser([ 'name' => 'Test User', 'email' => 'testcreate@test.com', 'password' => 'password123', ]); $this->assertTrue($user->roles->contains('id', $userRole->id)); } /** * core.permissions.update 가 없어도 상한 이내의 지정 역할은 그대로 부여된다 * (기본 역할로 덮어쓰지 않는다). * * 회귀 방지: 과거에는 core.permissions.update 미보유 시 지정 역할을 버리고 기본 역할을 * 강제 배정했다. 이제는 상한만 통과하면 지정 역할이 부여된다. */ public function test_create_user_assigns_requested_role_within_ceiling_without_permissions_update(): void { $userRole = Role::create([ 'identifier' => 'user', 'name' => ['ko' => '사용자', 'en' => 'User'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'is_active' => true, ]); // 권한이 없는 역할 → 상한 이내 $customRole = Role::create([ 'identifier' => 'custom-role', 'name' => ['ko' => '커스텀 역할', 'en' => 'Custom Role'], ]); $authUser = User::factory()->create(); Auth::login($authUser); $user = $this->userService->createUser([ 'name' => 'Test User', 'email' => 'testcreate@test.com', 'password' => 'password123', 'role_ids' => [$customRole->id], ]); // 지정 역할이 부여되고 기본 역할로 덮어쓰이지 않는다 $this->assertTrue($user->roles->contains('id', $customRole->id)); $this->assertFalse($user->roles->contains('id', $userRole->id)); } /** * 상한을 넘는 역할을 지정해 생성하면 명시적으로 거부된다. */ public function test_create_user_rejects_role_beyond_actor_ceiling(): void { $elevatedPermission = Permission::create([ 'identifier' => 'core.templates.uninstall', 'name' => ['ko' => '템플릿 삭제', 'en' => 'Uninstall Templates'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $elevatedRole = Role::create([ 'identifier' => 'elevated-create-role', 'name' => ['ko' => '상위 역할', 'en' => 'Elevated Role'], ]); $elevatedRole->permissions()->attach($elevatedPermission->id); $authUser = User::factory()->create(); Auth::login($authUser); $this->expectException(PermissionEscalationException::class); $this->userService->createUser([ 'name' => 'New User', 'email' => 'newuser@test.com', 'password' => 'password123', 'role_ids' => [$elevatedRole->id], ]); } /** * core.permissions.update 권한이 있으면 지정한 역할 할당 가능 */ public function test_create_user_assigns_specified_roles_with_permission(): void { $permUpdatePermission = Permission::create([ 'identifier' => 'core.permissions.update', 'name' => ['ko' => '권한 수정', 'en' => 'Update Permissions'], 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', 'type' => PermissionType::Admin, ]); $adminRole = Role::create([ 'identifier' => 'create-perm-admin', 'name' => ['ko' => '권한 관리자', 'en' => 'Permission Admin'], ]); $adminRole->permissions()->attach($permUpdatePermission->id); $customRole = Role::create([ 'identifier' => 'assigned-role', 'name' => ['ko' => '할당 역할', 'en' => 'Assigned Role'], ]); // 권한 있는 사용자로 인증 $authUser = User::factory()->create(); $authUser->roles()->attach($adminRole->id); Auth::login($authUser); $user = $this->userService->createUser([ 'name' => 'New User', 'email' => 'newuser@test.com', 'password' => 'password123', 'role_ids' => [$customRole->id], ]); // 지정한 역할이 할당되어야 함 $this->assertTrue($user->roles->contains('id', $customRole->id)); } }