Files
Gnuboard7/modules/_bundled/sirsoft-board/tests/Feature/User/PostSecretBooleanNormalizationTest.php
T
HeuJung d675c8ac9f fix(ecommerce,board): 쿠폰 중복사용 라디오 boolean 문자열화로 저장이 422 실패하던 결함 수정
공개 제보(gnuboard/g7 @lyg-kaban): 관리자 쿠폰 폼의 「타 쿠폰과 중복 사용」
라디오를 클릭하면 자동바인딩이 DOM 문자열 "true"/"false" 를 폼 상태에 그대로
저장해 서버 boolean 규칙에서 422 로 저장이 실패했다. 표시 계층은 느슨 비교로
정상이라 저장 시점에야 드러나는 결함.

- 레이아웃: is_combinable 라디오 autoBinding 해제 + String 표시 바인딩
 + change 액션 === 'true' boolean 캐스팅
- 서버: Store/UpdateCouponRequest prepareForValidation 에서 문자열 표기 수용
 boolean 정규화 (해석 불가값은 유지해 boolean 규칙이 422 처리)
- 게시판 is_secret 동일 잠재 위험 서버 방어 + User Store/UpdatePostRequest 의
 Board 직접 조회 4곳을 Repository 경유로 교체 (board 1.0.4)
- 회귀: PHPUnit 11건(RED 선행) + 레이아웃 쓰기경로 타입 프로브 + E2E 왕복 spec
 + 시나리오 매니페스트(cases=32)
- 재발 차단: ·AGENTS.md 금지 패턴 + audit 룰
 layout-boolean-option-autobinding 신설(전수 0건) + 형제 룰 4종
 extensions/** glob 누락 보강 + 트러블슈팅 사례 8 + 회귀 테스트
2026-08-12 16:22:33 +09:00

188 lines
6.9 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests\Feature\User;
// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요)
require_once __DIR__.'/../../ModuleTestCase.php';
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
/**
* 게시글 is_secret boolean 정규화 — 공개 이슈 #97 잠재 위험 방어
*
* 비밀글 여부를 문자열 "true"/"false" 로 보내는 클라이언트(레이아웃 hidden 시드 등)가
* 나타나도 `boolean` 규칙에서 422 로 깨지지 않도록 Store/UpdatePostRequest 의
* prepareForValidation 이 문자열 표기를 boolean 으로 정규화함을 고정한다.
* 해석 불가값은 건드리지 않아 boolean 규칙이 422 로 처리한다.
*/
class PostSecretBooleanNormalizationTest extends BoardTestCase
{
private User $memberUser;
/**
* 테스트 게시판 slug
*/
protected function getTestBoardSlug(): string
{
return 'secret-boolean-test';
}
/**
* 기본 게시판 속성 (비밀글 허용)
*/
protected function getDefaultBoardAttributes(string $slug): array
{
return [
'slug' => $slug,
'name' => ['ko' => '비밀글 boolean 테스트 게시판', 'en' => 'Secret Boolean Test Board'],
'is_active' => true,
'secret_mode' => 'enabled',
'blocked_keywords' => [],
];
}
protected function setUp(): void
{
parent::setUp();
$this->grantUserRolePermissions();
$this->memberUser = User::factory()->create(['email' => 'secret-bool@test.com']);
$userRole = Role::where('identifier', 'user')->first();
$this->memberUser->roles()->attach($userRole->id);
}
/**
* 수용해야 하는 boolean 표기 4종(문자열)과 기대 저장값
*
* @return array<string, array{mixed, bool}> [입력값, 기대 boolean]
*/
private function acceptedNotations(): array
{
return [
"문자열 'true'" => ['true', true],
"문자열 'false'" => ['false', false],
"문자열 '1'" => ['1', true],
"문자열 '0'" => ['0', false],
];
}
/**
* 작성: 문자열 boolean 표기 4종 모두 201 + DB boolean 저장
*/
public function test_store_accepts_string_boolean_notations(): void
{
foreach ($this->acceptedNotations() as $label => [$input, $expected]) {
$response = $this->actingAs($this->memberUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => "비밀글 표기 테스트 {$label}",
'content' => '비밀글 boolean 정규화 검증용 내용입니다.',
'is_secret' => $input,
]);
$response->assertStatus(201);
$postId = $response->json('data.id');
$stored = (bool) DB::table('board_posts')->where('id', $postId)->value('is_secret');
$this->assertSame(
$expected,
$stored,
"작성 {$label} 입력이 boolean 으로 저장되어야 합니다."
);
}
}
/**
* 수정: 문자열 boolean 표기 4종 모두 200 + DB boolean 저장
*/
public function test_update_accepts_string_boolean_notations(): void
{
$createResponse = $this->actingAs($this->memberUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비밀글 수정 표기 테스트',
'content' => '비밀글 boolean 정규화 수정 검증용 내용입니다.',
'is_secret' => false,
]);
$createResponse->assertStatus(201);
$postId = $createResponse->json('data.id');
foreach ($this->acceptedNotations() as $label => [$input, $expected]) {
$response = $this->actingAs($this->memberUser)
->putJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}", [
'is_secret' => $input,
]);
$response->assertStatus(200);
$stored = (bool) DB::table('board_posts')->where('id', $postId)->value('is_secret');
$this->assertSame(
$expected,
$stored,
"수정 {$label} 입력이 boolean 으로 저장되어야 합니다."
);
}
}
/**
* 작성: 해석 불가값('abc')은 정규화하지 않고 boolean 규칙이 422 처리
*/
public function test_store_rejects_unparsable_value_with_422(): void
{
$response = $this->actingAs($this->memberUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비밀글 해석불가 테스트',
'content' => '비밀글 boolean 해석 불가값 검증용 내용입니다.',
'is_secret' => 'abc',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['is_secret']);
}
/**
* 수정: 해석 불가값('abc')은 정규화하지 않고 boolean 규칙이 422 처리
*/
public function test_update_rejects_unparsable_value_with_422(): void
{
$createResponse = $this->actingAs($this->memberUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비밀글 수정 해석불가 테스트',
'content' => '비밀글 boolean 수정 해석 불가값 검증용 내용입니다.',
'is_secret' => true,
]);
$createResponse->assertStatus(201);
$postId = $createResponse->json('data.id');
$response = $this->actingAs($this->memberUser)
->putJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}", [
'is_secret' => 'abc',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['is_secret']);
// 실패한 요청이 저장값을 건드리지 않아야 한다
$this->assertSame(1, (int) DB::table('board_posts')->where('id', $postId)->value('is_secret'));
}
/**
* 작성: is_secret 미전송이면 정규화가 개입하지 않고 그대로 통과 (has 가드)
*/
public function test_store_without_is_secret_passes(): void
{
$response = $this->actingAs($this->memberUser)
->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [
'title' => '비밀글 미전송 테스트',
'content' => '비밀글 필드 미전송 검증용 내용입니다.',
]);
$response->assertStatus(201);
$postId = $response->json('data.id');
$this->assertSame(0, (int) DB::table('board_posts')->where('id', $postId)->value('is_secret'));
}
}