Files
Gnuboard7/modules/_bundled/sirsoft-board/tests/Feature/PostResourcePermissionTest.php
T
HeuJung 6505d85959 feat(board,ecommerce,template): 관리자↔유저 크로스 이동 링크 + 상품코드·게시판 슬러그 라우팅 전환
관리자·유저 화면 왕복 이동 수단을 추가하고, 상품/게시판 URL 식별자를
사람이 읽을 수 있는 값(상품코드/슬러그)으로 전환한다.

크로스 이동 링크 (전부 새 탭):
- 관리자 사이드바 "사이트 보기" + 모바일 드로어 상단 잘림 수정
- 관리자 게시물 조회 ↔ 유저 게시판, 유저 상품 상세 ↔ 관리자 상품 수정
- 유저 화면의 관리자 진입은 is_admin 이 아니라 세부 권한 플래그로 게이트
 (게시판 can_access_admin, 상품 can_update)

상품 라우팅 id → product_code:
- Product::resolveRouteBinding(code/id) + 공개 라우트 정규식 확장
- 모든 상품 링크(목록·최근본·검색·장바구니·주문내역·문의내역·관리자) 전환
- 상품 검색을 상품코드·번호 양쪽 조회 가능하게 확장(관리자+통합검색)

게시판 라우팅 board id → slug:
- Board::resolveRouteBinding(slug/id) + 관리자 라우트 slug 전환
- 생성 후 리다이렉트·토글·삭제·수정·add-to-menu·편집기 스코프 전수 정정
- BoardController::addToAdminMenu 를 Board 바인딩으로 정합

검증: 프론트 레이아웃 Vitest 36건 green, 백엔드 회귀 테스트 추가,
g7.dev 라이브 실측(생성/삭제/메뉴추가 slug, 문의내역·검색 상품코드 URL).
2026-07-07 10:40:22 +09:00

638 lines
22 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests\Feature;
// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요)
require_once __DIR__.'/../ModuleTestCase.php';
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
use PHPUnit\Framework\Attributes\Test;
/**
* PostResource 권한 API 응답 구조 테스트
*
* Admin/User 페이지별 권한 분기 및 소유권 플래그 테스트
*/
class PostResourcePermissionTest extends BoardTestCase
{
private User $adminUser;
private User $regularUser;
private User $postAuthor;
/**
* 테스트 게시판 slug
*/
protected function getTestBoardSlug(): string
{
return 'permission-test';
}
/**
* 기본 게시판 속성
*/
protected function getDefaultBoardAttributes(string $slug): array
{
return [
'slug' => $slug,
'name' => ['ko' => '권한 테스트 게시판', 'en' => 'Permission Test Board'],
'is_active' => true,
'secret_mode' => 'disabled',
'blocked_keywords' => [],
];
}
protected function setUp(): void
{
parent::setUp();
// 테스트 사용자 생성
$this->createTestUsers();
// 권한 설정
$this->setupPermissions();
}
/**
* 테스트 사용자 생성
*/
private function createTestUsers(): void
{
$this->adminUser = User::factory()->create();
$this->regularUser = User::factory()->create();
$this->postAuthor = User::factory()->create();
}
/**
* 권한 설정
*/
private function setupPermissions(): void
{
$slug = $this->board->slug;
// Admin 권한 생성 및 할당 (라우트 미들웨어와 일치하는 식별자 사용)
$adminPermissions = [];
$adminPermissionDefs = [
'admin.posts.read' => ['ko' => '관리자 게시글 조회', 'en' => 'Admin Posts Read'],
'admin.posts.write' => ['ko' => '관리자 게시글 작성', 'en' => 'Admin Posts Write'],
'admin.manage' => ['ko' => '관리자 관리', 'en' => 'Admin Manage'],
];
foreach ($adminPermissionDefs as $action => $name) {
$permission = Permission::firstOrCreate(
['identifier' => "sirsoft-board.{$slug}.{$action}"],
[
'name' => $name,
'slug' => "sirsoft-board.{$slug}.{$action}",
'type' => 'admin',
]
);
$adminPermissions[] = $permission->id;
}
$adminRole = Role::firstOrCreate(
['identifier' => "{$slug}-admin"],
['name' => ['ko' => '게시판 관리자', 'en' => 'Board Admin']]
);
$adminRole->permissions()->syncWithoutDetaching($adminPermissions);
$this->adminUser->roles()->attach($adminRole->id);
// User 권한 생성 및 할당 (라우트 미들웨어와 일치하는 식별자 사용)
$userPermissions = [];
$userPermissionDefs = [
'posts.read' => ['ko' => '게시글 조회', 'en' => 'Read Posts'],
'posts.write' => ['ko' => '게시글 작성', 'en' => 'Write Posts'],
'comments.read' => ['ko' => '댓글 조회', 'en' => 'Read Comments'],
'comments.write' => ['ko' => '댓글 작성', 'en' => 'Write Comments'],
];
foreach ($userPermissionDefs as $action => $name) {
$permission = Permission::firstOrCreate(
['identifier' => "sirsoft-board.{$slug}.{$action}"],
[
'name' => $name,
'slug' => "sirsoft-board.{$slug}.{$action}",
'type' => 'user',
]
);
$userPermissions[] = $permission->id;
}
$userRole = Role::firstOrCreate(
['identifier' => "{$slug}-user"],
['name' => ['ko' => '일반 사용자', 'en' => 'Regular User']]
);
$userRole->permissions()->syncWithoutDetaching($userPermissions);
$this->regularUser->roles()->attach($userRole->id);
$this->postAuthor->roles()->attach($userRole->id);
}
// ==========================================
// Admin 페이지 권한 테스트
// ==========================================
/**
* Admin 페이지에서는 admin_* 권한만 반환
*/
#[Test]
public function admin_page_returns_admin_permissions_only(): void
{
// Given: 회원 게시글 생성
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When: Admin API 호출
$response = $this->actingAs($this->adminUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/admin/board/{$this->board->slug}/posts/{$postId}");
// Then: admin 컨텍스트의 can_* abilities 포함
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
$this->assertArrayHasKey('can_read', $abilities);
$this->assertArrayHasKey('can_write', $abilities);
$this->assertArrayHasKey('can_manage', $abilities);
// Admin 컨텍스트이므로 admin.* 권한 기반으로 체크됨
// User 전용 키는 통합 can_* 키 사용으로 별도 존재하지 않음
$this->assertTrue($abilities['can_read']);
$this->assertTrue($abilities['can_write']);
$this->assertTrue($abilities['can_manage']);
}
/**
* Admin 페이지에서 User 권한 미반환
*/
#[Test]
public function admin_page_excludes_user_permissions(): void
{
// Given
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When
$response = $this->actingAs($this->adminUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/admin/board/{$this->board->slug}/posts/{$postId}");
// Then: abilities에 통합 can_* 키만 존재 (admin/user 구분 없음)
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
// 통합 키 사용으로 old-style 키는 존재하지 않음
$this->assertArrayNotHasKey('posts_read', $abilities);
$this->assertArrayNotHasKey('posts_write', $abilities);
$this->assertArrayNotHasKey('comments_read', $abilities);
$this->assertArrayNotHasKey('comments_write', $abilities);
}
// ==========================================
// User 페이지 권한 테스트
// ==========================================
/**
* User 페이지에서는 posts_*, comments_* 권한만 반환
*/
#[Test]
public function user_page_returns_user_permissions_only(): void
{
// Given
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When: User API 호출
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: User 컨텍스트의 can_* abilities 포함
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
$this->assertArrayHasKey('can_read', $abilities);
$this->assertArrayHasKey('can_write', $abilities);
$this->assertArrayHasKey('can_read_secret', $abilities);
$this->assertArrayHasKey('can_read_comments', $abilities);
$this->assertArrayHasKey('can_write_comments', $abilities);
// User 컨텍스트이므로 user.* 권한 기반으로 체크됨
$this->assertTrue($abilities['can_read']);
$this->assertTrue($abilities['can_write']);
$this->assertTrue($abilities['can_read_comments']);
$this->assertTrue($abilities['can_write_comments']);
}
/**
* User 페이지에서 Admin 권한 미반환
*/
#[Test]
public function user_page_excludes_admin_permissions(): void
{
// Given
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: abilities에 통합 can_* 키만 존재 (admin/user 구분 없음)
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
// 통합 키 사용으로 old-style admin 키는 존재하지 않음
$this->assertArrayNotHasKey('admin_posts_read', $abilities);
$this->assertArrayNotHasKey('admin_posts_write', $abilities);
$this->assertArrayNotHasKey('admin_manage', $abilities);
}
// ==========================================
// can_access_admin 게이트 테스트 (#450)
// 유저 화면에서 관리자 게시판 화면 진입 크로스링크 노출 여부
// ==========================================
/**
* User 상세: admin.manage(Admin 타입) 권한 보유자는 can_access_admin = true
*
* @scenario link=user_board_to_admin, permitted=true
* @effects board_admin_gate_true_shows_link
*/
#[Test]
public function user_detail_returns_can_access_admin_true_for_admin_permission_holder(): void
{
// Given: 게시글 + admin.manage 권한 보유 사용자
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When: 관리 권한 보유자가 유저 라우트로 상세 조회
$response = $this->actingAs($this->adminUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: 유저 컨텍스트에도 can_access_admin 노출 + true
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
$this->assertArrayHasKey('can_access_admin', $abilities);
$this->assertTrue($abilities['can_access_admin']);
}
/**
* User 상세: 일반 사용자(admin.manage 미보유)는 can_access_admin = false
*
* @scenario link=user_board_to_admin, permitted=false
* @effects board_admin_gate_false_hides_link
*/
#[Test]
public function user_detail_returns_can_access_admin_false_for_regular_user(): void
{
// Given
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When: user 권한만 보유한 사용자가 유저 라우트로 상세 조회
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: can_access_admin 은 노출되나 false
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
$this->assertArrayHasKey('can_access_admin', $abilities);
$this->assertFalse($abilities['can_access_admin']);
}
/**
* User 목록: 컬렉션 abilities 에도 can_access_admin 노출
*/
#[Test]
public function user_list_collection_exposes_can_access_admin(): void
{
// Given
$this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'List Post',
'content' => 'List Content',
]);
// When: 관리 권한 보유자가 유저 라우트로 목록 조회
$response = $this->actingAs($this->adminUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts");
// Then: 컬렉션 abilities 에 can_access_admin = true
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
$this->assertArrayHasKey('can_access_admin', $abilities);
$this->assertTrue($abilities['can_access_admin']);
}
/**
* User 목록/상세 응답의 board 정보에 id 가 포함된다 (#450)
*
* "게시판 관리" 크로스링크가 /admin/boards/:id/edit 로 이동하려면 board id 필요.
*/
#[Test]
public function user_response_board_info_includes_id(): void
{
// Given
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Board Id Post',
'content' => 'Board Id Content',
]);
// When: 목록
$listResponse = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts");
// And: 상세
$detailResponse = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: 목록/상세 board 정보 모두 실제 게시판 id 노출
$listResponse->assertStatus(200);
$this->assertSame($this->board->id, $listResponse->json('data.board.id'));
$detailResponse->assertStatus(200);
$this->assertSame($this->board->id, $detailResponse->json('data.board.id'));
}
// ==========================================
// 소유권 플래그 테스트
// ==========================================
/**
* 본인 글: is_author = true
*/
#[Test]
public function is_author_true_for_own_post(): void
{
// Given: 작성자가 작성한 게시글
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'My Post',
'content' => 'My Content',
]);
// When: 작성자 본인이 조회
$response = $this->actingAs($this->postAuthor, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then
$response->assertStatus(200);
$this->assertTrue($response->json('data.is_author'));
$this->assertFalse($response->json('data.is_guest_post'));
}
/**
* 타인 글: is_author = false
*/
#[Test]
public function is_author_false_for_others_post(): void
{
// Given: 다른 사용자가 작성한 게시글
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Others Post',
'content' => 'Others Content',
]);
// When: 다른 사용자가 조회
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then
$response->assertStatus(200);
$this->assertFalse($response->json('data.is_author'));
$this->assertFalse($response->json('data.is_guest_post'));
}
/**
* 비회원 글: is_guest_post = true
*/
#[Test]
public function is_guest_post_true_for_guest_post(): void
{
// Given: 비회원이 작성한 게시글
$postId = $this->createPost([
'user_id' => null,
'author_name' => 'Guest Author',
'title' => 'Guest Post',
'content' => 'Guest Content',
]);
// When: 로그인 사용자가 조회
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then
$response->assertStatus(200);
$this->assertFalse($response->json('data.is_author'));
$this->assertTrue($response->json('data.is_guest_post'));
}
/**
* 회원 글: is_guest_post = false
*/
#[Test]
public function is_guest_post_false_for_member_post(): void
{
// Given: 회원이 작성한 게시글
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Member Post',
'content' => 'Member Content',
]);
// When
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then
$response->assertStatus(200);
$this->assertFalse($response->json('data.is_guest_post'));
}
// ==========================================
// 비회원 권한 테스트
// ==========================================
/**
* 비회원: guest_permissions에 따라 권한 체크
*/
#[Test]
public function guest_permissions_respected_for_non_authenticated_user(): void
{
// Given: 비회원 게시글
$postId = $this->createPost([
'user_id' => null,
'author_name' => 'Guest',
'title' => 'Public Post',
'content' => 'Public Content',
]);
// When: 비로그인 상태로 조회
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: grantDefaultGuestPermissions()에서 posts.read, posts.write, attachments.upload 부여
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
$this->assertTrue($abilities['can_read']); // guest에 posts.read 있음
$this->assertTrue($abilities['can_write']); // guest에 posts.write 있음
$this->assertFalse($abilities['can_read_comments']); // guest에 comments.read 없음
$this->assertFalse($abilities['can_write_comments']); // guest에 comments.write 없음
}
/**
* 로그인 사용자: Gate로 권한 체크
*/
#[Test]
public function authenticated_user_permissions_check_via_gate(): void
{
// Given
$postId = $this->createPost([
'user_id' => $this->postAuthor->id,
'title' => 'Test Post',
'content' => 'Test Content',
]);
// When: 로그인 사용자 조회
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}");
// Then: Role-Permission으로 체크
$response->assertStatus(200);
$abilities = $response->json('data.abilities');
// regularUser는 posts.read, posts.write, comments.read, comments.write 권한 보유
$this->assertTrue($abilities['can_read']);
$this->assertTrue($abilities['can_write']);
$this->assertTrue($abilities['can_read_comments']);
$this->assertTrue($abilities['can_write_comments']);
}
// ==========================================
// 답글 섹션 삭제 게시글 필터링 테스트
// ==========================================
/**
* 일반 사용자는 답글 섹션에서 삭제된 답글을 볼 수 없다
*/
#[Test]
public function regular_user_cannot_see_deleted_reply_in_post_detail(): void
{
// Given: 원글 생성
$parentId = $this->createTestPost([
'user_id' => $this->postAuthor->id,
'title' => '원글',
'content' => '원글 내용',
]);
// 정상 답글 생성
$normalReplyId = $this->createTestPost([
'user_id' => $this->postAuthor->id,
'parent_id' => $parentId,
'title' => '정상 답글',
'content' => '정상 답글 내용',
'status' => 'published',
]);
// 삭제된 답글 생성 (status='deleted', deleted_at 설정)
$this->createTestPost([
'user_id' => $this->postAuthor->id,
'parent_id' => $parentId,
'title' => '삭제된 답글',
'content' => '삭제된 답글 내용',
'status' => 'deleted',
'deleted_at' => now(),
]);
// When: 일반 사용자가 원글 상세 조회
$response = $this->actingAs($this->regularUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$parentId}");
// Then: replies에 삭제된 답글이 포함되지 않음
$response->assertStatus(200);
$replies = $response->json('data.replies');
$this->assertCount(1, $replies);
$this->assertEquals($normalReplyId, $replies[0]['id']);
}
/**
* 관리자는 답글 섹션에서 삭제된 답글을 볼 수 있다
*/
#[Test]
public function admin_user_can_see_deleted_reply_in_post_detail(): void
{
// Given: 원글 생성
$parentId = $this->createTestPost([
'user_id' => $this->postAuthor->id,
'title' => '원글',
'content' => '원글 내용',
]);
// 정상 답글 생성
$this->createTestPost([
'user_id' => $this->postAuthor->id,
'parent_id' => $parentId,
'title' => '정상 답글',
'content' => '정상 답글 내용',
'status' => 'published',
]);
// 삭제된 답글 생성
$this->createTestPost([
'user_id' => $this->postAuthor->id,
'parent_id' => $parentId,
'title' => '삭제된 답글',
'content' => '삭제된 답글 내용',
'status' => 'deleted',
'deleted_at' => now(),
]);
// When: 관리자가 Admin API로 원글 상세 조회
$response = $this->actingAs($this->adminUser, 'sanctum')
->getJson("/api/modules/sirsoft-board/admin/board/{$this->board->slug}/posts/{$parentId}");
// Then: replies에 삭제된 답글이 포함됨
$response->assertStatus(200);
$replies = $response->json('data.replies');
$this->assertCount(2, $replies);
}
// ==========================================
// 헬퍼 메서드
// ==========================================
/**
* 게시글 생성 헬퍼
*/
private function createPost(array $attributes): int
{
$postId = $this->createTestPost($attributes);
return $postId;
}
}