Files
HeuJung b3d0da9355 fix(release): 7.0.7 릴리즈 검증 발견 결함 전수 처리 및 출시일 8/18 정렬
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) 동기
2026-08-18 08:12:55 +09:00

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)
);
}
}