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) + 회귀 테스트로 확인.
90 lines
3.2 KiB
PHP
90 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Middleware;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Laravel\Sanctum\PersonalAccessToken;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* SetLocale 미들웨어가 만료된 Sanctum 토큰을 인정하지 않는지 검증합니다.
|
|
*
|
|
* 결함(㉗): resolveUser() 가 토큰의 만료(expires_at) 여부를 검사하지 않아
|
|
* 만료된 토큰으로도 그 회원의 로케일이 해석되었다. OptionalSanctumMiddleware 와
|
|
* 동일한 만료 검사를 이식하여 만료 토큰은 guest(null) 로 취급한다.
|
|
*/
|
|
class SetLocaleExpiredTokenTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// 인증 불필요 공개 라우트 — auth:sanctum 이 만료 토큰을 먼저 차단하지 않도록
|
|
// SetLocale(api 그룹) 단독 동작을 관찰한다.
|
|
Route::middleware(['api'])->get('/api/test-expired-locale', function () {
|
|
return response()->json([
|
|
'locale' => App::getLocale(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 만료된 토큰으로 요청하면 그 회원의 언어 설정을 따르지 않고
|
|
* guest 로케일(Accept-Language)로 폴백해야 합니다.
|
|
*
|
|
* @scenario case=expired_token_locale
|
|
*
|
|
* @effects expired_token_treated_as_guest_for_locale
|
|
*/
|
|
public function test_expired_token_does_not_resolve_member_locale(): void
|
|
{
|
|
$user = User::factory()->create(['language' => 'ko']);
|
|
$newToken = $user->createToken('test-token');
|
|
$plainTextToken = $newToken->plainTextToken;
|
|
|
|
// 토큰 만료 시각을 과거로 직접 세팅 (Sanctum 기본 expiration 은 null 이라 미설정됨)
|
|
PersonalAccessToken::findToken($plainTextToken)->update([
|
|
'expires_at' => now()->subDay(),
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$plainTextToken,
|
|
'Accept-Language' => 'en-US,en;q=0.9',
|
|
])->getJson('/api/test-expired-locale');
|
|
|
|
$response->assertStatus(200);
|
|
|
|
// 회원 언어(ko)를 따르지 않고 Accept-Language(en) 로 폴백해야 한다.
|
|
$this->assertNotSame('ko', $response->json('locale'));
|
|
$this->assertSame('en', $response->json('locale'));
|
|
}
|
|
|
|
/**
|
|
* 유효한 토큰은 그 회원의 언어 설정을 따라야 합니다 (기존 동작 유지).
|
|
*
|
|
* @scenario case=valid_token_locale
|
|
*
|
|
* @effects valid_token_applies_member_locale
|
|
*/
|
|
public function test_valid_token_resolves_member_locale(): void
|
|
{
|
|
$user = User::factory()->create(['language' => 'ko']);
|
|
$plainTextToken = $user->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$plainTextToken,
|
|
'Accept-Language' => 'en-US,en;q=0.9',
|
|
])->getJson('/api/test-expired-locale');
|
|
|
|
$response->assertStatus(200);
|
|
|
|
// 회원 언어(ko)가 Accept-Language(en)보다 우선한다.
|
|
$this->assertSame('ko', $response->json('locale'));
|
|
}
|
|
}
|