Files
HeuJung 71abdeaf60 fix(security): KVE-2026-1914/1915/1919 remediation 전건
위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던
결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을
내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고
같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다.

- 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거
 대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는
 DB 쓰기에 선행하여 거부 시 상태 불변.
- 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체.
 비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static
 제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형.
- secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙
 소유권·비밀·발행 상태 검사 통일.
- 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를
 런타임·저장측·정적검사 3층 동형화.
- 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
2026-08-17 01:45:09 +09:00

879 lines
29 KiB
PHP

<?php
namespace Tests\Feature\Api\Admin;
use App\Enums\ExtensionOwnerType;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* AdminRoleController 테스트
*
* 역할 관리 API 엔드포인트를 테스트합니다.
*/
class RoleControllerTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private string $token;
protected function setUp(): void
{
parent::setUp();
$this->admin = $this->createAdminUser();
$this->token = $this->admin->createToken('test-token')->plainTextToken;
}
/**
* 관리자 역할 생성 및 할당
*
* @param array $permissions 사용자에게 부여할 권한 식별자 목록
*/
private function createAdminUser(array $permissions = ['core.permissions.read', 'core.permissions.create', 'core.permissions.update', 'core.permissions.delete', 'core.users.read']): User
{
$user = User::factory()->create();
// 권한 생성
$permissionIds = [];
foreach ($permissions as $permIdentifier) {
$permission = Permission::firstOrCreate(
['identifier' => $permIdentifier],
[
'name' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]),
'description' => json_encode(['ko' => $permIdentifier.' 권한', 'en' => $permIdentifier.' Permission']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'type' => 'admin',
]
);
$permissionIds[] = $permission->id;
}
// 고유한 식별자로 역할 생성 (테스트별 격리를 위해)
$roleIdentifier = 'admin_test_'.uniqid();
$testRole = Role::create([
'identifier' => $roleIdentifier,
'name' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Administrator']),
'description' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Administrator']),
'is_active' => true,
]);
// admin 역할도 추가 (admin 미들웨어 통과용)
$adminRole = Role::firstOrCreate(
['identifier' => 'admin'],
[
'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']),
'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']),
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
'is_active' => true,
]
);
// 테스트용 역할에 권한 할당
$testRole->permissions()->sync($permissionIds);
// 사용자에게 admin 역할과 테스트용 역할 모두 할당
$user->roles()->attach($adminRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
$user->roles()->attach($testRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
return $user->fresh();
}
/**
* 인증된 요청 헬퍼 메서드
*/
private function authRequest(): static
{
return $this->withHeaders([
'Authorization' => 'Bearer '.$this->token,
'Accept' => 'application/json',
]);
}
/**
* 테스트용 역할 생성 헬퍼
*/
private function createTestRole(array $attributes = []): Role
{
return Role::create(array_merge([
'identifier' => 'test_role_'.uniqid(),
'name' => json_encode(['ko' => '테스트 역할', 'en' => 'Test Role']),
'description' => json_encode(['ko' => '테스트 역할 설명', 'en' => 'Test Role Description']),
'is_active' => true,
], $attributes));
}
/**
* 테스트용 권한 생성 헬퍼
*/
private function createTestPermission(?string $identifier = null): Permission
{
return Permission::create([
'identifier' => $identifier ?? 'test.permission.'.uniqid(),
'name' => json_encode(['ko' => '테스트 권한', 'en' => 'Test Permission']),
'description' => json_encode(['ko' => '테스트 권한 설명', 'en' => 'Test Permission Description']),
'type' => 'admin',
]);
}
// ========================================================================
// 인증/권한 테스트
// ========================================================================
/**
* 인증 없이 역할 목록 조회 시 401 반환
*/
public function test_index_returns_401_without_authentication(): void
{
$response = $this->getJson('/api/admin/roles');
$response->assertStatus(401);
}
/**
* 권한 없이 역할 목록 조회 시 403 반환
*/
public function test_index_returns_403_without_permission(): void
{
// 권한 없는 관리자 생성
$user = User::factory()->create();
$adminRole = Role::where('identifier', 'admin')->first();
// 기존 권한 분리
$readPermission = Permission::where('identifier', 'core.permissions.read')->first();
if ($adminRole && $readPermission) {
$adminRole->permissions()->detach($readPermission->id);
}
$user->roles()->attach($adminRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
$token = $user->createToken('test-token')->plainTextToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/json',
])->getJson('/api/admin/roles');
$response->assertStatus(403);
}
/**
* 생성 권한 없이 역할 생성 시 403 반환
*/
public function test_store_returns_403_without_create_permission(): void
{
// read 권한만 있는 관리자 생성
$user = $this->createAdminUser(['core.permissions.read']);
$token = $user->createToken('test-token')->plainTextToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/json',
])->postJson('/api/admin/roles', [
'name' => ['ko' => '새 역할', 'en' => 'New Role'],
]);
$response->assertStatus(403);
}
// ========================================================================
// 역할 목록 테스트 (index)
// ========================================================================
/**
* 페이지네이션된 역할 목록 조회 성공
*/
public function test_index_returns_paginated_roles(): void
{
// 테스트 역할 생성
$this->createTestRole();
$this->createTestRole();
$response = $this->authRequest()->getJson('/api/admin/roles');
$response->assertStatus(200)
->assertJson(['success' => true])
->assertJsonStructure([
'success',
'message',
'data' => [
'data',
'abilities',
'pagination' => [
'current_page',
'last_page',
'per_page',
'total',
],
],
]);
}
/**
* 검색 필터 테스트
*/
public function test_index_supports_search_filter(): void
{
$this->createTestRole(['name' => json_encode(['ko' => '특별 역할', 'en' => 'Special Role'])]);
$this->createTestRole(['name' => json_encode(['ko' => '일반 역할', 'en' => 'General Role'])]);
$response = $this->authRequest()->getJson('/api/admin/roles?search=특별');
$response->assertStatus(200)
->assertJson(['success' => true]);
}
/**
* is_active 필터 테스트
*/
public function test_index_supports_is_active_filter(): void
{
$this->createTestRole(['is_active' => true]);
$this->createTestRole(['is_active' => false]);
$response = $this->authRequest()->getJson('/api/admin/roles?is_active=true');
$response->assertStatus(200)
->assertJson(['success' => true]);
}
// ========================================================================
// 활성 역할 테스트 (active)
// ========================================================================
/**
* 활성화된 역할만 조회
*/
public function test_active_returns_only_active_roles(): void
{
$activeRole = $this->createTestRole(['is_active' => true]);
$inactiveRole = $this->createTestRole(['is_active' => false]);
$response = $this->authRequest()->getJson('/api/admin/roles/active');
$response->assertStatus(200)
->assertJson(['success' => true]);
// 활성화된 역할이 포함되어 있는지 확인
$identifiers = collect($response->json('data.data'))->pluck('identifier')->toArray();
$this->assertContains($activeRole->identifier, $identifiers);
$this->assertNotContains($inactiveRole->identifier, $identifiers);
}
/**
* core.permissions.read 권한이 있으면 전체 활성 역할을 반환해야 합니다.
*/
public function test_active_returns_all_roles_with_permissions_read(): void
{
$extraRole = $this->createTestRole(['is_active' => true]);
$response = $this->authRequest()->getJson('/api/admin/roles/active');
$response->assertStatus(200)
->assertJson(['success' => true]);
$identifiers = collect($response->json('data.data'))->pluck('identifier')->toArray();
$this->assertContains($extraRole->identifier, $identifiers);
}
/**
* core.permissions.read 권한이 없으면 자기에게 부여된 역할만 반환해야 합니다.
*/
public function test_active_returns_only_own_roles_without_permissions_read(): void
{
// core.permissions.read 권한 없이 admin 미들웨어만 통과하는 사용자
$limitedUser = $this->createAdminUser(['core.users.read']);
$limitedToken = $limitedUser->createToken('test-token')->plainTextToken;
// 사용자에게 부여되지 않은 활성 역할 생성
$unassignedRole = $this->createTestRole(['is_active' => true]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$limitedToken,
'Accept' => 'application/json',
])->getJson('/api/admin/roles/active');
$response->assertStatus(200)
->assertJson(['success' => true]);
$identifiers = collect($response->json('data.data'))->pluck('identifier')->toArray();
// 부여되지 않은 역할은 포함되지 않아야 함
$this->assertNotContains($unassignedRole->identifier, $identifiers);
// 사용자의 역할(admin)은 포함되어야 함
$this->assertContains('admin', $identifiers);
}
/**
* roles/active 응답에 can_assign_roles abilities 포함 확인
*/
public function test_active_returns_abilities_with_can_assign_roles(): void
{
$response = $this->authRequest()->getJson('/api/admin/roles/active');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'abilities' => ['can_assign_roles'],
],
]);
}
// ========================================================================
// 역할 상세 테스트 (show)
// ========================================================================
/**
* 역할 상세 조회 성공 (권한 포함)
*
* 목록에서 뺀 권한 집합이 상세에는 그대로 남아 있어야 한다 — 이 계약이 깨지면 권한 편집
* 화면이 빈 채로 열린다.
*
* @effects detail_still_returns_full_payload
*/
public function test_show_returns_role_with_permissions(): void
{
$role = $this->createTestRole();
$permission = $this->createTestPermission();
$role->permissions()->attach($permission->id);
$response = $this->authRequest()->getJson("/api/admin/roles/{$role->id}");
$response->assertStatus(200)
->assertJson(['success' => true])
->assertJsonStructure([
'success',
'message',
'data' => [
'id',
'identifier',
'name',
'description',
'extension_type',
'is_active',
],
]);
}
/**
* 역할 상세 조회 시 extension_name 필드 포함 확인
*/
public function test_show_returns_extension_name_field(): void
{
$role = $this->createTestRole();
$response = $this->authRequest()->getJson("/api/admin/roles/{$role->id}");
$response->assertStatus(200)
->assertJson(['success' => true])
->assertJsonPath('data.extension_name', null); // user-created role → null
}
/**
* 코어 역할 조회 시 extension_name null 확인
*/
public function test_show_returns_null_extension_name_for_core_role(): void
{
$coreRole = $this->createTestRole([
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
]);
$response = $this->authRequest()->getJson("/api/admin/roles/{$coreRole->id}");
$response->assertStatus(200)
->assertJsonPath('data.extension_type', 'core')
->assertJsonPath('data.extension_name', null);
}
/**
* 존재하지 않는 역할 조회 시 404 반환
*/
public function test_show_returns_404_for_nonexistent_role(): void
{
$response = $this->authRequest()->getJson('/api/admin/roles/99999');
$response->assertStatus(404);
}
// ========================================================================
// 역할 생성 테스트 (store)
// ========================================================================
/**
* 역할 생성 성공
*/
public function test_store_creates_role_successfully(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'identifier' => 'new_role_'.uniqid(),
'name' => ['ko' => '새 역할', 'en' => 'New Role'],
'description' => ['ko' => '새 역할 설명', 'en' => 'New Role Description'],
'is_active' => true,
]);
$response->assertStatus(201)
->assertJson(['success' => true]);
// 역할이 생성되었는지 확인 (응답의 ID로 조회)
$roleId = $response->json('data.id');
$role = Role::find($roleId);
$this->assertNotNull($role);
$this->assertEquals('새 역할', $role->name['ko']);
}
/**
* 권한과 함께 역할 생성 성공
*/
public function test_store_creates_role_with_permissions(): void
{
// 권한 부여 상한(KVE-2026-1919 C-4): 비-슈퍼관리자는 자신이 보유하지 않은
// 권한을 역할에 부여할 수 없다. 이 테스트는 임의 권한 부여의 정상 경로를
// 검증하므로 액터를 슈퍼 관리자로 승격한다.
$this->admin->update(['is_super' => true]);
$permission1 = $this->createTestPermission('test.perm.one');
$permission2 = $this->createTestPermission('test.perm.two');
$response = $this->authRequest()->postJson('/api/admin/roles', [
'identifier' => 'role_with_perms_'.uniqid(),
'name' => ['ko' => '권한 포함 역할', 'en' => 'Role with Permissions'],
'permissions' => [
['id' => $permission1->id, 'scope_type' => null],
['id' => $permission2->id, 'scope_type' => null],
],
]);
$response->assertStatus(201)
->assertJson(['success' => true]);
// 권한이 할당되었는지 확인
$roleId = $response->json('data.id');
$role = Role::find($roleId);
$this->assertCount(2, $role->permissions);
}
/**
* name 필수 검증
*/
public function test_store_validates_name_required(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'description' => ['ko' => '설명만', 'en' => 'Description only'],
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['name']);
}
/**
* identifier를 지정하여 역할 생성 성공
*/
public function test_store_creates_role_with_custom_identifier(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => 'ID 지정 역할', 'en' => 'Role with ID'],
'identifier' => 'custom_role_id',
'is_active' => true,
]);
$response->assertStatus(201)
->assertJson(['success' => true]);
$roleId = $response->json('data.id');
$role = Role::find($roleId);
$this->assertEquals('custom_role_id', $role->identifier);
}
/**
* identifier 미지정 시 422 반환 (필수 필드)
*/
public function test_store_validates_identifier_required(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => '자동 생성', 'en' => 'Auto Generated'],
'is_active' => true,
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['identifier']);
}
/**
* identifier 중복 시 422 반환
*/
public function test_store_validates_identifier_unique(): void
{
$this->createTestRole(['identifier' => 'duplicate_id']);
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => '중복 ID 역할', 'en' => 'Duplicate ID Role'],
'identifier' => 'duplicate_id',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['identifier']);
}
/**
* identifier 형식 검증 (대문자 포함 시 실패)
*/
public function test_store_validates_identifier_format(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => '형식 오류', 'en' => 'Bad Format'],
'identifier' => 'Invalid-ID',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['identifier']);
}
/**
* identifier 형식 검증 (숫자로 시작 시 실패)
*/
public function test_store_validates_identifier_must_start_with_letter(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => '숫자 시작', 'en' => 'Starts with number'],
'identifier' => '1invalid',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['identifier']);
}
/**
* 다국어 name 형식 검증
*/
public function test_store_validates_translatable_name_format(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => '', 'en' => ''], // 빈 문자열
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['name']);
}
/**
* permissions.*.id 존재 검증
*/
public function test_store_validates_permission_ids_exist(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'name' => ['ko' => '새 역할', 'en' => 'New Role'],
'permissions' => [
['id' => 99999, 'scope_type' => null],
],
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['permissions.0.id']);
}
/**
* 문자열 name을 다국어 배열로 자동 변환
*/
public function test_store_converts_string_name_to_translatable(): void
{
$response = $this->authRequest()->postJson('/api/admin/roles', [
'identifier' => 'string_name_role_'.uniqid(),
'name' => '단순 문자열 역할', // 문자열로 전달
]);
$response->assertStatus(201)
->assertJson(['success' => true]);
// 다국어 형식으로 저장되었는지 확인
$roleId = $response->json('data.id');
$role = Role::find($roleId);
// Role 모델의 name은 array로 캐스팅되므로 직접 배열로 접근
$name = $role->name;
$this->assertIsArray($name);
$this->assertArrayHasKey('ko', $name);
$this->assertArrayHasKey('en', $name);
}
// ========================================================================
// 역할 수정 테스트 (update)
// ========================================================================
/**
* 역할 수정 성공
*/
public function test_update_modifies_role_successfully(): void
{
$role = $this->createTestRole();
$response = $this->authRequest()->putJson("/api/admin/roles/{$role->id}", [
'name' => ['ko' => '수정된 역할', 'en' => 'Updated Role'],
'is_active' => false,
]);
$response->assertStatus(200)
->assertJson(['success' => true]);
$this->assertDatabaseHas('roles', [
'id' => $role->id,
'is_active' => false,
]);
}
/**
* 권한 동기화 테스트
*/
public function test_update_syncs_permissions(): void
{
// 권한 부여 상한(KVE-2026-1919 C-4): 임의 권한 동기화의 정상 경로를 검증하므로
// 액터를 슈퍼 관리자로 승격한다.
$this->admin->update(['is_super' => true]);
$role = $this->createTestRole();
$oldPermission = $this->createTestPermission('old.permission');
$newPermission = $this->createTestPermission('new.permission');
// 기존 권한 할당
$role->permissions()->attach($oldPermission->id);
// 새 권한으로 교체
$response = $this->authRequest()->putJson("/api/admin/roles/{$role->id}", [
'name' => ['ko' => '수정된 역할', 'en' => 'Updated Role'],
'permissions' => [
['id' => $newPermission->id, 'scope_type' => null],
],
]);
$response->assertStatus(200);
// 권한이 교체되었는지 확인
$role->refresh();
$permissionIds = $role->permissions->pluck('id')->toArray();
$this->assertNotContains($oldPermission->id, $permissionIds);
$this->assertContains($newPermission->id, $permissionIds);
}
// ========================================================================
// 역할 삭제 테스트 (destroy)
// ========================================================================
/**
* 역할 삭제 성공
*/
public function test_destroy_deletes_role_successfully(): void
{
$role = $this->createTestRole();
$response = $this->authRequest()->deleteJson("/api/admin/roles/{$role->id}");
$response->assertStatus(200)
->assertJson(['success' => true]);
$this->assertDatabaseMissing('roles', [
'id' => $role->id,
]);
}
/**
* 시스템 역할 삭제 시 403 반환
*/
public function test_destroy_returns_403_for_core_role(): void
{
$systemRole = $this->createTestRole([
'extension_type' => ExtensionOwnerType::Core,
'extension_identifier' => 'core',
]);
$response = $this->authRequest()->deleteJson("/api/admin/roles/{$systemRole->id}");
$response->assertStatus(403);
// 역할이 삭제되지 않았는지 확인
$this->assertDatabaseHas('roles', [
'id' => $systemRole->id,
]);
}
/**
* 모듈 소유 역할 삭제 시 403 반환
*/
public function test_destroy_returns_403_for_module_owned_role(): void
{
$moduleRole = $this->createTestRole([
'extension_type' => ExtensionOwnerType::Module,
'extension_identifier' => 'sirsoft-board',
]);
$response = $this->authRequest()->deleteJson("/api/admin/roles/{$moduleRole->id}");
$response->assertStatus(403);
// 역할이 삭제되지 않았는지 확인
$this->assertDatabaseHas('roles', [
'id' => $moduleRole->id,
]);
}
/**
* 플러그인 소유 역할 삭제 시 403 반환
*/
public function test_destroy_returns_403_for_plugin_owned_role(): void
{
$pluginRole = $this->createTestRole([
'extension_type' => ExtensionOwnerType::Plugin,
'extension_identifier' => 'sirsoft-payment',
]);
$response = $this->authRequest()->deleteJson("/api/admin/roles/{$pluginRole->id}");
$response->assertStatus(403);
// 역할이 삭제되지 않았는지 확인
$this->assertDatabaseHas('roles', [
'id' => $pluginRole->id,
]);
}
/**
* 사용자 생성 역할(extension_type=null) 삭제 성공
*/
public function test_destroy_succeeds_for_user_created_role(): void
{
$userRole = $this->createTestRole(); // extension_type is null by default
$response = $this->authRequest()->deleteJson("/api/admin/roles/{$userRole->id}");
$response->assertStatus(200)
->assertJson(['success' => true]);
$this->assertDatabaseMissing('roles', [
'id' => $userRole->id,
]);
}
/**
* 존재하지 않는 역할 삭제 시 404 반환
*/
public function test_destroy_returns_404_for_nonexistent_role(): void
{
$response = $this->authRequest()->deleteJson('/api/admin/roles/99999');
$response->assertStatus(404);
}
// ========================================================================
// 역할 상태 토글 테스트 (toggleStatus)
// ========================================================================
/**
* 활성 역할을 비활성으로 토글
*/
public function test_toggle_status_deactivates_active_role(): void
{
$role = $this->createTestRole(['is_active' => true]);
$response = $this->authRequest()->patchJson("/api/admin/roles/{$role->id}/toggle-status");
$response->assertStatus(200)
->assertJson(['success' => true]);
$this->assertDatabaseHas('roles', [
'id' => $role->id,
'is_active' => false,
]);
}
/**
* 비활성 역할을 활성으로 토글
*/
public function test_toggle_status_activates_inactive_role(): void
{
$role = $this->createTestRole(['is_active' => false]);
$response = $this->authRequest()->patchJson("/api/admin/roles/{$role->id}/toggle-status");
$response->assertStatus(200)
->assertJson(['success' => true]);
$this->assertDatabaseHas('roles', [
'id' => $role->id,
'is_active' => true,
]);
}
/**
* 토글 후 응답에 최신 is_active 값이 포함되는지 확인
*/
public function test_toggle_status_returns_updated_resource(): void
{
$role = $this->createTestRole(['is_active' => true]);
$response = $this->authRequest()->patchJson("/api/admin/roles/{$role->id}/toggle-status");
$response->assertStatus(200)
->assertJsonPath('data.is_active', false);
}
/**
* update 권한 없이 토글 시 403 반환
*/
public function test_toggle_status_returns_403_without_update_permission(): void
{
$user = $this->createAdminUser(['core.permissions.read']);
$token = $user->createToken('test-token')->plainTextToken;
$role = $this->createTestRole();
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
'Accept' => 'application/json',
])->patchJson("/api/admin/roles/{$role->id}/toggle-status");
$response->assertStatus(403);
}
/**
* 존재하지 않는 역할 토글 시 404 반환
*/
public function test_toggle_status_returns_404_for_nonexistent_role(): void
{
$response = $this->authRequest()->patchJson('/api/admin/roles/99999/toggle-status');
$response->assertStatus(404);
}
/**
* 응답에 can_toggle_status ability 포함 확인
*/
public function test_toggle_status_response_includes_abilities(): void
{
$role = $this->createTestRole();
$response = $this->authRequest()->patchJson("/api/admin/roles/{$role->id}/toggle-status");
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'abilities' => ['can_toggle_status'],
],
]);
}
}