Files
Gnuboard7/tests/Unit/Services/MailIdentityProviderCodeLengthTest.php
T
HeuJung d17ea28da5 fix(core,board,ecommerce,page,kginicis): 정책값 하드코딩·계약 분리 결함 일괄 수정
관리자 환경설정이 값을 저장은 하지만 실제 동작에는 닿지 않던 결함군을 정리했다.
설정 화면(min/max)·저장 검증(FormRequest)·런타임 적용(Service) 세 계층이 각자
리터럴을 들고 있어 서로 조용히 갈라지던 것이 공통 원인이다.

경계값 SSoT 도입:
- config/core.php `settings_limits` 를 단일 출처로 두고, FormRequest 는 이 값으로
 규칙을 만들고 화면은 설정 응답의 `_meta.limits` 로 같은 값을 받아 바인딩한다.
- 게시판·이커머스도 각 모듈 config 의 limits 를 동일 구조로 노출한다.

적용되지 않던 정책을 실제로 적용:
- 비밀번호 최소 길이·특수문자 필수(신규 지정 7개 경로. 로그인은 제외 — 정책을
 올렸을 때 기존 회원이 자기 계정에 접근조차 못 하는 데드락을 막기 위함)
- 계정 잠금 시간 0 = 무한대(전용 컬럼 분리. timestamp 상한과 NULL 규약 때문에
 기존 컬럼 재사용이 불가능하다) + 관리자 수동 해제 경로
- 업로드 최대 크기·허용 확장자, 게시판 첨부 개수·댓글 깊이, 페이지 첨부 정책,
 장바구니 수량·구매 한도, 마일리지 사용 한도, 주문 부가세

파생 결함:
- 큐 Job(sync 드라이버)이 진행 중인 요청의 Request 바인딩을 덮어써, 뒤이어 실행되는
 훅 리스너가 헤더를 잃고 조용히 건너뛰었다. Job 이 원 바인딩을 복원하도록 고치고,
 전송 헤더가 필요한 리스너는 동기 실행으로 선언한다.
- 확장 업데이트 실패 시 상태만 되돌리고 상태 캐시를 비우지 않아, 그 확장의 관리자
 화면만 캐시 TTL(기본 하루) 동안 404 로 남았다. 실패 경로에도 무효화를 대칭으로
 두고, 빈 목록 자체를 캐시하지 않는다.
- 글 작성 시 첨부 파일이 Service 로 전달되지 않아 검증·권한은 통과하고 첨부만 사라졌다.
- 환경설정 저장 실패 안내가 내부 설정 키를 그대로 노출했다(105개 항목에 라벨 부여).

재발 방지:
- audit 룰 3종(레이아웃 경계 리터럴 / FormRequest 경계 불일치 / Service 재클램프)
- 테스트 실행 범위 강제 훅(check-test-scope)
- 트러블슈팅 사례 2건(큐 Job 요청 오염 / 확장 상태 캐시 고착)

공개 이슈: gnuboard/g7
2026-07-30 07:52:24 +09:00

119 lines
4.1 KiB
PHP

<?php
namespace Tests\Unit\Services;
use App\Extension\IdentityVerification\Providers\MailIdentityProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use ReflectionMethod;
use Tests\TestCase;
/**
* IDV 인증코드 길이 경계 테스트 (C3)
*
* 스키마가 경계를 선언하지 않아 서비스가 자체 상수(4~10)로 조용히 클램프했습니다.
* 관리자가 12를 설정하면 화면은 12로 보이는데 실제로는 10자리 코드가 발급됐습니다.
*/
class MailIdentityProviderCodeLengthTest extends TestCase
{
use RefreshDatabase;
/**
* 프로바이더 인스턴스를 반환합니다.
*
* @return MailIdentityProvider 프로바이더
*/
private function provider(): MailIdentityProvider
{
return app(MailIdentityProvider::class);
}
/**
* 스키마가 경계를 선언하는지 테스트합니다.
*
* 관리자 화면은 스키마를 그대로 노출하므로, 경계 선언이 곧 UI 계약이 된다.
*
* @scenario provider=mail, code_length=schema_declaration
*
* @effects schema_declares_min_max_boundaries
*/
public function test_schema_declares_code_length_boundaries(): void
{
$schema = $this->provider()->getSettingsSchema();
// 수정 전: min/max 키 자체가 없었다
$this->assertArrayHasKey('min', $schema['code_length']);
$this->assertArrayHasKey('max', $schema['code_length']);
$this->assertSame(MailIdentityProvider::MIN_CODE_LENGTH, $schema['code_length']['min']);
$this->assertSame(MailIdentityProvider::MAX_CODE_LENGTH, $schema['code_length']['max']);
$this->assertSame(MailIdentityProvider::DEFAULT_CODE_LENGTH, $schema['code_length']['default']);
}
/**
* 경계 내 길이는 그대로 적용되는지 테스트합니다.
*
* @scenario provider=mail, code_length=within_bounds
*
* @effects configured_length_applied_verbatim
*/
public function test_code_length_within_bounds_is_applied(): void
{
$method = new ReflectionMethod(MailIdentityProvider::class, 'generateNumericCode');
$this->assertSame(8, strlen($method->invoke($this->provider(), 8)));
$this->assertSame(4, strlen($method->invoke($this->provider(), 4)));
$this->assertSame(10, strlen($method->invoke($this->provider(), 10)));
}
/**
* 경계를 벗어나면 조용한 클램프 대신 기본값 폴백 + 경고 로그를 남기는지 테스트합니다.
*
* @scenario provider=mail, code_length=above_max
*
* @effects out_of_range_falls_back_to_default_with_warning
*/
public function test_out_of_range_falls_back_to_default_with_warning(): void
{
Log::spy();
$method = new ReflectionMethod(MailIdentityProvider::class, 'generateNumericCode');
// 수정 전: 12 → 10 으로 조용히 클램프
$code = $method->invoke($this->provider(), 12);
$this->assertSame(MailIdentityProvider::DEFAULT_CODE_LENGTH, strlen($code));
Log::shouldHaveReceived('warning')->once();
}
/**
* 하한 미만도 동일하게 폴백하는지 테스트합니다.
*
* @scenario provider=mail, code_length=below_min
*
* @effects out_of_range_falls_back_to_default_with_warning
*/
public function test_below_minimum_falls_back_to_default(): void
{
$method = new ReflectionMethod(MailIdentityProvider::class, 'generateNumericCode');
$this->assertSame(
MailIdentityProvider::DEFAULT_CODE_LENGTH,
strlen($method->invoke($this->provider(), 2))
);
}
/**
* 생성된 코드가 숫자로만 구성되는지 테스트합니다.
*
* @scenario provider=mail, code_length=within_bounds
*
* @effects generated_code_is_numeric
*/
public function test_generated_code_is_numeric(): void
{
$method = new ReflectionMethod(MailIdentityProvider::class, 'generateNumericCode');
$this->assertMatchesRegularExpression('/^\d{6}$/', $method->invoke($this->provider(), 6));
}
}