Files
Gnuboard7/tests/Feature/Upgrade/BackfillForeignKeyColumnCommentsTest.php
T
HeuJung bd215bc586 feat(identity): NHN KCP 휴대폰 본인확인 플러그인 추가
메일 인증 대신 휴대폰 본인확인을 쓸 수 있게 하는 플러그인을 추가한다.
가입·비밀번호 재설정·성인인증 등 코어 IDV 가 강제하는 모든 지점에서 동작하며,
테스트 모드로 계약 없이 전 흐름을 확인할 수 있다.

구현·검수 과정에서 드러난 저장소 전역 결함을 함께 처리했다.

- 외래키 컬럼의 한국어 comment 가 방출되지 않던 문제 (소스 38건 교정 +
 기설치본 백필 업그레이드 스텝 7종). ->comment 를 ->constrained 뒤에
 체인하면 예외 없이 통과하면서 comment 가 조용히 사라진다
- 로케일 전환 후 확장 액션 핸들러가 소실되던 문제 (플러그인 3종에
 재등록 진입점 노출)
- 본인인증 상태 폴링 응답이 누적 시도 횟수를 함께 내보내던 문제
- 인증 안내 문구와 실제 진행 방식의 폭 경계 불일치 (768~1023px).
 엔진과 같은 값을 같은 방법으로 읽도록 교정
- transition_overlay_target 이 replace 없이 무효이던 관리자 목록 40곳.
 로딩 표시가 나오지 않아도 경고가 남지 않아 화면을 봐야만 알 수 있었다

두 인증 플러그인의 코어 최소 요구 버전을 7.0.6 으로 올린다. 운영 모드
자격증명 필수 검사가 7.0.6 의 설정 저장 검증 표면에 의존하며, 그보다 낮은
코어에서는 그 검사가 조용히 통과해 빈 자격증명으로 저장할 수 있었다.

재발 차단으로 정적 검사 룰 4종을 신설하고, 검사 도구가 미커밋 신규 확장을
"검사 파일 0" 으로 통과시키던 사각을 함께 막았다.
2026-07-31 00:32:46 +09:00

263 lines
9.5 KiB
PHP

<?php
namespace Tests\Feature\Upgrade;
use App\Extension\UpgradeContext;
use App\Upgrades\Data\V7_0_6\Migrations\BackfillForeignKeyColumnComments;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use ReflectionClass;
use Tests\TestCase;
/**
* 외래키 컬럼 comment 회귀 — 소스 교정(신규 설치)과 백필(기설치본) 양쪽.
*
* 결함 시나리오 (2026-07-29 실측으로 입증):
* `$table->foreignId('created_by')->constrained('users')->nullOnDelete()->comment('등록자 ID')`
* 형태는 comment 가 컬럼이 아니라 외래키 정의에 부착되어 DB 에 방출되지 않는다.
* `ForeignIdColumnDefinition::constrained()` 가 `references()->on()`, 즉 `ForeignKeyDefinition`
* 을 반환하기 때문이며, MySQL grammar 의 compileForeign 은 comment 를 다루지 않는다.
*
* 실측 (g7_3, 교정 전):
* g7_menus.name → [메뉴 이름 (다국어 JSON)]
* g7_menus.created_by → [] ← 규정("전 컬럼 한국어 comment") 위반
*
* 본 테스트는 두 축을 함께 잠근다:
* 1. 신규 설치 — 마이그레이션이 만든 스키마에 외래키 컬럼 comment 가 실재하는가
* (교정 전이라면 실패한다)
* 2. 기설치본 — 백필 스텝이 빈 comment 만 채우고 운영자가 넣은 값은 보존하는가
*/
class BackfillForeignKeyColumnCommentsTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// DataMigration 클래스는 AbstractUpgradeStep::dataMigrations() 가 동적으로 require
// 하므로 일반 autoload 대상이 아님 — 테스트에서는 명시적으로 require_once.
require_once base_path('upgrades/data/7.0.6/migrations/03_BackfillForeignKeyColumnComments.php');
}
/**
* 테스트 시작 전 MySQL 계열이 아니면 스킵 대신 조기 종료 판단에 쓰는 플래그.
*
* @return bool MySQL 계열이면 true
*/
private function isMysql(): bool
{
return in_array(DB::getDriverName(), ['mysql', 'mariadb'], true);
}
/**
* 백필 스텝이 동결해 둔 대상 목록을 꺼냅니다.
*
* 테스트가 대상 목록을 따로 옮겨 적지 않게 하여, 스텝에 대상이 추가되면
* 본 테스트의 검증 범위도 자동으로 함께 넓어지도록 합니다.
*
* @return array<string, array<string, string>> 테이블 => [컬럼 => 설명]
*/
private function targets(): array
{
return (new ReflectionClass(BackfillForeignKeyColumnComments::class))
->getConstant('TARGETS');
}
/**
* 컬럼의 현재 comment 를 조회합니다.
*
* @param string $table 프리픽스 없는 테이블명
* @param string $column 컬럼명
* @return string|null comment (컬럼 부재 시 null)
*/
private function commentOf(string $table, string $column): ?string
{
$rows = DB::select(
'SELECT COLUMN_COMMENT FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?',
[DB::getTablePrefix().$table, $column],
);
return isset($rows[0]) ? (string) $rows[0]->COLUMN_COMMENT : null;
}
/**
* 컬럼 comment 를 강제로 지정합니다 (기설치본 상태 재현용).
*
* @param string $table 프리픽스 없는 테이블명
* @param string $column 컬럼명
* @param string $comment 설정할 comment (빈 문자열이면 교정 전 상태 재현)
*/
private function forceComment(string $table, string $column, string $comment): void
{
$prefixed = DB::getTablePrefix().$table;
$meta = DB::select(
'SELECT COLUMN_TYPE, IS_NULLABLE FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?',
[$prefixed, $column],
)[0];
DB::statement(sprintf(
'ALTER TABLE `%s` MODIFY COLUMN `%s` %s %s COMMENT %s',
$prefixed,
$column,
$meta->COLUMN_TYPE,
$meta->IS_NULLABLE === 'YES' ? 'NULL' : 'NOT NULL',
DB::connection()->getPdo()->quote($comment),
));
}
/**
* 백필 스텝을 실행합니다.
*/
private function runBackfill(): void
{
(new BackfillForeignKeyColumnComments)->run(
new UpgradeContext(fromVersion: '7.0.5', toVersion: '7.0.6'),
);
}
/**
* 마이그레이션이 만든 스키마에 외래키 컬럼 comment 가 실재해야 한다.
*
* 교정 전 소스(`->constrained()->comment()`)라면 이 단언이 전부 실패한다.
*/
public function test_migrations_emit_comments_on_foreign_key_columns(): void
{
if (! $this->isMysql()) {
$this->markTestSkipped('컬럼 comment 검증은 MySQL 계열에서만 의미가 있습니다.');
}
$missing = [];
foreach ($this->targets() as $table => $columns) {
foreach (array_keys($columns) as $column) {
$comment = $this->commentOf($table, $column);
if ($comment === null) {
continue; // 해당 테이블이 없는 구성 — 검증 대상 아님
}
if (trim($comment) === '') {
$missing[] = "{$table}.{$column}";
}
}
}
$this->assertSame(
[],
$missing,
'FK 컬럼 comment 누락 — ->comment() 가 ->constrained() 뒤에 체인되었는지 확인: '
.implode(', ', $missing),
);
}
/**
* 기설치본 재현 — 비어 있는 comment 를 백필이 채워야 한다.
*/
public function test_backfill_fills_empty_comment(): void
{
if (! $this->isMysql()) {
$this->markTestSkipped('컬럼 comment 검증은 MySQL 계열에서만 의미가 있습니다.');
}
$this->forceComment('menus', 'created_by', '');
$this->assertSame('', $this->commentOf('menus', 'created_by'));
$this->runBackfill();
$this->assertSame('등록자 ID', $this->commentOf('menus', 'created_by'));
}
/**
* 운영자가 직접 넣어 둔 comment 는 덮어쓰지 않아야 한다.
*/
public function test_backfill_preserves_operator_written_comment(): void
{
if (! $this->isMysql()) {
$this->markTestSkipped('컬럼 comment 검증은 MySQL 계열에서만 의미가 있습니다.');
}
$this->forceComment('menus', 'created_by', '운영자가 직접 적어 둔 설명');
$this->runBackfill();
$this->assertSame('운영자가 직접 적어 둔 설명', $this->commentOf('menus', 'created_by'));
}
/**
* 재실행해도 결과가 같아야 한다 (멱등).
*/
public function test_backfill_is_idempotent(): void
{
if (! $this->isMysql()) {
$this->markTestSkipped('컬럼 comment 검증은 MySQL 계열에서만 의미가 있습니다.');
}
$this->forceComment('menus', 'created_by', '');
$this->runBackfill();
$first = $this->commentOf('menus', 'created_by');
$this->runBackfill();
$second = $this->commentOf('menus', 'created_by');
$this->assertSame('등록자 ID', $first);
$this->assertSame($first, $second);
}
/**
* 설명 외의 컬럼 속성(자료형·NULL 허용)은 바뀌지 않아야 한다.
*/
public function test_backfill_does_not_alter_column_definition(): void
{
if (! $this->isMysql()) {
$this->markTestSkipped('컬럼 comment 검증은 MySQL 계열에서만 의미가 있습니다.');
}
$before = DB::select(
'SELECT COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?',
[DB::getTablePrefix().'menus', 'created_by'],
)[0];
$this->forceComment('menus', 'created_by', '');
$this->runBackfill();
$after = DB::select(
'SELECT COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?',
[DB::getTablePrefix().'menus', 'created_by'],
)[0];
$this->assertEquals($before->COLUMN_TYPE, $after->COLUMN_TYPE);
$this->assertEquals($before->IS_NULLABLE, $after->IS_NULLABLE);
$this->assertEquals($before->COLUMN_DEFAULT, $after->COLUMN_DEFAULT);
$this->assertEquals($before->EXTRA, $after->EXTRA);
}
/**
* 외래키 제약이 백필 이후에도 유지되어야 한다.
*/
public function test_backfill_keeps_foreign_key_constraint(): void
{
if (! $this->isMysql()) {
$this->markTestSkipped('컬럼 comment 검증은 MySQL 계열에서만 의미가 있습니다.');
}
$countFk = fn (): int => (int) DB::select(
'SELECT COUNT(*) AS c FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
AND REFERENCED_TABLE_NAME IS NOT NULL',
[DB::getTablePrefix().'menus', 'created_by'],
)[0]->c;
$before = $countFk();
$this->forceComment('menus', 'created_by', '');
$this->runBackfill();
$this->assertSame($before, $countFk());
}
}