검색 — 이 코어에 추가한 BOOLEAN MODE 정제기가 게시판에는 도달하지 않아 원문 키워드가 그대로 바인딩되고 있었다. + - * " < > 를 넣으면 오류 화면이 떴다. 코어 정제기 경유로 옮겼고, 같은 술어를 count 와 get 으로 두 번 실행하던 것을 페이지네이터 한 번으로 합쳤다. 구체 엔진 직접 호출도 해석기 경유로 바꿔 플러그인 검색엔진이 게시글·신고 검색에 적용된다. 상세 — 같은 글을 3회, 같은 게시판을 5회 읽고 있었다. 조회한 인스턴스를 넘겨 글 1회·게시판 1회로 줄였고, 조회수 증가를 권한 확인 뒤로 옮겨 열람 권한이 없는 요청이 조회수를 올리지 않게 했다. 목록 — 답변 트리의 무제한 조회에 상한과 순환 가드를 두고, O(n²) 병합을 한 패스로 바꿨다. 댓글도 무제한 조회에 상한을 두되 잘렸다는 사실과 전체 건수를 함께 알린다. 조용히 잘리면 "댓글이 그만큼뿐" 으로 보인다. 원댓글 단위 페이지네이션을 더해 답글이 부모와 떨어지지 않게 했다. 관리자 화면 — 총 건수가 잘린 목록에서 페이저가 "1페이지뿐" 으로 접히던 자리를 고쳤다. 저장소가 상한을 붙인 목록인데 화면이 그 값을 1 로 채우고 있어, 기록이 상한을 넘으면 2페이지에 도달할 방법이 없었다. 같은 파일에서 드러난 사전 결함도 함께 처리했다 — 반복 렌더 안의 정적 HTML id, Icon 의 박스 크기 지정, 단색 버튼의 다크 모드 대비.
338 lines
11 KiB
PHP
338 lines
11 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);
|
|
}
|
|
}
|