공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다. 응답·행의 무게를 다룬 · 과 겹치지 않는다. 공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이 기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은 per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지 번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로 정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은 계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다. 계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면 그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다. 응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새 필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate 응답은 필드 단위로 이전과 동일하다. 요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일 재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어 화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한 변경이 종전처럼 다음 요청에 반영된다. 검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한 엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어 활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩 대신 선언형 config 로 옮겼다. 총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을 돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
1204 lines
46 KiB
PHP
1204 lines
46 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Middleware;
|
|
|
|
use App\Enums\ExtensionOwnerType;
|
|
use App\Enums\PermissionType;
|
|
use App\Enums\ScheduleType;
|
|
use App\Enums\ScopeType;
|
|
use App\Helpers\PermissionHelper;
|
|
use App\Http\Middleware\PermissionMiddleware;
|
|
use App\Models\Menu;
|
|
use App\Models\Permission;
|
|
use App\Models\Role;
|
|
use App\Models\Schedule;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Tests\TestCase;
|
|
|
|
class PermissionMiddlewareTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $userWithAdminPermission;
|
|
|
|
private User $userWithUserPermission;
|
|
|
|
private User $userWithoutPermission;
|
|
|
|
private Permission $adminPermission;
|
|
|
|
private Permission $userPermission;
|
|
|
|
private Permission $additionalAdminPermission;
|
|
|
|
private Role $adminRole;
|
|
|
|
private Role $userRole;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// PermissionHelper static 캐시 초기화 (테스트 간 격리)
|
|
$reflection = new \ReflectionClass(PermissionHelper::class);
|
|
$prop = $reflection->getProperty('permissionCache');
|
|
$prop->setAccessible(true);
|
|
$prop->setValue(null, []);
|
|
|
|
// PermissionMiddleware guest role 캐시 초기화
|
|
PermissionMiddleware::clearGuestRoleCache();
|
|
|
|
// 테스트용 admin 권한 생성
|
|
$this->adminPermission = Permission::create([
|
|
'identifier' => 'test.permission',
|
|
'name' => ['ko' => '테스트 관리자 권한', 'en' => 'Test Admin Permission'],
|
|
'description' => ['ko' => '테스트용 관리자 권한', 'en' => 'Test admin permission'],
|
|
'type' => PermissionType::Admin,
|
|
]);
|
|
|
|
// 테스트용 user 권한 생성 (같은 identifier로 user 타입)
|
|
$this->userPermission = Permission::create([
|
|
'identifier' => 'test.user.permission',
|
|
'name' => ['ko' => '테스트 사용자 권한', 'en' => 'Test User Permission'],
|
|
'description' => ['ko' => '테스트용 사용자 권한', 'en' => 'Test user permission'],
|
|
'type' => PermissionType::User,
|
|
]);
|
|
|
|
// 테스트용 추가 admin 권한 생성
|
|
$this->additionalAdminPermission = Permission::create([
|
|
'identifier' => 'test.additional',
|
|
'name' => ['ko' => '추가 관리자 권한', 'en' => 'Additional Admin Permission'],
|
|
'description' => ['ko' => '추가 테스트용 관리자 권한', 'en' => 'Additional admin test permission'],
|
|
'type' => PermissionType::Admin,
|
|
]);
|
|
|
|
// 테스트용 admin 역할 생성
|
|
$this->adminRole = Role::create([
|
|
'identifier' => 'test_admin_role',
|
|
'name' => ['ko' => '테스트 관리자 역할', 'en' => 'Test Admin Role'],
|
|
'description' => ['ko' => '테스트용 관리자 역할', 'en' => 'Test admin role'],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// 테스트용 user 역할 생성
|
|
$this->userRole = Role::create([
|
|
'identifier' => 'test_user_role',
|
|
'name' => ['ko' => '테스트 사용자 역할', 'en' => 'Test User Role'],
|
|
'description' => ['ko' => '테스트용 사용자 역할', 'en' => 'Test user role'],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// 역할에 권한 할당
|
|
$this->adminRole->permissions()->attach([$this->adminPermission->id, $this->additionalAdminPermission->id]);
|
|
$this->userRole->permissions()->attach([$this->userPermission->id]);
|
|
|
|
// admin 권한이 있는 사용자 생성
|
|
$this->userWithAdminPermission = User::factory()->create();
|
|
$this->userWithAdminPermission->roles()->attach($this->adminRole->id);
|
|
|
|
// user 권한이 있는 사용자 생성
|
|
$this->userWithUserPermission = User::factory()->create();
|
|
$this->userWithUserPermission->roles()->attach($this->userRole->id);
|
|
|
|
// 권한이 없는 사용자 생성
|
|
$this->userWithoutPermission = User::factory()->create();
|
|
|
|
// 테스트 라우트 등록 (새 형식: permission:type,identifier)
|
|
Route::middleware(['api', 'permission:admin,test.permission'])->get('/api/test-admin-permission', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
|
|
Route::middleware(['api', 'permission:user,test.user.permission'])->get('/api/test-user-permission', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
|
|
Route::middleware(['api', 'permission:admin,test.permission|test.additional'])->get('/api/test-multiple-permissions', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
|
|
Route::middleware(['api', 'permission:admin,test.permission|test.additional,false'])->get('/api/test-or-permissions', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* admin 권한이 있는 사용자는 admin 권한 체크 라우트에 정상 접근할 수 있어야 합니다.
|
|
*/
|
|
public function test_user_with_admin_permission_can_access_admin_route(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-admin-permission');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Access granted']);
|
|
}
|
|
|
|
/**
|
|
* user 권한만 있는 사용자는 admin 권한 체크 라우트에서 403 응답을 받아야 합니다.
|
|
* (같은 identifier라도 type이 다르면 접근 불가)
|
|
*/
|
|
public function test_user_with_user_permission_cannot_access_admin_route(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithUserPermission)
|
|
->getJson('/api/test-admin-permission');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* user 권한이 있는 사용자는 user 권한 체크 라우트에 정상 접근할 수 있어야 합니다.
|
|
*/
|
|
public function test_user_with_user_permission_can_access_user_route(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithUserPermission)
|
|
->getJson('/api/test-user-permission');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Access granted']);
|
|
}
|
|
|
|
/**
|
|
* admin 권한만 있는 사용자는 user 권한 체크 라우트에서 403 응답을 받아야 합니다.
|
|
*/
|
|
public function test_user_with_admin_permission_cannot_access_user_route(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-user-permission');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* 권한이 없는 사용자는 403 응답을 받아야 합니다.
|
|
*/
|
|
public function test_user_without_permission_receives_403(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithoutPermission)
|
|
->getJson('/api/test-admin-permission');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* 인증되지 않은 사용자는 401 응답을 받아야 합니다.
|
|
*/
|
|
public function test_unauthenticated_user_receives_401(): void
|
|
{
|
|
$response = $this->getJson('/api/test-admin-permission');
|
|
|
|
$response->assertStatus(401)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* 유효하지 않은 권한 타입으로 접근 시 403 응답을 받아야 합니다.
|
|
*/
|
|
public function test_invalid_permission_type_receives_403(): void
|
|
{
|
|
Route::middleware(['api', 'permission:invalid,test.permission'])->get('/api/test-invalid-type', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-invalid-type');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* 다중 권한 (AND 로직) - 모든 권한을 가진 경우 접근 가능해야 합니다.
|
|
*/
|
|
public function test_multiple_permissions_with_and_logic_grants_access(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-multiple-permissions');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Access granted']);
|
|
}
|
|
|
|
/**
|
|
* 다중 권한 (AND 로직) - 하나라도 권한이 없으면 403 응답을 받아야 합니다.
|
|
*/
|
|
public function test_multiple_permissions_with_and_logic_denies_without_all(): void
|
|
{
|
|
// 하나의 권한만 가진 역할 생성
|
|
$partialRole = Role::create([
|
|
'identifier' => 'partial_role',
|
|
'name' => ['ko' => '일부 권한 역할', 'en' => 'Partial Role'],
|
|
'description' => ['ko' => '일부 권한만 가진 역할', 'en' => 'Role with partial permissions'],
|
|
'is_active' => true,
|
|
]);
|
|
$partialRole->permissions()->attach($this->adminPermission->id);
|
|
|
|
/** @var User $partialUser */
|
|
$partialUser = User::factory()->create();
|
|
$partialUser->roles()->attach($partialRole->id);
|
|
|
|
$response = $this->actingAs($partialUser)
|
|
->getJson('/api/test-multiple-permissions');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* 다중 권한 (OR 로직) - 하나의 권한만 있어도 접근 가능해야 합니다.
|
|
*/
|
|
public function test_multiple_permissions_with_or_logic_grants_access(): void
|
|
{
|
|
// 하나의 권한만 가진 역할 생성
|
|
$singlePermRole = Role::create([
|
|
'identifier' => 'single_perm_role',
|
|
'name' => ['ko' => '단일 권한 역할', 'en' => 'Single Permission Role'],
|
|
'description' => ['ko' => '하나의 권한만 가진 역할', 'en' => 'Role with single permission'],
|
|
'is_active' => true,
|
|
]);
|
|
$singlePermRole->permissions()->attach($this->adminPermission->id);
|
|
|
|
/** @var User $singlePermUser */
|
|
$singlePermUser = User::factory()->create();
|
|
$singlePermUser->roles()->attach($singlePermRole->id);
|
|
|
|
$response = $this->actingAs($singlePermUser)
|
|
->getJson('/api/test-or-permissions');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Access granted']);
|
|
}
|
|
|
|
/**
|
|
* 다중 권한 (OR 로직) - 어떤 권한도 없으면 403 응답을 받아야 합니다.
|
|
*/
|
|
public function test_multiple_permissions_with_or_logic_denies_without_any(): void
|
|
{
|
|
$response = $this->actingAs($this->userWithoutPermission)
|
|
->getJson('/api/test-or-permissions');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* 역할을 통한 권한 확인이 정상 동작해야 합니다 (type 포함).
|
|
*/
|
|
public function test_permission_check_through_role_with_type(): void
|
|
{
|
|
// admin 권한 체크
|
|
$this->assertTrue($this->userWithAdminPermission->hasPermission('test.permission', PermissionType::Admin));
|
|
$this->assertFalse($this->userWithAdminPermission->hasPermission('test.permission', PermissionType::User));
|
|
|
|
// user 권한 체크
|
|
$this->assertTrue($this->userWithUserPermission->hasPermission('test.user.permission', PermissionType::User));
|
|
$this->assertFalse($this->userWithUserPermission->hasPermission('test.user.permission', PermissionType::Admin));
|
|
|
|
// type 없이 호출하면 어떤 타입이든 확인
|
|
$this->assertTrue($this->userWithAdminPermission->hasPermission('test.permission'));
|
|
$this->assertTrue($this->userWithUserPermission->hasPermission('test.user.permission'));
|
|
$this->assertFalse($this->userWithoutPermission->hasPermission('test.permission'));
|
|
}
|
|
|
|
/**
|
|
* 존재하지 않는 권한으로 접근 시 403 응답을 받아야 합니다.
|
|
*/
|
|
public function test_nonexistent_permission_denies_access(): void
|
|
{
|
|
Route::middleware(['api', 'permission:admin,nonexistent.permission'])->get('/api/test-nonexistent', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-nonexistent');
|
|
|
|
$response->assertStatus(403)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
/**
|
|
* hasPermissions 메서드가 type과 함께 정상 동작해야 합니다.
|
|
*/
|
|
public function test_has_permissions_with_type(): void
|
|
{
|
|
// admin 권한 여러 개 체크 (AND)
|
|
$this->assertTrue(
|
|
$this->userWithAdminPermission->hasPermissions(
|
|
['test.permission', 'test.additional'],
|
|
true,
|
|
PermissionType::Admin
|
|
)
|
|
);
|
|
|
|
// user 타입으로 체크하면 실패
|
|
$this->assertFalse(
|
|
$this->userWithAdminPermission->hasPermissions(
|
|
['test.permission', 'test.additional'],
|
|
true,
|
|
PermissionType::User
|
|
)
|
|
);
|
|
|
|
// OR 로직으로 체크
|
|
$this->assertTrue(
|
|
$this->userWithAdminPermission->hasPermissions(
|
|
['test.permission', 'nonexistent.permission'],
|
|
false,
|
|
PermissionType::Admin
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Bearer 토큰을 통한 인증이 정상 동작해야 합니다.
|
|
*/
|
|
public function test_authenticates_user_via_bearer_token(): void
|
|
{
|
|
// 라우트 등록 (optional.sanctum이 Bearer 토큰을 파싱하고 인증 처리)
|
|
Route::middleware(['api', 'optional.sanctum', 'permission:user,test.user.permission'])->get('/api/test-bearer-auth', function () {
|
|
return response()->json(['message' => 'Authenticated']);
|
|
});
|
|
|
|
// 토큰 생성
|
|
$token = $this->userWithUserPermission->createToken('test-token')->plainTextToken;
|
|
|
|
// Bearer 토큰으로 요청
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
])->getJson('/api/test-bearer-auth');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Authenticated']);
|
|
}
|
|
|
|
/**
|
|
* guest role에 권한이 부여된 경우 비회원도 접근할 수 있어야 합니다.
|
|
*/
|
|
public function test_guest_with_permission_can_access(): void
|
|
{
|
|
// guest role 생성
|
|
$guestRole = Role::create([
|
|
'identifier' => 'guest',
|
|
'name' => ['ko' => '비회원', 'en' => 'Guest'],
|
|
'description' => ['ko' => '비회원 역할', 'en' => 'Guest role'],
|
|
'extension_type' => ExtensionOwnerType::Core,
|
|
'extension_identifier' => 'core',
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// public 권한 생성
|
|
$publicPermission = Permission::create([
|
|
'identifier' => 'test.public',
|
|
'name' => ['ko' => '공개 권한', 'en' => 'Public Permission'],
|
|
'description' => ['ko' => '공개 권한', 'en' => 'Public permission'],
|
|
'type' => PermissionType::User,
|
|
]);
|
|
|
|
$guestRole->permissions()->attach($publicPermission->id);
|
|
|
|
// 라우트 등록
|
|
Route::middleware(['api', 'permission:user,test.public'])->get('/api/test-guest-access', function () {
|
|
return response()->json(['message' => 'Public access']);
|
|
});
|
|
|
|
// 인증 없이 요청
|
|
$response = $this->getJson('/api/test-guest-access');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Public access']);
|
|
}
|
|
|
|
/**
|
|
* guest role에 권한이 없는 경우 비회원은 401 응답을 받아야 합니다.
|
|
*/
|
|
public function test_guest_without_permission_receives_401(): void
|
|
{
|
|
// guest role 생성 (권한 없음)
|
|
Role::create([
|
|
'identifier' => 'guest',
|
|
'name' => ['ko' => '비회원', 'en' => 'Guest'],
|
|
'description' => ['ko' => '비회원 역할', 'en' => 'Guest role'],
|
|
'extension_type' => ExtensionOwnerType::Core,
|
|
'extension_identifier' => 'core',
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// 라우트 등록
|
|
Route::middleware(['api', 'permission:user,test.private'])->get('/api/test-guest-denied', function () {
|
|
return response()->json(['message' => 'Access granted']);
|
|
});
|
|
|
|
// 인증 없이 요청
|
|
$response = $this->getJson('/api/test-guest-denied');
|
|
|
|
$response->assertStatus(401)
|
|
->assertJsonStructure(['success', 'message']);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 동적 파라미터 치환 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 동적 파라미터 치환이 정상 동작해야 합니다.
|
|
*/
|
|
public function test_resolves_dynamic_permission_parameters(): void
|
|
{
|
|
// 동적 권한 생성
|
|
$dynamicPermission = Permission::create([
|
|
'identifier' => 'test-module.notice.posts.create',
|
|
'name' => ['ko' => '공지 게시글 생성', 'en' => 'Create Notice Posts'],
|
|
'description' => ['ko' => '공지 게시글 생성 권한', 'en' => 'Permission to create notice posts'],
|
|
'type' => PermissionType::User,
|
|
]);
|
|
|
|
$this->userRole->permissions()->attach($dynamicPermission->id);
|
|
|
|
// 동적 라우트 등록 (optional.sanctum이 Bearer 토큰을 파싱하고 인증 처리)
|
|
Route::middleware(['api', 'optional.sanctum', 'permission:user,test-module.{slug}.posts.create'])->post('/api/boards/{slug}/posts', function () {
|
|
return response()->json(['message' => 'Post created']);
|
|
});
|
|
|
|
// 토큰 생성
|
|
$token = $this->userWithUserPermission->createToken('test-token')->plainTextToken;
|
|
|
|
// 동적 파라미터가 포함된 요청
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
])->postJson('/api/boards/notice/posts', ['title' => 'Test']);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['message' => 'Post created']);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type 기반 상세 접근 체크 — User (owner_key='id')
|
|
// ========================================================================
|
|
|
|
/**
|
|
* scope=null — 타인 사용자 상세 접근 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_null_allows_access_to_other_user(): void
|
|
{
|
|
// 권한에 resource_route_key, owner_key 설정
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
// scope_type=null (전체 접근) 설정
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => null],
|
|
]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-user/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK', 'user_id' => $user->id]);
|
|
});
|
|
|
|
// 타인 사용자 접근 → 200 (전체 접근)
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-user/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=self — 자기 자신 상세 접근 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_self_allows_access_to_own_user(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-self-user/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// 자기 자신 접근 → 200
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-self-user/'.$this->userWithAdminPermission->uuid);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=self — 타인 사용자 상세 접근 시 403 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_self_denies_access_to_other_user(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-self-deny/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// 타인 사용자 접근 → 403
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-self-deny/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* scope=role — 동일 역할 사용자 상세 접근 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_allows_access_to_same_role_user(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Role],
|
|
]);
|
|
|
|
// 동일 역할 사용자 생성
|
|
/** @var User $sameRoleUser */
|
|
$sameRoleUser = User::factory()->create();
|
|
$sameRoleUser->roles()->attach($this->adminRole->id);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-role-user/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// 동일 역할 사용자 접근 → 200
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-role-user/'.$sameRoleUser->uuid);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=role — 다른 역할 사용자 상세 접근 시 403 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_denies_access_to_different_role_user(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Role],
|
|
]);
|
|
|
|
// 다른 역할 사용자 (역할 없는 사용자)
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-role-deny/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// 역할이 다른 사용자 접근 → 403
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-role-deny/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type 기반 상세 접근 체크 — Menu (owner_key='created_by')
|
|
// ========================================================================
|
|
|
|
/**
|
|
* scope=null — 타인이 만든 메뉴 수정 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_null_allows_access_to_other_menu(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'menu',
|
|
'owner_key' => 'created_by',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => null],
|
|
]);
|
|
|
|
$menu = $this->createMenu(['created_by' => $this->userWithoutPermission->id]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->put('/api/test-scope-menu/{menu}', function (Menu $menu) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->putJson('/api/test-scope-menu/'.$menu->id);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=self — 자기가 만든 메뉴 수정 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_self_allows_access_to_own_menu(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'menu',
|
|
'owner_key' => 'created_by',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
$menu = $this->createMenu(['created_by' => $this->userWithAdminPermission->id]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->put('/api/test-scope-self-menu/{menu}', function (Menu $menu) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->putJson('/api/test-scope-self-menu/'.$menu->id);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=self — 타인이 만든 메뉴 수정 시 403 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_self_denies_access_to_other_menu(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'menu',
|
|
'owner_key' => 'created_by',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
$menu = $this->createMenu(['created_by' => $this->userWithoutPermission->id]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->put('/api/test-scope-self-menu-deny/{menu}', function (Menu $menu) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->putJson('/api/test-scope-self-menu-deny/'.$menu->id);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* scope=role — 동일 역할 사용자가 만든 메뉴 수정 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_allows_access_to_same_role_menu(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'menu',
|
|
'owner_key' => 'created_by',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Role],
|
|
]);
|
|
|
|
/** @var User $sameRoleUser */
|
|
$sameRoleUser = User::factory()->create();
|
|
$sameRoleUser->roles()->attach($this->adminRole->id);
|
|
|
|
$menu = $this->createMenu(['created_by' => $sameRoleUser->id]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->put('/api/test-scope-role-menu/{menu}', function (Menu $menu) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->putJson('/api/test-scope-role-menu/'.$menu->id);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=role — 다른 역할 사용자가 만든 메뉴 수정 시 403 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_denies_access_to_different_role_menu(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'menu',
|
|
'owner_key' => 'created_by',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Role],
|
|
]);
|
|
|
|
// 다른 역할 사용자가 만든 메뉴
|
|
$menu = $this->createMenu(['created_by' => $this->userWithoutPermission->id]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->put('/api/test-scope-role-menu-deny/{menu}', function (Menu $menu) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->putJson('/api/test-scope-role-menu-deny/'.$menu->id);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type — resource_route_key=null 리소스 (스코프 체크 스킵)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* resource_route_key=null — scope 설정과 무관하게 권한만 있으면 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_skipped_when_no_resource_route_key(): void
|
|
{
|
|
// resource_route_key/owner_key가 null인 권한 (시스템 리소스)
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => null,
|
|
'owner_key' => null,
|
|
]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-no-route-key', function () {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-no-route-key');
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type — 목록 엔드포인트 (모델 바인딩 없음 → 스코프 체크 스킵)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 목록 엔드포인트 — scope=self 설정이어도 모델 바인딩 없으면 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_skipped_for_list_endpoint(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
// 모델 바인딩 없는 목록 엔드포인트
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-list', function () {
|
|
return response()->json(['message' => 'List OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-list');
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type — union 정책 테스트 (복수 역할)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* union 정책 — 역할A(self) + 역할B(role) → role 적용 → 동일 역할 리소스 접근 200이어야 합니다.
|
|
*/
|
|
public function test_union_policy_self_plus_role_uses_role(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
// 역할A에 scope_type='self' 설정
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
// 역할B 생성 후 scope_type='role' 설정
|
|
$roleB = Role::create([
|
|
'identifier' => 'test_role_b',
|
|
'name' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'description' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'is_active' => true,
|
|
]);
|
|
$roleB->permissions()->attach($this->adminPermission->id, ['scope_type' => ScopeType::Role]);
|
|
|
|
// 사용자에 두 역할 모두 할당
|
|
$this->userWithAdminPermission->roles()->attach($roleB->id);
|
|
|
|
// 동일 역할 사용자 생성
|
|
/** @var User $sameRoleUser */
|
|
$sameRoleUser = User::factory()->create();
|
|
$sameRoleUser->roles()->attach($this->adminRole->id);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-union-self-role/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// union: self + role → role → 동일 역할 사용자 접근 200
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-union-self-role/'.$sameRoleUser->uuid);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* union 정책 — 역할A(role) + 역할B(null) → null 적용 → 타인 리소스 접근 200이어야 합니다.
|
|
*/
|
|
public function test_union_policy_role_plus_null_uses_null(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
// 역할A에 scope_type='role' 설정
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Role],
|
|
]);
|
|
|
|
// 역할B 생성 후 scope_type=null (전체) 설정
|
|
$roleB = Role::create([
|
|
'identifier' => 'test_role_b_null',
|
|
'name' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'description' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'is_active' => true,
|
|
]);
|
|
$roleB->permissions()->attach($this->adminPermission->id, ['scope_type' => null]);
|
|
|
|
$this->userWithAdminPermission->roles()->attach($roleB->id);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-union-role-null/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// union: role + null → null → 아무나 접근 200
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-union-role-null/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* union 정책 — 역할A(self) + 역할B(self) → self 유지 → 타인 리소스 접근 403이어야 합니다.
|
|
*/
|
|
public function test_union_policy_self_plus_self_remains_self(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
// 역할B도 self
|
|
$roleB = Role::create([
|
|
'identifier' => 'test_role_b_self',
|
|
'name' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'description' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'is_active' => true,
|
|
]);
|
|
$roleB->permissions()->attach($this->adminPermission->id, ['scope_type' => ScopeType::Self]);
|
|
|
|
$this->userWithAdminPermission->roles()->attach($roleB->id);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-union-self-self/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// union: self + self → self → 타인 접근 403
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-union-self-self/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* union 정책 — 역할A(self) + 역할B(null) → null 적용 → 타인 리소스 접근 200이어야 합니다.
|
|
*/
|
|
public function test_union_policy_self_plus_null_uses_null(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Self],
|
|
]);
|
|
|
|
$roleB = Role::create([
|
|
'identifier' => 'test_role_b_null2',
|
|
'name' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'description' => ['ko' => '역할B', 'en' => 'Role B'],
|
|
'is_active' => true,
|
|
]);
|
|
$roleB->permissions()->attach($this->adminPermission->id, ['scope_type' => null]);
|
|
|
|
$this->userWithAdminPermission->roles()->attach($roleB->id);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-union-self-null/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// union: self + null → null → 200
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-union-self-null/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type — 권한 미보유 시 scope 도달 전 403
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 권한 미보유 시 — scope 체크 도달 전 권한 체크에서 403이어야 합니다.
|
|
*/
|
|
public function test_scope_not_reached_without_permission(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'user',
|
|
'owner_key' => 'id',
|
|
]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-no-perm/{user}', function (User $user) {
|
|
return response()->json(['message' => 'OK']);
|
|
});
|
|
|
|
// 권한 없는 사용자 → 권한 체크에서 403 (scope 미도달)
|
|
$response = $this->actingAs($this->userWithoutPermission)
|
|
->getJson('/api/test-scope-no-perm/'.$this->userWithoutPermission->uuid);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type 기반 상세 접근 체크 — Schedule (owner_key='created_by')
|
|
// ========================================================================
|
|
|
|
/**
|
|
* scope=null — 타인이 만든 스케줄 수정 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_null_allows_access_to_other_schedule(): void
|
|
{
|
|
$this->setupScopePermission('schedule', 'created_by', null);
|
|
$schedule = $this->createSchedule(['created_by' => $this->userWithoutPermission->id]);
|
|
$path = $this->registerScopeRoute('schedule', 'null-schedule', Schedule::class);
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson("{$path}/{$schedule->id}");
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=self — 자기가 만든 스케줄 수정 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_self_allows_access_to_own_schedule(): void
|
|
{
|
|
$this->setupScopePermission('schedule', 'created_by', ScopeType::Self);
|
|
$schedule = $this->createSchedule(['created_by' => $this->userWithAdminPermission->id]);
|
|
$path = $this->registerScopeRoute('schedule', 'self-own-schedule', Schedule::class);
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson("{$path}/{$schedule->id}");
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=self — 타인이 만든 스케줄 수정 시 403 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_self_denies_access_to_other_schedule(): void
|
|
{
|
|
$this->setupScopePermission('schedule', 'created_by', ScopeType::Self);
|
|
$schedule = $this->createSchedule(['created_by' => $this->userWithoutPermission->id]);
|
|
$path = $this->registerScopeRoute('schedule', 'self-deny-schedule', Schedule::class);
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson("{$path}/{$schedule->id}");
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* scope=role — 동일 역할 사용자가 만든 스케줄 수정 시 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_allows_access_to_same_role_schedule(): void
|
|
{
|
|
$this->setupScopePermission('schedule', 'created_by', ScopeType::Role);
|
|
|
|
/** @var User $sameRoleUser */
|
|
$sameRoleUser = User::factory()->create();
|
|
$sameRoleUser->roles()->attach($this->adminRole->id);
|
|
|
|
$schedule = $this->createSchedule(['created_by' => $sameRoleUser->id]);
|
|
$path = $this->registerScopeRoute('schedule', 'role-schedule', Schedule::class);
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson("{$path}/{$schedule->id}");
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* scope=role — 다른 역할 사용자가 만든 스케줄 수정 시 403 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_denies_access_to_different_role_schedule(): void
|
|
{
|
|
$this->setupScopePermission('schedule', 'created_by', ScopeType::Role);
|
|
$schedule = $this->createSchedule(['created_by' => $this->userWithoutPermission->id]);
|
|
$path = $this->registerScopeRoute('schedule', 'role-deny-schedule', Schedule::class);
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson("{$path}/{$schedule->id}");
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
// ========================================================================
|
|
// scope_type — 추가 목록 엔드포인트 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* scope=role — 목록 엔드포인트에서 모델 바인딩 없으면 200 응답이어야 합니다.
|
|
*/
|
|
public function test_scope_role_skipped_for_list_endpoint(): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => 'product',
|
|
'owner_key' => 'created_by',
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => ScopeType::Role],
|
|
]);
|
|
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get('/api/test-scope-role-list', function () {
|
|
return response()->json(['message' => 'List OK']);
|
|
});
|
|
|
|
$response = $this->actingAs($this->userWithAdminPermission)
|
|
->getJson('/api/test-scope-role-list');
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 헬퍼 메서드
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 테스트용 Menu 생성 헬퍼 (factory의 module_id 컬럼 이슈 회피)
|
|
*
|
|
* @param array $overrides 오버라이드할 필드
|
|
* @return Menu 생성된 메뉴
|
|
*/
|
|
private function createMenu(array $overrides = []): Menu
|
|
{
|
|
return Menu::create(array_merge([
|
|
'name' => ['ko' => '테스트 메뉴', 'en' => 'Test Menu'],
|
|
'slug' => 'test-menu-'.uniqid(),
|
|
'url' => '/admin/test',
|
|
'icon' => 'fas fa-cog',
|
|
'order' => 1,
|
|
'is_active' => true,
|
|
], $overrides));
|
|
}
|
|
|
|
/**
|
|
* 테스트용 Schedule 생성 헬퍼
|
|
*
|
|
* @param array $overrides 오버라이드할 필드
|
|
* @return Schedule 생성된 스케줄
|
|
*/
|
|
private function createSchedule(array $overrides = []): Schedule
|
|
{
|
|
return Schedule::create(array_merge([
|
|
'name' => 'Test Schedule '.uniqid(),
|
|
'type' => ScheduleType::Artisan,
|
|
'command' => 'test:command',
|
|
'expression' => '* * * * *',
|
|
'frequency' => 'everyMinute',
|
|
], $overrides));
|
|
}
|
|
|
|
/**
|
|
* 테스트용 권한에 scope 설정 헬퍼
|
|
*
|
|
* @param string $routeKey resource_route_key 값
|
|
* @param string $ownerKey owner_key 값
|
|
* @param ScopeType|null $scopeType scope_type 값
|
|
* @return void
|
|
*/
|
|
private function setupScopePermission(string $routeKey, string $ownerKey, ?ScopeType $scopeType): void
|
|
{
|
|
$this->adminPermission->update([
|
|
'resource_route_key' => $routeKey,
|
|
'owner_key' => $ownerKey,
|
|
]);
|
|
|
|
$this->adminRole->permissions()->syncWithoutDetaching([
|
|
$this->adminPermission->id => ['scope_type' => $scopeType],
|
|
]);
|
|
|
|
// static 캐시 초기화 (permission 변경 반영)
|
|
$reflection = new \ReflectionClass(PermissionHelper::class);
|
|
$prop = $reflection->getProperty('permissionCache');
|
|
$prop->setAccessible(true);
|
|
$prop->setValue(null, []);
|
|
}
|
|
|
|
/**
|
|
* 모델 바인딩 포함 테스트 라우트 등록 헬퍼
|
|
*
|
|
* @param string $routeKey 라우트 파라미터명
|
|
* @param string $suffix 라우트 경로 구분용 접미사
|
|
* @param string $modelClass 바인딩할 모델 FQCN
|
|
* @return string 등록된 라우트 경로 (파라미터 제외)
|
|
*/
|
|
private function registerScopeRoute(string $routeKey, string $suffix, string $modelClass): string
|
|
{
|
|
// 명시적 라우트 모델 바인딩 (모듈 모델 클래스 해석 보장)
|
|
Route::bind($routeKey, fn ($value) => $modelClass::findOrFail($value));
|
|
|
|
$path = "/api/test-scope-{$suffix}";
|
|
Route::middleware(['api', 'auth:sanctum', 'permission:admin,test.permission'])
|
|
->get("{$path}/{{$routeKey}}", fn (Model $model) => response()->json(['message' => 'OK']));
|
|
|
|
return $path;
|
|
}
|
|
}
|