sir.kr 커뮤니티 제보(Xbuilder 님 외)와 전수조사 발굴분을 코어·게시판· 이커머스·페이지·ckeditor5 전반에 반영. - 통합 검색 하이라이트: 코어 SearchHighlighter 로 원문 이스케이프 후 강조 (저장형 XSS 차단, board/ecommerce/page 3중복제 통합) - 공개 최신글·인기글·관리자 대시보드: 읽기권한 없는 게시판·블라인드·비활성 게시판 글 필터. 비밀글은 기존 공개 정책대로 제목 노출 유지(본문만 보호) - 게시판 관리 대시보드 권한 게이트, 이커머스 쿠폰 다운로드·리뷰 대상·주문옵션 확정 스코프 강화, 취소/환불 항목 검증 회원/비회원 대칭 - 만료 토큰 거부(SetLocale), 회원 탈퇴·정보관리 실패 시 내부 오류 원문 마스킹 - 예외→응답 매핑 정비(generic catch 상태코드), ckeditor 업로드 권한 게이트 정리 정책 정합 정정: 착수 계획이 "비밀글 제목 노출=취약점"으로 전제해 공개 최신글· 대시보드에 넣었던 is_secret 필터는, 저장소가 확정한 "제목 공개, 본문만 보호" 정책(2026-01-02/02-04/06-18/06-19)과 배치되어 되돌림. 읽기권한·블라인드 필터는 유지. Chrome MCP 실측(T1~T10) + 회귀 테스트로 확인.
168 lines
5.0 KiB
PHP
168 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Feature\Admin;
|
|
|
|
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
|
|
require_once __DIR__.'/../../ModuleTestCase.php';
|
|
|
|
use App\Models\User;
|
|
use Carbon\CarbonImmutable;
|
|
use Modules\Sirsoft\Board\Models\Board;
|
|
use Modules\Sirsoft\Board\Models\BoardStat;
|
|
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
|
|
/**
|
|
* 게시판 대시보드 API 테스트
|
|
*
|
|
* /api/modules/sirsoft-board/admin/dashboard/* 4 엔드포인트
|
|
* - overview / post-graph / recent-posts / pending-reports
|
|
* - 가드: admin 미들웨어 (admin 권한 보유자만)
|
|
*/
|
|
class DashboardApiTest extends ModuleTestCase
|
|
{
|
|
private User $adminUser;
|
|
|
|
private User $normalUser;
|
|
|
|
private const BASE = '/api/modules/sirsoft-board/admin/dashboard';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
BoardStat::query()->delete();
|
|
|
|
// 대시보드 라우트는 admin 미들웨어 + permission:admin,sirsoft-board.dashboard.view 가드를 사용한다.
|
|
$this->adminUser = $this->createAdminUser(['sirsoft-board.boards.read', 'sirsoft-board.dashboard.view']);
|
|
$this->normalUser = $this->createUser();
|
|
|
|
Board::create([
|
|
'name' => ['ko' => 'API 테스트', 'en' => 'API Test'],
|
|
'slug' => 'api-test-'.uniqid(),
|
|
'type' => 'basic',
|
|
]);
|
|
}
|
|
|
|
// ========== overview ==========
|
|
|
|
#[Test]
|
|
public function test_overview_requires_authentication(): void
|
|
{
|
|
$this->getJson(self::BASE.'/overview')->assertStatus(401);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_overview_rejects_non_admin_user(): void
|
|
{
|
|
$this->actingAs($this->normalUser)->getJson(self::BASE.'/overview')->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* @scenario case=dashboard_permission_gate
|
|
*
|
|
* @effects dashboard_requires_view_permission
|
|
*/
|
|
#[Test]
|
|
public function test_overview_returns_today_counts_for_admin(): void
|
|
{
|
|
BoardStat::create([
|
|
'date' => CarbonImmutable::today()->toDateString(),
|
|
'post_count' => 5,
|
|
'comment_count' => 12,
|
|
]);
|
|
|
|
$this->actingAs($this->adminUser)->getJson(self::BASE.'/overview')
|
|
->assertStatus(200)
|
|
->assertJsonStructure(['success', 'data' => ['today_posts', 'today_comments']])
|
|
->assertJsonPath('data.today_posts', 5)
|
|
->assertJsonPath('data.today_comments', 12);
|
|
}
|
|
|
|
// ========== post-graph ==========
|
|
|
|
#[Test]
|
|
public function test_post_graph_requires_authentication(): void
|
|
{
|
|
$this->getJson(self::BASE.'/post-graph')->assertStatus(401);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_post_graph_returns_schema_for_admin(): void
|
|
{
|
|
$today = CarbonImmutable::today();
|
|
for ($i = 0; $i < 7; $i++) {
|
|
BoardStat::create([
|
|
'date' => $today->subDays($i)->toDateString(),
|
|
'post_count' => 1,
|
|
'comment_count' => 2,
|
|
]);
|
|
}
|
|
|
|
$this->actingAs($this->adminUser)->getJson(self::BASE.'/post-graph')
|
|
->assertStatus(200)
|
|
->assertJsonStructure([
|
|
'success',
|
|
'data' => [
|
|
'days' => [['date', 'post_count', 'comment_count']],
|
|
'total_posts',
|
|
'total_comments',
|
|
'posts_change',
|
|
'comments_change',
|
|
'updated_at',
|
|
],
|
|
])
|
|
->assertJsonCount(7, 'data.days')
|
|
->assertJsonPath('data.total_posts', 7)
|
|
->assertJsonPath('data.total_comments', 14);
|
|
}
|
|
|
|
// ========== recent-posts ==========
|
|
|
|
#[Test]
|
|
public function test_recent_posts_requires_authentication(): void
|
|
{
|
|
$this->getJson(self::BASE.'/recent-posts')->assertStatus(401);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_recent_posts_returns_collection_for_admin(): void
|
|
{
|
|
$this->actingAs($this->adminUser)->getJson(self::BASE.'/recent-posts')
|
|
->assertStatus(200)
|
|
->assertJsonStructure(['success', 'data']);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_recent_posts_rejects_limit_over_max(): void
|
|
{
|
|
$this->actingAs($this->adminUser)
|
|
->getJson(self::BASE.'/recent-posts?limit=999')
|
|
->assertStatus(422);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_recent_posts_rejects_non_integer_limit(): void
|
|
{
|
|
$this->actingAs($this->adminUser)
|
|
->getJson(self::BASE.'/recent-posts?limit=abc')
|
|
->assertStatus(422);
|
|
}
|
|
|
|
// ========== pending-reports ==========
|
|
|
|
#[Test]
|
|
public function test_pending_reports_requires_authentication(): void
|
|
{
|
|
$this->getJson(self::BASE.'/pending-reports')->assertStatus(401);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_pending_reports_returns_items_and_total_for_admin(): void
|
|
{
|
|
$this->actingAs($this->adminUser)->getJson(self::BASE.'/pending-reports')
|
|
->assertStatus(200)
|
|
->assertJsonStructure(['success', 'data' => ['items', 'total']]);
|
|
}
|
|
}
|