$slug, 'name' => ['ko' => '공개 게시글 테스트 게시판', 'en' => 'Public Posts Test Board'], 'is_active' => true, 'blocked_keywords' => [], ]; } /** * 각 테스트 전 실행 */ protected function setUp(): void { parent::setUp(); // 다른 테스트 클래스에서 user_id=1로 커밋된 posts가 user_id 기반 API에 영향을 미치므로 전체 정리 // (truncateUserTables() 후 새 user는 항상 id=1을 받으므로 타 테스트 posts와 충돌) DB::table('board_posts')->delete(); DB::table('board_comments')->delete(); // 테스트 대상 사용자 생성 (parent::setUp()에서 users 테이블 초기화됨) $this->targetUser = User::factory()->create(); } /** * 각 테스트 후 실행 */ protected function tearDown(): void { parent::tearDown(); } /** * 비인증 사용자가 다른 사용자의 공개 게시글을 조회할 수 있다. */ public function test_guest_can_fetch_user_public_posts(): void { // Given: 사용자가 게시글을 작성했을 때 $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '공개 게시글', 'status' => PostStatus::Published->value, ]); // When: 비인증 사용자가 해당 사용자의 게시글을 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts"); // Then: 성공 응답과 함께 게시글이 반환되어야 함 $response->assertOk(); $data = $response->json('data.data'); $this->assertIsArray($data); $this->assertCount(1, $data); $this->assertEquals('공개 게시글', $data[0]['title']); } /** * 타인 프로필에서도 비밀글은 목록에 나오되 본문은 나가지 않는다. * * 이 목록은 본문 일부(content_plain)를 함께 싣는데 라우트가 optional.sanctum 이라 * **미인증 요청에 타인의 비밀글 본문이 그대로 나갔다**(KVE-2026-1914 형제). * 행·제목은 게시판 목록에서 이미 같은 수준으로 보이므로(PostResource 의 목록 규칙) * 행은 남기고 본문만 비운다 — UI 의 비밀글 배지도 그대로 동작한다. */ public function test_secret_posts_are_listed_without_content_for_other_viewers(): void { // Given: 공개글과 비밀글을 작성 $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '공개 게시글', 'is_secret' => false, 'status' => PostStatus::Published->value, ]); $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '비밀 게시글', 'is_secret' => true, 'status' => PostStatus::Published->value, ]); // When: 사용자 게시글을 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts"); // Then: 두 건 모두 나오되 비밀글의 본문만 비어 있다 $response->assertOk(); $data = $response->json('data.data'); $this->assertCount(2, $data); $secret = collect($data)->firstWhere('is_secret', true); $public = collect($data)->firstWhere('is_secret', false); $this->assertNotNull($secret, '비밀글도 목록에는 나와야 합니다(배지로 구분)'); $this->assertSame('', $secret['content_plain'], '비밀글 본문은 나가면 안 됩니다'); $this->assertNotSame('', $public['content_plain'], '공개글 본문은 그대로여야 합니다'); } /** * 타인 프로필에서도 블라인드 게시글은 목록에 나오되 본문은 나가지 않는다. * * 블라인드 글의 본문은 이 모듈의 다른 목록에서도 비워진다 * (PostResource::getMaskedContentPreviewForList) — 같은 규칙을 적용한다. */ public function test_blinded_posts_are_listed_without_content_for_other_viewers(): void { // Given: 공개된 게시글과 블라인드 처리된 게시글 $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '발행된 게시글', 'status' => PostStatus::Published->value, ]); $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '블라인드 게시글', 'status' => PostStatus::Blinded->value, ]); // When: 사용자 게시글을 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts"); // Then: 두 건 모두 나오되 블라인드 글의 본문만 비어 있다 $response->assertOk(); $data = $response->json('data.data'); $this->assertCount(2, $data); $blinded = collect($data)->firstWhere('status', 'blinded'); $this->assertNotNull($blinded, '블라인드 글도 목록에는 나와야 합니다(배지로 구분)'); $this->assertSame('', $blinded['content_plain'], '블라인드 글 본문은 나가면 안 됩니다'); } /** * 게시글/댓글 통계를 조회할 수 있다. */ public function test_can_fetch_user_posts_stats(): void { // Given: 게시글과 댓글 작성 $post = $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '테스트 게시글', 'status' => PostStatus::Published->value, ]); $this->createComment($this->board->slug, $post->id, [ 'user_id' => $this->targetUser->id, 'content' => '내 댓글 1', 'status' => PostStatus::Published->value, ]); $this->createComment($this->board->slug, $post->id, [ 'user_id' => $this->targetUser->id, 'content' => '내 댓글 2', 'status' => PostStatus::Published->value, ]); // When: 통계를 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts/stats"); // Then: 통계 데이터가 반환되어야 함 $response->assertOk(); $data = $response->json('data'); $this->assertEquals(1, $data['posts_count']); $this->assertEquals(2, $data['comments_count']); } /** * 통계에서 블라인드 게시글/댓글은 제외된다. */ public function test_stats_exclude_blinded_items(): void { // Given: 발행된 게시글과 블라인드 처리된 게시글 $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '발행된 게시글', 'status' => PostStatus::Published->value, ]); $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => '블라인드 게시글', 'status' => PostStatus::Blinded->value, ]); // When: 통계를 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts/stats"); // Then: 발행된 게시글만 카운트되어야 함 $response->assertOk(); $data = $response->json('data'); $this->assertEquals(1, $data['posts_count']); } /** * 페이지네이션이 정상 동작한다. */ public function test_pagination_works(): void { // Given: 25개의 게시글 작성 for ($i = 1; $i <= 25; $i++) { $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => "게시글 {$i}", 'status' => PostStatus::Published->value, ]); } // When: 첫 페이지 조회 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts?page=1"); // Then: 20개의 항목과 페이지네이션 메타데이터가 있어야 함 $response->assertOk(); $this->assertCount(20, $response->json('data.data')); $this->assertEquals(1, $response->json('data.current_page')); $this->assertEquals(2, $response->json('data.last_page')); $this->assertEquals(25, $response->json('data.total')); } /** * per_page 파라미터로 페이지당 항목 수를 조절할 수 있다. */ public function test_per_page_parameter_works(): void { // Given: 10개의 게시글 작성 for ($i = 1; $i <= 10; $i++) { $this->createPost($this->board->slug, [ 'user_id' => $this->targetUser->id, 'title' => "게시글 {$i}", 'status' => PostStatus::Published->value, ]); } // When: per_page=5로 조회 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts?per_page=5"); // Then: 5개의 항목만 반환되어야 함 $response->assertOk(); $this->assertCount(5, $response->json('data.data')); } /** * 존재하지 않는 사용자의 게시글 조회 시 빈 배열이 반환된다. */ public function test_returns_empty_for_non_existent_user(): void { // When: 존재하지 않는 사용자 UUID 로 조회 (라우트는 {user:uuid} 바인딩) $nonExistentUuid = '00000000-0000-0000-0000-000000000000'; $response = $this->getJson("/api/modules/sirsoft-board/users/{$nonExistentUuid}/posts"); // Then: 모델 바인딩 실패로 404 또는 빈 배열 (optional.sanctum 경로) // UUID 형식은 유효하지만 실제 사용자가 없으므로 ImplicitRouteBinding 이 404 반환 $response->assertStatus(404); } /** * 헬퍼 메서드: 게시글 생성 */ private function createPost(string $slug, array $attributes = []): Post { return Post::create(array_merge([ 'board_id' => $this->board->id, 'title' => 'Test Post', 'content' => 'Test Content', 'user_id' => $this->targetUser->id, 'ip_address' => '127.0.0.1', 'is_notice' => false, 'is_secret' => false, 'status' => PostStatus::Published->value, ], $attributes)); } /** * 헬퍼 메서드: 댓글 생성 */ private function createComment(string $slug, int $postId, array $attributes = []): Comment { return Comment::create(array_merge([ 'board_id' => $this->board->id, 'post_id' => $postId, 'content' => 'Test Comment', 'user_id' => $this->targetUser->id, 'status' => PostStatus::Published->value, ], $attributes)); } }