7.0.7 페이로드 전수 검증에서 발견된 error 4건·warn 14건을 릴리즈 전에 처리했다. 미출시 분량의 자체 결함(회원 통합검색 500, 관리자 스코프 403 의 500 위장 등)이므로 페이로드에 합류시키고, 출시 대상 26개 CHANGELOG 의 버전 섹션 날짜를 2026-08-18 로 정렬했다. - 통합검색 import 복원 + 이커머스 관리자 typed 403 catch 대칭(테스트 동반) - admin 판정 스니핑 사설 사본 전수 수렴(BoardResource) + 조건 스캔 테스트 - 스케줄 인터프리터 변형 표기(확장자·버전 접미사) 정규화 차단 - ckeditor5 참조 판정 배치화·fail-safe, 결제 플러그인 진단 페이로드 대칭 - 템플릿 다크 대비·상태 Select value-제어 + --production 재빌드 - Upgrade_7_0_7 잔존 중복 색인 정리 스텝, API 문서·i18n(ko/en/ja) 동기
380 lines
12 KiB
PHP
380 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Unit\Traits;
|
|
|
|
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
|
|
require_once __DIR__.'/../../ModuleTestCase.php';
|
|
|
|
use App\Enums\PermissionType;
|
|
use App\Http\Middleware\PermissionMiddleware;
|
|
use App\Models\Permission;
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
|
|
use Modules\Sirsoft\Board\Traits\ChecksBoardPermission;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
|
|
/**
|
|
* ChecksBoardPermission Trait 테스트
|
|
*
|
|
* 게시판 권한 체크 로직을 테스트합니다.
|
|
* - 회원 권한 체크 (Gate)
|
|
* - 비회원 권한 체크 (guest role)
|
|
*/
|
|
class ChecksBoardPermissionTest extends ModuleTestCase
|
|
{
|
|
/**
|
|
* Trait을 사용하는 테스트용 클래스
|
|
*/
|
|
private object $traitObject;
|
|
|
|
/**
|
|
* 테스트 설정
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// PermissionMiddleware 정적 캐시 초기화
|
|
$this->resetPermissionMiddlewareCache();
|
|
|
|
// ChecksBoardPermission 요청 내 캐시 초기화
|
|
ChecksBoardPermission::clearPermissionCache();
|
|
|
|
// Trait을 사용하는 익명 클래스 생성
|
|
$this->traitObject = new class
|
|
{
|
|
use ChecksBoardPermission;
|
|
|
|
public function invokeCheckPermissionByIdentifier(string $identifier): bool
|
|
{
|
|
return $this->checkPermissionByIdentifier($identifier);
|
|
}
|
|
|
|
public function invokeCheckBoardPermission(string $slug, string $action, PermissionType $type = PermissionType::User): bool
|
|
{
|
|
return $this->checkBoardPermission($slug, $action, $type);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* PermissionMiddleware 정적 캐시 초기화
|
|
*/
|
|
private function resetPermissionMiddlewareCache(): void
|
|
{
|
|
PermissionMiddleware::clearGuestRoleCache();
|
|
}
|
|
|
|
/**
|
|
* 테스트 후 정리
|
|
*/
|
|
protected function tearDown(): void
|
|
{
|
|
Auth::logout();
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 회원이 권한을 보유한 경우 true 반환
|
|
*/
|
|
#[Test]
|
|
public function 회원_권한_보유_시_true_반환(): void
|
|
{
|
|
// Given: 권한을 가진 회원 생성
|
|
$user = User::factory()->create();
|
|
$role = Role::firstOrCreate(
|
|
['identifier' => 'user'],
|
|
['name' => ['ko' => '회원', 'en' => 'User'], 'is_active' => true]
|
|
);
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.read',
|
|
'type' => 'user',
|
|
]);
|
|
|
|
$role->permissions()->syncWithoutDetaching([$permission->id]);
|
|
$user->roles()->syncWithoutDetaching([$role->id]);
|
|
|
|
Auth::login($user);
|
|
// Unit 테스트에서 request()->user()가 Auth::login()을 반영하도록 설정
|
|
request()->setUserResolver(fn () => $user);
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.read');
|
|
|
|
// Then: true 반환
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
/**
|
|
* 회원이 권한을 보유하지 않은 경우 false 반환
|
|
*/
|
|
#[Test]
|
|
public function 회원_권한_미보유_시_false_반환(): void
|
|
{
|
|
// Given: 권한이 없는 회원 생성
|
|
$user = User::factory()->create();
|
|
$role = Role::firstOrCreate(
|
|
['identifier' => 'user'],
|
|
['name' => ['ko' => '회원', 'en' => 'User'], 'is_active' => true]
|
|
);
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.write',
|
|
'type' => 'user',
|
|
]);
|
|
|
|
$role->permissions()->syncWithoutDetaching([$permission->id]);
|
|
// 사용자에게 역할 할당하지 않음
|
|
|
|
Auth::login($user);
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.write');
|
|
|
|
// Then: false 반환
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* 비회원이 guest role 권한을 보유한 경우 true 반환
|
|
*/
|
|
#[Test]
|
|
public function 비회원_guest_role_권한_보유_시_true_반환(): void
|
|
{
|
|
// Given: guest role과 권한 생성
|
|
$guestRole = Role::firstOrCreate(
|
|
['identifier' => 'guest'],
|
|
['name' => ['ko' => '비회원', 'en' => 'Guest'], 'is_active' => true]
|
|
);
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.read',
|
|
'type' => 'user',
|
|
]);
|
|
|
|
$guestRole->permissions()->syncWithoutDetaching([$permission->id]);
|
|
|
|
// Auth::logout() 또는 Auth 없음 (비회원 상태)
|
|
Auth::logout();
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.read');
|
|
|
|
// Then: true 반환
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
/**
|
|
* 비회원이 guest role 권한을 보유하지 않은 경우 false 반환
|
|
*/
|
|
#[Test]
|
|
public function 비회원_guest_role_권한_미보유_시_false_반환(): void
|
|
{
|
|
// Given: guest role은 있지만 해당 권한은 없음
|
|
$guestRole = Role::firstOrCreate(
|
|
['identifier' => 'guest'],
|
|
['name' => ['ko' => '비회원', 'en' => 'Guest'], 'is_active' => true]
|
|
);
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.write',
|
|
'type' => 'user',
|
|
]);
|
|
|
|
$guestRole->permissions()->syncWithoutDetaching([$permission->id]);
|
|
|
|
Auth::logout();
|
|
|
|
// When: 다른 권한 체크 (guest role에 없는 권한)
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.delete');
|
|
|
|
// Then: false 반환
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* guest role이 없는 경우 false 반환
|
|
*/
|
|
#[Test]
|
|
public function guest_role_없으면_false_반환(): void
|
|
{
|
|
// Given: guest role이 아예 없음
|
|
Auth::logout();
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.read');
|
|
|
|
// Then: false 반환 (전체 허용도 없음)
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* 역할 할당이 없는 권한은 접근 거부 (false 반환)
|
|
*/
|
|
#[Test]
|
|
public function 역할_할당_없는_권한은_접근_거부(): void
|
|
{
|
|
// Given: 권한은 존재하지만 역할 할당이 없음
|
|
Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.read',
|
|
'type' => 'user',
|
|
]);
|
|
// role_permissions 테이블에 레코드 없음
|
|
|
|
Auth::logout();
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.read');
|
|
|
|
// Then: false 반환 (역할 할당 필수)
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* checkBoardPermission 메서드가 올바른 권한 식별자를 생성하는지 테스트
|
|
*/
|
|
#[Test]
|
|
public function check_board_permission_메서드_올바른_식별자_생성(): void
|
|
{
|
|
// Given: 권한 생성 + guest role에 할당
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.read',
|
|
'type' => 'user',
|
|
]);
|
|
$guestRole = Role::firstOrCreate(
|
|
['identifier' => 'guest'],
|
|
['name' => ['ko' => '비회원', 'en' => 'Guest'], 'is_active' => true]
|
|
);
|
|
$guestRole->permissions()->syncWithoutDetaching([$permission->id]);
|
|
|
|
Auth::logout();
|
|
|
|
// When: checkBoardPermission 호출
|
|
$result = $this->traitObject->invokeCheckBoardPermission('notice', 'posts.read');
|
|
|
|
// Then: 올바른 식별자로 권한 체크됨 (guest role 권한 보유)
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
/**
|
|
* 권한이 존재하지 않고 역할 할당도 없으면 false 반환
|
|
*/
|
|
#[Test]
|
|
public function 권한_미존재_시_false_반환(): void
|
|
{
|
|
// Given: 권한 자체가 존재하지 않음
|
|
Auth::logout();
|
|
|
|
// When: 존재하지 않는 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.nonexistent.permission');
|
|
|
|
// Then: false 반환
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* 회원 권한 체크 우선순위 테스트 (회원이 로그인하면 Gate 먼저 체크)
|
|
*/
|
|
#[Test]
|
|
public function 회원_로그인_시_gate_우선_체크(): void
|
|
{
|
|
// Given: guest role에도 권한 부여, 회원에게도 권한 부여
|
|
$guestRole = Role::firstOrCreate(
|
|
['identifier' => 'guest'],
|
|
['name' => ['ko' => '비회원', 'en' => 'Guest'], 'is_active' => true]
|
|
);
|
|
$userRole = Role::firstOrCreate(
|
|
['identifier' => 'user'],
|
|
['name' => ['ko' => '회원', 'en' => 'User'], 'is_active' => true]
|
|
);
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.read',
|
|
'type' => 'user',
|
|
]);
|
|
|
|
$guestRole->permissions()->syncWithoutDetaching([$permission->id]);
|
|
$userRole->permissions()->syncWithoutDetaching([$permission->id]);
|
|
|
|
$user = User::factory()->create();
|
|
$user->roles()->syncWithoutDetaching([$userRole->id]);
|
|
|
|
Auth::login($user);
|
|
// Unit 테스트에서 request()->user()가 Auth::login()을 반영하도록 설정
|
|
request()->setUserResolver(fn () => $user);
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.read');
|
|
|
|
// Then: true 반환 (회원 권한으로 체크됨)
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
/**
|
|
* 비회원은 guest role에 할당된 권한만 접근 가능
|
|
*/
|
|
#[Test]
|
|
public function 비회원_guest_role_미할당_권한은_접근_거부(): void
|
|
{
|
|
// Given: 권한 생성하지만 guest role에 할당하지 않음
|
|
$permission = Permission::factory()->create([
|
|
'identifier' => 'sirsoft-board.notice.posts.read',
|
|
'type' => 'user',
|
|
]);
|
|
|
|
// guest role 생성하지만 해당 권한 부여하지 않음
|
|
Role::firstOrCreate(
|
|
['identifier' => 'guest'],
|
|
['name' => ['ko' => '비회원', 'en' => 'Guest'], 'is_active' => true]
|
|
);
|
|
|
|
Auth::logout();
|
|
|
|
// When: 권한 체크
|
|
$result = $this->traitObject->invokeCheckPermissionByIdentifier('sirsoft-board.notice.posts.read');
|
|
|
|
// Then: false 반환 (guest role에 권한 미할당)
|
|
$this->assertFalse($result);
|
|
}
|
|
|
|
/**
|
|
* Admin 요청 판정(컨트롤러 네임스페이스 스니핑)은 트레이트 단일 지점이어야 한다
|
|
*
|
|
* 판정 규칙이 Resource/게이트마다 사설 복제되면 규칙 변경 시 한 곳만 누락되어
|
|
* 경로별 판정이 갈린다. 모듈 src 전체를 스캔하여 `getController()` 호출이
|
|
* ChecksBoardPermission 트레이트에만 존재함을 고정한다 (열거가 아닌 조건 스캔).
|
|
*/
|
|
#[Test]
|
|
public function admin_요청_판정은_트레이트_단일_지점이다(): void
|
|
{
|
|
$srcDir = dirname(__DIR__, 3).'/src';
|
|
$allowed = str_replace('\\', '/', realpath($srcDir.'/Traits/ChecksBoardPermission.php'));
|
|
$violations = [];
|
|
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($srcDir, \FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->getExtension() !== 'php') {
|
|
continue;
|
|
}
|
|
|
|
$path = str_replace('\\', '/', $file->getRealPath());
|
|
|
|
if ($path === $allowed) {
|
|
continue;
|
|
}
|
|
|
|
if (str_contains((string) file_get_contents($path), 'getController(')) {
|
|
$violations[] = $path;
|
|
}
|
|
}
|
|
|
|
$this->assertSame(
|
|
[],
|
|
$violations,
|
|
'Admin 요청 판정 사설 복제본 발견 — ChecksBoardPermission::isAdminRequest() 를 사용할 것: '
|
|
.implode(', ', $violations)
|
|
);
|
|
}
|
|
}
|