$slug, 'name' => ['ko' => '사용자 권한 테스트 게시판', 'en' => 'User Abilities Test Board'], 'is_active' => true, 'use_comment' => 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.comments.read' => ['ko' => '관리자 댓글 조회', 'en' => 'Admin Comments Read'], 'admin.comments.write' => ['ko' => '관리자 댓글 작성', 'en' => 'Admin Comments 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); } // ========================================== // BoardResource: user_abilities 키 구조 테스트 // ========================================== /** * 게시판 상세 API가 user_abilities 키를 can_* 형식으로 반환 */ #[Test] public function board_detail_returns_user_abilities_with_can_keys(): void { // When: include_user_abilities 파라미터와 함께 게시판 상세 조회 $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}?include_user_abilities=1"); // Then: user_abilities 키가 존재하고 can_* 형식 키를 포함 $response->assertStatus(200); $userAbilities = $response->json('data.user_abilities'); $this->assertNotNull($userAbilities, 'user_abilities 키가 존재해야 합니다'); $this->assertIsArray($userAbilities); // can_* 형식 키 존재 확인 $this->assertArrayHasKey('can_read', $userAbilities); $this->assertArrayHasKey('can_write', $userAbilities); $this->assertArrayHasKey('can_read_comments', $userAbilities); $this->assertArrayHasKey('can_write_comments', $userAbilities); } /** * 게시판 상세 API가 user_permissions 키를 반환하지 않음 (이전 형식) */ #[Test] public function board_detail_does_not_return_old_user_permissions_key(): void { // When $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}?include_user_abilities=1"); // Then: user_permissions 키가 존재하지 않음 $response->assertStatus(200); $data = $response->json('data'); $this->assertArrayNotHasKey('user_permissions', $data, 'user_permissions 키는 존재하지 않아야 합니다 (user_abilities로 대체됨)'); } /** * 게시판 user_abilities에 이전 underscore 형식 키가 존재하지 않음 */ #[Test] public function board_user_abilities_does_not_contain_old_underscore_keys(): void { // When $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}?include_user_abilities=1"); // Then: 이전 underscore 형식 키 미존재 $response->assertStatus(200); $userAbilities = $response->json('data.user_abilities'); // 이전 형식 키들이 존재하지 않는지 확인 $oldKeys = ['posts_read', 'posts_write', 'comments_read', 'comments_write', 'manager', 'admin_manage']; foreach ($oldKeys as $oldKey) { $this->assertArrayNotHasKey($oldKey, $userAbilities, "이전 형식 키 '{$oldKey}'가 존재하지 않아야 합니다"); } } /** * include_user_abilities 미전달 시 user_abilities가 null */ #[Test] public function board_detail_returns_null_user_abilities_without_param(): void { // When: include_user_abilities 파라미터 없이 조회 $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}"); // Then: user_abilities가 null $response->assertStatus(200); $this->assertNull($response->json('data.user_abilities'), 'include_user_abilities 미전달 시 user_abilities는 null이어야 합니다'); } // ========================================== // PostResource: abilities 키 구조 테스트 (목록) // ========================================== /** * 게시글 목록 API 가 컬렉션 레벨에서 abilities.can_* 형식을 반환 * * 목록 성능 최적화로 per-post abilities 는 상세 페이지에서만 제공되고, * 목록은 게시판 레벨 abilities 를 컬렉션 루트(data.abilities)에 한 번만 내려보낸다. * (PostResource::toListArray — "게시판 권한은 PostCollection의 컬렉션 레벨 abilities로 제공됩니다") */ #[Test] public function post_list_returns_collection_level_abilities_with_can_keys(): void { // Given: 게시글 생성 $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '테스트 게시글 1', 'content' => '내용 1', ]); // When: 게시글 목록 조회 $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts"); // Then: 컬렉션 루트에 abilities 키가 can_* 형식으로 존재 $response->assertStatus(200); $abilities = $response->json('data.abilities'); $this->assertIsArray($abilities, '컬렉션 레벨 abilities 가 존재해야 합니다'); $this->assertArrayHasKey('can_read', $abilities); $this->assertArrayHasKey('can_write', $abilities); } /** * 게시글 목록 컬렉션 abilities 에 이전 underscore 형식 키가 존재하지 않음 */ #[Test] public function post_list_collection_abilities_do_not_contain_old_underscore_keys(): void { // Given $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '테스트 게시글', 'content' => '내용', ]); // When $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts"); // Then: 컬렉션 루트 abilities 확인 $response->assertStatus(200); $abilities = $response->json('data.abilities'); $this->assertIsArray($abilities); $oldKeys = ['posts_read', 'posts_write', 'comments_read', 'comments_write', 'manager']; foreach ($oldKeys as $oldKey) { $this->assertArrayNotHasKey($oldKey, $abilities, "게시글 목록 abilities 에 이전 형식 키 '{$oldKey}' 가 존재하지 않아야 합니다"); } } // ========================================== // PostResource: abilities 키 구조 테스트 (상세) // ========================================== /** * 게시글 상세 API가 abilities.can_manage, abilities.can_write_comments 반환 */ #[Test] public function post_detail_returns_abilities_with_can_manage_and_can_write_comments(): void { // Given $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '상세 테스트 게시글', 'content' => '상세 내용', ]); // When: 게시글 상세 조회 $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}"); // Then: abilities에 can_manage, can_write_comments 키 존재 $response->assertStatus(200); $abilities = $response->json('data.abilities'); $this->assertNotNull($abilities, 'abilities 키가 존재해야 합니다'); $this->assertArrayHasKey('can_manage', $abilities); $this->assertArrayHasKey('can_write_comments', $abilities); $this->assertArrayHasKey('can_read', $abilities); $this->assertArrayHasKey('can_write', $abilities); $this->assertArrayHasKey('can_read_comments', $abilities); } /** * 게시글 상세 abilities에 이전 underscore 형식 키가 존재하지 않음 */ #[Test] public function post_detail_abilities_do_not_contain_old_underscore_keys(): void { // Given $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '테스트 게시글', 'content' => '내용', ]); // When $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}"); // Then $response->assertStatus(200); $abilities = $response->json('data.abilities'); $oldKeys = ['posts_read', 'posts_write', 'comments_read', 'comments_write', 'manager', 'admin_manage']; foreach ($oldKeys as $oldKey) { $this->assertArrayNotHasKey($oldKey, $abilities, "게시글 상세 abilities에 이전 형식 키 '{$oldKey}'가 존재하지 않아야 합니다"); } } // ========================================== // CommentResource: abilities 키 구조 테스트 // ========================================== /** * 댓글이 포함된 게시글 상세에서 댓글의 abilities.can_write 반환 */ #[Test] public function comment_in_post_detail_returns_abilities_with_can_write(): void { // Given: 게시글 + 댓글 생성 $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '댓글 테스트 게시글', 'content' => '내용', ]); $this->createTestComment($postId, [ 'user_id' => $this->regularUser->id, 'author_name' => $this->regularUser->name, 'content' => '테스트 댓글입니다.', ]); // When: 게시글 상세 조회 (댓글 포함) $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}"); // Then: 댓글에 abilities 키가 존재하고 can_write 포함 $response->assertStatus(200); $comments = $response->json('data.comments'); $this->assertNotEmpty($comments, '댓글이 1개 이상 존재해야 합니다'); $firstComment = $comments[0]; $this->assertArrayHasKey('abilities', $firstComment, '댓글에 abilities 키가 존재해야 합니다'); $this->assertArrayHasKey('can_write', $firstComment['abilities']); } /** * 댓글 abilities에 이전 underscore 형식 키가 존재하지 않음 */ #[Test] public function comment_abilities_do_not_contain_old_underscore_keys(): void { // Given $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '댓글 테스트 게시글', 'content' => '내용', ]); $this->createTestComment($postId, [ 'user_id' => $this->regularUser->id, 'author_name' => $this->regularUser->name, 'content' => '테스트 댓글입니다.', ]); // When $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}"); // Then $response->assertStatus(200); $comments = $response->json('data.comments'); $this->assertNotEmpty($comments); $commentAbilities = $comments[0]['abilities']; $oldKeys = ['comments_write', 'comments_read', 'manager']; foreach ($oldKeys as $oldKey) { $this->assertArrayNotHasKey($oldKey, $commentAbilities, "댓글 abilities에 이전 형식 키 '{$oldKey}'가 존재하지 않아야 합니다"); } } // ========================================== // Admin 컨텍스트 abilities 키 구조 테스트 // ========================================== /** * Admin 게시글 상세에서도 abilities가 can_* 형식으로 반환 */ #[Test] public function admin_post_detail_returns_abilities_with_can_keys(): void { // Given $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => 'Admin 테스트 게시글', 'content' => '내용', ]); // When: Admin API로 조회 $response = $this->actingAs($this->adminUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/admin/board/{$this->board->slug}/posts/{$postId}"); // Then: abilities에 can_* 형식 키 존재 $response->assertStatus(200); $abilities = $response->json('data.abilities'); $this->assertNotNull($abilities); $this->assertArrayHasKey('can_read', $abilities); $this->assertArrayHasKey('can_write', $abilities); $this->assertArrayHasKey('can_manage', $abilities); // 이전 형식 키 미존재 $this->assertArrayNotHasKey('admin_posts_read', $abilities); $this->assertArrayNotHasKey('admin_posts_write', $abilities); $this->assertArrayNotHasKey('admin_manage', $abilities); } // ========================================== // 권한 값 정확성 테스트 // ========================================== /** * 권한을 가진 사용자의 abilities 값이 true */ #[Test] public function user_with_permissions_has_true_abilities(): void { // Given $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '권한 테스트 게시글', 'content' => '내용', ]); // When: 권한을 가진 사용자가 조회 $response = $this->actingAs($this->regularUser, 'sanctum') ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}"); // Then: 해당 권한 값이 true $response->assertStatus(200); $abilities = $response->json('data.abilities'); $this->assertTrue($abilities['can_read'], 'posts.read 권한 보유자의 can_read는 true'); $this->assertTrue($abilities['can_write'], 'posts.write 권한 보유자의 can_write는 true'); $this->assertTrue($abilities['can_read_comments'], 'comments.read 권한 보유자의 can_read_comments는 true'); $this->assertTrue($abilities['can_write_comments'], 'comments.write 권한 보유자의 can_write_comments는 true'); } /** * 비회원의 abilities 값이 기본 권한에 따라 정확히 설정됨 */ #[Test] public function guest_user_abilities_reflect_default_permissions(): void { // Given: 게시글 생성 $postId = $this->createTestPost([ 'user_id' => $this->postAuthor->id, 'title' => '비회원 테스트 게시글', 'content' => '내용', ]); // When: 비로그인 상태로 조회 $response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}"); // Then: guest에 posts.read, posts.write 있음 / comments.read, comments.write 없음 $response->assertStatus(200); $abilities = $response->json('data.abilities'); $this->assertTrue($abilities['can_read'], 'guest에 posts.read 있으므로 can_read는 true'); $this->assertTrue($abilities['can_write'], 'guest에 posts.write 있으므로 can_write는 true'); $this->assertFalse($abilities['can_read_comments'], 'guest에 comments.read 없으므로 can_read_comments는 false'); $this->assertFalse($abilities['can_write_comments'], 'guest에 comments.write 없으므로 can_write_comments는 false'); } }