관리자 환경설정에서 미입금 자동취소 기한을 저장하면 HTML number 입력의 문자열이 그대로 영속되고, Carbon 3.13 의 strict 타입 경계에서 TypeError 가 발생해 무통장입금·가상계좌 주문이 전면 실패했다(공개 제보). 한 곳만 막으면 다른 저장 경로가 남으므로 저장·조회·소비·모델 네 계층에 각각 방어를 두고, 저장소 전역에서 같은 형태가 더 없음을 audit 룰로 확정했다. 같은 계획서 수행 중 드러난 인접 결함도 함께 해소했다. - 쿠폰 수정 경로로 (발급일 기준, 일수 NULL) 조합이 저장되어 발급 즉시 만료된 쿠폰이 조용히 나가던 문제 — 저장·발급 양쪽에서 차단 - 리뷰 작성 기한을 화면과 저장 판정이 서로 다른 기준으로 계산하던 문제 - 결제 플러그인이 PaymentMethodEnum 과 다른 어휘로 지원 결제수단을 선언해 가상계좌·계좌이체·휴대폰결제에 PG사를 지정할 수 없던 문제 엔진에서는 `_localInit` 적용 여부를 전역 해시로만 판정해 인스턴스별 저장소가 리셋되지 않던 결함(engine-v1.54.5)과, 제거 대상이 없어도 상태 갱신·캐시 무효화가 상시 발생하던 판정 결함(engine-v1.54.6)을 고쳤다. Playwright MCP 정밀 점검(T1~T11)에서 설정이 빈 문자열로 남은 사이트의 주문서 입금기한 안내가 미치환 플레이스홀더를 노출하는 것을 실측해, 화면 폴백을 서버와 같은 규칙으로 맞췄다.
83 lines
3.0 KiB
PHP
83 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Unit\Models;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\Sirsoft\Board\Models\Board;
|
|
use Modules\Sirsoft\Board\Models\Post;
|
|
use Modules\Sirsoft\Board\Tests\BoardTestCase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
|
|
/**
|
|
* new_display_hours 정수 캐스트 회귀 테스트
|
|
*
|
|
* Post::isNew() 는 게시판의 new_display_hours 를 Carbon subHours() 에 넘긴다.
|
|
* subHours 는 값을 음수화하며 넘기므로 숫자 문자열은 우연히 통과하지만(비숫자 문자열은
|
|
* "Unsupported operand types" 로 실패), 같은 값이 addHours 계열에 닿는 순간 TypeError 가 된다.
|
|
*
|
|
* 더 직접적인 결함은 타입 불일치다 — DB 왕복 후에는 드라이버가 INT 컬럼을 정수로 반환하지만,
|
|
* 요청 입력으로 모델에 대입된 직후(재조회 전)에는 문자열이 그대로 남아 API 응답/비교 연산의
|
|
* 타입이 조회 시점에 따라 달라진다. 형제 숫자 컬럼(max_reply_depth/posts_count 등)과
|
|
* 동일하게 integer 캐스트로 통일한다.
|
|
*
|
|
* @effects board_new_display_hours_is_integer
|
|
*/
|
|
class NewDisplayHoursCastTest extends BoardTestCase
|
|
{
|
|
#[Test]
|
|
public function new_display_hours_is_integer_right_after_mass_assignment(): void
|
|
{
|
|
// 요청 입력(HTML number → 문자열)으로 모델을 만든 직후, 재조회 없이 읽는 경로
|
|
$board = new Board(['new_display_hours' => '48']);
|
|
|
|
$this->assertSame(48, $board->new_display_hours);
|
|
}
|
|
|
|
#[Test]
|
|
public function new_display_hours_is_integer_after_create_without_refresh(): void
|
|
{
|
|
$board = Board::create([
|
|
'slug' => 'cast-new-display-hours',
|
|
'name' => ['ko' => '캐스트 테스트', 'en' => 'Cast Test'],
|
|
'is_active' => true,
|
|
'new_display_hours' => '48',
|
|
]);
|
|
|
|
$this->assertSame(48, $board->new_display_hours);
|
|
}
|
|
|
|
#[Test]
|
|
public function new_display_hours_is_cast_to_integer_when_loaded_from_db(): void
|
|
{
|
|
DB::table('boards')->where('id', $this->board->id)->update(['new_display_hours' => 12]);
|
|
|
|
$board = Board::findOrFail($this->board->id);
|
|
|
|
$this->assertSame(12, $board->new_display_hours);
|
|
}
|
|
|
|
#[Test]
|
|
public function post_is_new_does_not_crash_when_board_attribute_is_string(): void
|
|
{
|
|
$postId = $this->createTestPost();
|
|
$post = Post::with('board')->findOrFail($postId);
|
|
|
|
// 문자열 유입 재현 (캐스트 도입 후 정수로 해석되어야 함 — 비회귀 가드)
|
|
$post->board->new_display_hours = '24';
|
|
|
|
$this->assertTrue($post->isNew());
|
|
}
|
|
|
|
#[Test]
|
|
public function post_is_new_returns_false_for_old_post(): void
|
|
{
|
|
$postId = $this->createTestPost();
|
|
DB::table('board_posts')->where('id', $postId)->update(['created_at' => now()->subHours(5)]);
|
|
|
|
$post = Post::with('board')->findOrFail($postId);
|
|
$post->board->new_display_hours = '1';
|
|
|
|
$this->assertFalse($post->isNew());
|
|
}
|
|
}
|