Files
Gnuboard7/tests/Unit/Config/DatabaseConfigDeprecationTest.php
HeuJung cbc91117d7 fix(installer): Read DB 미사용 시 Write DB만 참조하도록 수정 + GitHub 이슈 템플릿 신설
설치 시 use_read_db=false 인데도 db_read_host 잔존값으로 read 커넥션을
조립해 5단계 마이그레이션이 실패하던 문제를 수정한다. "read 사용 여부"
판정이 세 지점에서 달랐던 것이 근본 원인으로, use_read_db 플래그를 단일
SSoT 로 통일한다.

- installer-runtime.php: read 블록을 use_read_db 게이트로. 미사용 시 runtime
 에 read 키 미생성 → 하류가 write 로 자동 fallback. mergeRuntimeIntoEnv 도
 read 부재 시 DB_READ_* 를 빈 값으로 기록(write 복사 제거).
- config/database.php: read env 를 Elvis(?:) fallback 으로 복원. 중첩
 env(A, env(B)) 는 DB_READ_* 가 빈 문자열일 때 fallback 이 무력화되므로,
 빈 값까지 흡수하는 ?: 를 사용(tinker 실측 확인). 설치 이후 운영 중
 DB_READ_* 공란 시에도 SELECT 가 write DB 로 정확히 향하게 한다.
- functions.php / request-handler.php: read 미사용 시 .env DB_READ_* 빈 값
 기록 + state.json 잔존 read 필드 제거(2단계 방어).

기존 설치 환경은 .env 에 DB_READ_* 가 이미 채워져 Elvis fallback 미발동 →
동작 불변이라 업그레이드 스텝 불필요(실측 확인).

무관 사전 실패 테스트(beta.7 stale-ENV 복구)는 코어 version 이 beta.7 이
아니면 skip 하도록 가드 헬퍼 추가(프로덕션 한시 가드 전제를 테스트에 반영).

자유형식 제보에서 진단 축(버전/활성 확장/커스텀 자산/환경)이 누락되기 쉬워
G7 전용 이슈 템플릿을 추가한다. bug_report / feature_request / upgrade_issue
+ config.yml(blank 비활성, Discussions·보안신고 링크).
2026-07-06 17:15:29 +09:00

175 lines
6.5 KiB
PHP

<?php
namespace Tests\Unit\Config;
use Tests\TestCase;
/**
* config/database.php 의 PHP 8.5+ deprecation 회피 회귀 테스트
*
* PHP 8.5 에서 PDO::MYSQL_ATTR_SSL_CA 상수가 deprecated 되었으므로
* config 로드 시 E_DEPRECATED 가 트리거되지 않아야 한다.
*
* 검증 방식:
* 1. 동적: config 파일을 재include 하여 발생한 PDO 관련 deprecation 캡처 (PHP 8.5+ 에서만 트리거)
* 2. 정적: 파일 텍스트에서 가드 없는 PDO::MYSQL_ATTR_SSL_CA 직접 참조 패턴 검출 (모든 PHP 버전)
*
* 정적 검사는 PHP 8.4 이하 CI 에서도 회귀를 차단하기 위함이다.
*
* @see https://github.com/gnuboard/g7/issues/23
*/
class DatabaseConfigDeprecationTest extends TestCase
{
public function test_mysql_options_does_not_trigger_pdo_deprecation(): void
{
$deprecations = $this->captureDeprecations(function () {
// Laravel 부팅 후 config 파일을 재include — database_path() 등 헬퍼 사용 가능
$config = require config_path('database.php');
$mysqlOptions = $config['connections']['mysql']['options'];
$mariadbOptions = $config['connections']['mariadb']['options'];
// array_filter 결과를 실제로 순회하여 키 상수 평가 강제
foreach ($mysqlOptions as $k => $v) {
$unused = $k;
}
foreach ($mariadbOptions as $k => $v) {
$unused = $k;
}
});
$this->assertEmpty(
$deprecations,
'config/database.php emitted PHP deprecation: '
.json_encode($deprecations, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
);
}
public function test_config_file_does_not_reference_unguarded_deprecated_pdo_constant(): void
{
$file = file_get_contents(config_path('database.php'));
// 가드 패턴: PHP_VERSION_ID >= 80500 ternary 안에서만 PDO::MYSQL_ATTR_SSL_CA 사용 허용
$guardedPattern = '/PHP_VERSION_ID\s*>=\s*80500.*?Pdo\\\\Mysql::ATTR_SSL_CA.*?PDO::MYSQL_ATTR_SSL_CA/s';
$hasGuard = (bool) preg_match($guardedPattern, $file);
// 직접 참조 횟수
$directReferences = preg_match_all('/PDO::MYSQL_ATTR_SSL_CA/', $file);
if ($directReferences > 0 && ! $hasGuard) {
$this->fail(
'config/database.php references PDO::MYSQL_ATTR_SSL_CA without a '
.'PHP_VERSION_ID >= 80500 ternary guard. This causes E_DEPRECATED '
.'on PHP 8.5+. Use \\Pdo\\Mysql::ATTR_SSL_CA when PHP_VERSION_ID >= 80500.'
);
}
$this->assertTrue(true);
}
/**
* 이슈 #63 회귀 — Read 커넥션은 DB_READ_* 가 비어 있으면 DB_WRITE_* 로 fallback 한다.
*
* 미설정(null) / 빈 문자열('') / 명시값 세 케이스를 모두 검증한다. 특히 빈 문자열
* 케이스는 config 가 `env('DB_READ_HOST', default)` (중첩 env) 로 되돌아가면
* fallback 이 무력화되므로(env() 가 ''를 반환) 이 테스트가 그 회귀를 차단한다.
*
* @dataProvider readFallbackProvider
*/
public function test_read_connection_falls_back_to_write_when_read_env_empty(
?string $readHost,
string $expectedHost
): void {
$original = [
'DB_WRITE_HOST' => $_ENV['DB_WRITE_HOST'] ?? null,
'DB_READ_HOST' => $_ENV['DB_READ_HOST'] ?? null,
];
$this->setEnvVar('DB_WRITE_HOST', 'write-host.test');
$this->setEnvVar('DB_READ_HOST', $readHost);
try {
// config 파일을 직접 require — 내부 env() 가 현재 $_ENV/putenv 값을 평가한다.
$config = require config_path('database.php');
$resolvedHost = $config['connections']['mysql']['read']['host'][0] ?? null;
$this->assertSame($expectedHost, $resolvedHost);
} finally {
$this->setEnvVar('DB_WRITE_HOST', $original['DB_WRITE_HOST']);
$this->setEnvVar('DB_READ_HOST', $original['DB_READ_HOST']);
}
}
/**
* @return array<string, array{0: ?string, 1: string}>
*/
public static function readFallbackProvider(): array
{
return [
'read 미설정 → write' => [null, 'write-host.test'],
'read 빈 문자열 → write' => ['', 'write-host.test'],
'read 명시 → read replica 존중' => ['read-replica.test', 'read-replica.test'],
];
}
/**
* 정적 검사 — config 가 read fallback 에 Elvis(?:) 패턴을 사용하는지 확인.
* 중첩 env(A, env(B)) 로 회귀하면(빈 문자열 fallback 실패) 이 테스트가 차단한다.
*/
public function test_config_uses_elvis_fallback_for_read_connection(): void
{
$file = file_get_contents(config_path('database.php'));
$this->assertMatchesRegularExpression(
"/env\('DB_READ_HOST'\)\s*\?:\s*env\('DB_WRITE_HOST'/",
$file,
'config/database.php 의 read.host 가 Elvis(?:) write fallback 을 사용해야 한다 '
.'(이슈 #63 — 빈 문자열 DB_READ_HOST 시 write 로 fallback).'
);
}
/**
* process env 를 임시로 설정/해제한다 (putenv + $_ENV + $_SERVER 동기).
*
* @param string $key 환경변수 키
* @param string|null $value 값 (null 이면 제거)
*/
private function setEnvVar(string $key, ?string $value): void
{
if ($value === null) {
putenv($key);
unset($_ENV[$key], $_SERVER[$key]);
} else {
putenv("{$key}={$value}");
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
/**
* @param callable $fn 실행 중 발생한 deprecation 을 수집할 콜백
* @return array<int, array{message: string, file: string, line: int}>
*/
private function captureDeprecations(callable $fn): array
{
$deprecations = [];
set_error_handler(function ($severity, $message, $file, $line) use (&$deprecations) {
if ($severity === E_DEPRECATED || $severity === E_USER_DEPRECATED) {
if (str_contains($message, 'PDO::') || str_contains($message, 'MYSQL_ATTR_')) {
$deprecations[] = compact('message', 'file', 'line');
}
}
return true;
}, E_DEPRECATED | E_USER_DEPRECATED);
try {
$fn();
} finally {
restore_error_handler();
}
return $deprecations;
}
}