Files
Gnuboard7/modules/_bundled/sirsoft-board/tests/Unit/ReportRepositoryTest.php
T
2026-05-11 11:29:41 +09:00

581 lines
18 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests\Unit;
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
require_once __DIR__.'/../ModuleTestCase.php';
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Enums\ReportReasonType;
use Modules\Sirsoft\Board\Enums\ReportStatus;
use Modules\Sirsoft\Board\Models\Board;
use Modules\Sirsoft\Board\Models\Report;
use Modules\Sirsoft\Board\Models\ReportLog;
use Modules\Sirsoft\Board\Repositories\ReportRepository;
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
/**
* ReportRepository 단위 테스트
*/
class ReportRepositoryTest extends ModuleTestCase
{
private ReportRepository $repository;
private User $user;
private Board $board;
protected function setUp(): void
{
parent::setUp();
// Telescope 비활성화 (테스트 환경)
config(['telescope.enabled' => false]);
// Phase 8: DDL implicit commit으로 이전 테스트 데이터가 남아있을 수 있으므로 정리
DB::table('boards_report_logs')->delete();
DB::table('boards_reports')->delete();
$this->repository = new ReportRepository;
$this->user = User::factory()->create();
$this->board = Board::firstOrCreate(
['slug' => 'report-repo-test'],
[
'name' => ['ko' => '신고 레포지토리 테스트 게시판'],
'type' => 'list',
'per_page' => 20,
'per_page_mobile' => 10,
'order_by' => 'created_at',
'order_direction' => 'DESC',
'secret_mode' => 'disabled',
'use_comment' => true,
'use_reply' => false,
'use_file_upload' => false,
'permissions' => [],
'notify_admin_on_post' => false,
'notify_author_on_comment' => false,
]
);
}
/**
* paginate() 메서드 테스트
*/
public function test_paginate_returns_paginated_reports(): void
{
// Given: 15개 신고 생성
Report::factory()->count(15)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
// When: per_page=10으로 페이지네이션
$result = $this->repository->paginate([], 10);
// Then: 10개 조회, 2페이지 존재
$this->assertCount(10, $result->items());
$this->assertEquals(15, $result->total());
$this->assertEquals(10, $result->perPage());
}
/**
* paginate() 메서드 - perPage 최소값 제한
*/
public function test_paginate_enforces_minimum_per_page(): void
{
// Given
Report::factory()->count(20)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
// When: per_page=5 요청 (최소값은 Repository에서 처리하지 않음, 그대로 5)
$result = $this->repository->paginate([], 5);
// Then: 5개씩 페이지네이션
$this->assertEquals(5, $result->perPage());
}
/**
* paginate() 메서드 - perPage 최대값 제한
*/
public function test_paginate_enforces_maximum_per_page(): void
{
// Given
Report::factory()->count(30)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
// When: per_page=25 요청 (최대값은 Repository에서 처리하지 않음, 그대로 25)
$result = $this->repository->paginate([], 25);
// Then: 25개씩 페이지네이션
$this->assertEquals(25, $result->perPage());
}
/**
* paginate() - 상태 필터링
*/
public function test_paginate_filters_by_status(): void
{
// Given
Report::factory()->count(3)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Pending,
]);
Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Review,
]);
// When
$result = $this->repository->paginate(['status' => 'pending'], 15);
// Then: pending 상태만 조회됨
$this->assertEquals(3, $result->total());
}
/**
* paginate() - 타입 필터링
*/
public function test_paginate_filters_by_target_type(): void
{
// Given
Report::factory()->count(3)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'target_type' => 'post',
]);
Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'target_type' => 'comment',
]);
// When
$result = $this->repository->paginate(['type' => 'post'], 15);
// Then
$this->assertEquals(3, $result->total());
}
/**
* paginate() - 검색 (게시판명)
*/
public function test_paginate_searches_by_board_name(): void
{
// Given
$anotherBoard = Board::firstOrCreate(
['slug' => 'report-repo-another'],
[
'name' => ['ko' => '다른 게시판'],
'type' => 'list',
'per_page' => 20,
'per_page_mobile' => 10,
'order_by' => 'created_at',
'order_direction' => 'DESC',
'secret_mode' => 'disabled',
'use_comment' => true,
'use_reply' => false,
'use_file_upload' => false,
'permissions' => [],
'notify_admin_on_post' => false,
'notify_author_on_comment' => false,
]
);
// board_name 검색은 board 관계의 JSON name 컬럼 기반 ($this->board name: '신고 레포지토리 테스트 게시판')
Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
// anotherBoard name: '다른 게시판' → '테스트' 검색에 미히트
Report::factory()->count(1)->create([
'board_id' => $anotherBoard->id,
'author_id' => $this->user->id,
]);
// MySQL InnoDB FULLTEXT 내부 cache 강제 플러시 — Fresh 삽입 직후 ngram 토큰이
// 아직 인덱싱되지 않아 LIKE 가 아닌 FULLTEXT 검색은 0 건 반환. ALTER TABLE ENGINE=InnoDB 로
// flush 강제 (이커머스 ProductSearchIntegrationTest 와 동일한 회피책).
$boardsTable = DB::connection()->getTablePrefix().(new Board)->getTable();
DB::statement("ALTER TABLE `{$boardsTable}` ENGINE=InnoDB");
// When
$result = $this->repository->paginate([
'search_field' => 'board_name',
'search' => '테스트',
], 15);
// Then
$this->assertEquals(2, $result->total());
}
/**
* create() 메서드 테스트
*/
public function test_create_saves_report_with_snapshot(): void
{
// Given: 1케이스 구조 — boards_reports에는 케이스 정보만, snapshot은 boards_report_logs에 저장
$caseData = [
'board_id' => $this->board->id,
'target_type' => 'post',
'target_id' => 1,
'author_id' => $this->user->id,
'status' => ReportStatus::Pending,
];
// When: 케이스 생성
$report = $this->repository->create($caseData);
// Then: 케이스 저장 확인
$report->refresh();
$this->assertEquals($this->board->id, $report->board_id);
$this->assertEquals('post', $report->target_type->value);
$this->assertEquals(ReportStatus::Pending, $report->status);
// snapshot은 createLog로 별도 저장
$log = $this->repository->createLog([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
'snapshot' => [
'board_name' => '테스트 게시판',
'title' => '테스트 제목',
'content' => '테스트 내용',
'content_mode' => 'text',
'author_name' => '작성자',
],
'reason_type' => ReportReasonType::Spam,
'reason_detail' => '스팸입니다.',
]);
$this->assertEquals('테스트 게시판', $log->snapshot['board_name']);
$this->assertEquals('테스트 제목', $log->snapshot['title']);
}
/**
* update() 메서드 테스트
*/
public function test_update_modifies_report(): void
{
// Given
$report = Report::factory()->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Pending,
]);
// When
$updated = $this->repository->update($report->id, [
'status' => ReportStatus::Review,
'processed_by' => $this->user->id,
]);
// Then
$this->assertEquals(ReportStatus::Review, $updated->status);
$this->assertEquals($this->user->id, $updated->processed_by);
}
/**
* bulkUpdateStatus() 메서드 테스트
*/
public function test_bulk_update_status_modifies_multiple_reports(): void
{
// Given
$reports = Report::factory()->count(3)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Pending,
]);
$ids = $reports->pluck('id')->toArray();
// When
$this->repository->bulkUpdateStatus($ids, [
'status' => ReportStatus::Rejected,
'processed_by' => $this->user->id,
]);
// Then
foreach ($ids as $id) {
$this->assertDatabaseHas('boards_reports', [
'id' => $id,
'status' => 'rejected',
]);
}
}
/**
* delete() 메서드 - 소프트 삭제
*/
public function test_delete_soft_deletes_report(): void
{
// Given
$report = Report::factory()->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
// When
$this->repository->delete($report->id);
// Then
$this->assertSoftDeleted('boards_reports', [
'id' => $report->id,
]);
}
/**
* countTodayReportsByUser() — 오늘 신고 건수 조회
*/
public function test_count_today_reports_by_user(): void
{
// Given: 사용자가 오늘 3건 신고 (1케이스 구조: logs에 reporter_id 기록)
$reports = Report::factory()->count(3)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
foreach ($reports as $report) {
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
]);
}
// 다른 사용자의 신고 2건
$otherUser = User::factory()->create();
$otherReports = Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $otherUser->id,
]);
foreach ($otherReports as $report) {
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $otherUser->id,
]);
}
// When
$count = $this->repository->countTodayReportsByUser($this->user->id);
// Then: 본인 신고만 카운트
$this->assertEquals(3, $count);
}
/**
* countRejectedReportsByUser() — 반려 건수 조회
*/
public function test_count_rejected_reports_by_user(): void
{
// Given: 사용자가 반려 케이스 4건에 신고 (logs 기준, report.status=rejected + processed_at 기준)
$rejectedReports = Report::factory()->count(4)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Rejected,
'processed_at' => now(),
]);
foreach ($rejectedReports as $report) {
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
]);
}
// 대기 중 케이스 2건 (반려 아님 → 카운트 제외)
$pendingReports = Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Pending,
]);
foreach ($pendingReports as $report) {
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
]);
}
// When: 30일 이내 반려 건수
$count = $this->repository->countRejectedReportsByUser($this->user->id, 30);
// Then
$this->assertEquals(4, $count);
}
/**
* countRejectedReportsByUser() — 기간 외 반려는 미포함
*/
public function test_count_rejected_reports_excludes_old_records(): void
{
// Given: 60일 전 반려 케이스 2건 (processed_at 기준 → 기간 밖)
$oldReports = Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Rejected,
'processed_at' => now()->subDays(60),
]);
foreach ($oldReports as $report) {
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
]);
}
// 최근 반려 케이스 1건 (processed_at = now → 기간 내)
$recentReport = Report::factory()->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Rejected,
'processed_at' => now(),
]);
ReportLog::factory()->create([
'report_id' => $recentReport->id,
'reporter_id' => $this->user->id,
]);
// When: 30일 이내만 조회
$count = $this->repository->countRejectedReportsByUser($this->user->id, 30);
// Then: 최근 1건만
$this->assertEquals(1, $count);
}
/**
* forceDelete() 메서드 - 영구 삭제
*/
public function test_force_delete_permanently_removes_report(): void
{
// Given
$report = Report::factory()->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
]);
// When
$this->repository->forceDelete($report->id);
// Then
$this->assertDatabaseMissing('boards_reports', [
'id' => $report->id,
]);
}
/**
* hasUserReported() — 신고한 경우 true 반환
*/
public function test_has_user_reported_returns_true_when_already_reported(): void
{
// Given: 사용자가 게시글을 신고한 케이스 + 로그 생성
$report = Report::factory()->create([
'board_id' => $this->board->id,
'target_type' => 'post',
'target_id' => 999,
'author_id' => $this->user->id,
]);
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
]);
// When
$result = $this->repository->hasUserReported($this->user->id, $this->board->id, 'post', 999);
// Then
$this->assertTrue($result);
}
/**
* hasUserReported() — 신고하지 않은 경우 false 반환
*/
public function test_has_user_reported_returns_false_when_not_reported(): void
{
// Given: 다른 사용자가 신고한 케이스만 존재
$otherUser = User::factory()->create();
$report = Report::factory()->create([
'board_id' => $this->board->id,
'target_type' => 'post',
'target_id' => 998,
'author_id' => $otherUser->id,
]);
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $otherUser->id,
]);
// When: $this->user는 신고한 적 없음
$result = $this->repository->hasUserReported($this->user->id, $this->board->id, 'post', 998);
// Then
$this->assertFalse($result);
}
/**
* hasUserReported() — 다른 대상(target_id)은 false 반환
*/
public function test_has_user_reported_returns_false_for_different_target(): void
{
// Given: target_id=997 신고
$report = Report::factory()->create([
'board_id' => $this->board->id,
'target_type' => 'post',
'target_id' => 997,
'author_id' => $this->user->id,
]);
ReportLog::factory()->create([
'report_id' => $report->id,
'reporter_id' => $this->user->id,
]);
// When: target_id=996 조회
$result = $this->repository->hasUserReported($this->user->id, $this->board->id, 'post', 996);
// Then
$this->assertFalse($result);
}
/**
* getStatistics() 메서드 테스트
*/
public function test_get_statistics_returns_counts(): void
{
// Given
Report::factory()->count(3)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Pending,
]);
Report::factory()->count(2)->create([
'board_id' => $this->board->id,
'author_id' => $this->user->id,
'status' => ReportStatus::Review,
]);
// When
$stats = $this->repository->getStatistics([]);
// Then
$this->assertEquals(3, $stats['by_status']['pending']);
$this->assertEquals(2, $stats['by_status']['review']);
$this->assertEquals(5, $stats['total']);
}
}