From 1eb9d9ca72c79194abba2f520e72c4e6f855f08d Mon Sep 17 00:00:00 2001 From: 128Na Date: Mon, 19 Oct 2026 11:21:11 +0900 Subject: [PATCH 1/4] =?UTF-8?q?portal=E8=AA=AD=E3=81=BF=E5=8F=96=E3=82=8A?= =?UTF-8?q?=E5=B0=82=E7=94=A8DB=E3=81=AE=E6=8E=A5=E7=B6=9A=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=82=92=E7=8B=AC=E7=AB=8B=E8=A8=AD=E5=AE=9A=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 10 ++ config/database.php | 10 +- .../Unit/PortalDatabaseConfigurationTest.php | 137 ++++++++++++++++++ 3 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/PortalDatabaseConfigurationTest.php diff --git a/.env.example b/.env.example index 3b9dc2f..73182c8 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,17 @@ LOG_DISCORD_WEBHOOK_URL= DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD= +# Portal database to read from. Use a separate read-only database user. PORTAL_DB_DATABASE= +# Optional overrides. If omitted, host/port/user/password/socket use DB_* values. +# Empty credentials are used as-is; leave these lines commented until configured. +# PORTAL_DB_HOST=127.0.0.1 +# PORTAL_DB_PORT=3306 +# PORTAL_DB_USERNAME=cross_search_readonly +# PORTAL_DB_PASSWORD= +# PORTAL_DB_SOCKET= +# Optional dedicated connection URL (never inherits DB_URL). +# PORTAL_DB_URL= MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" diff --git a/config/database.php b/config/database.php index 98352d8..4412b27 100644 --- a/config/database.php +++ b/config/database.php @@ -54,13 +54,13 @@ ], 'portal' => [ 'driver' => 'mysql', - 'url' => env('DB_URL'), + 'url' => env('PORTAL_DB_URL'), 'host' => env('PORTAL_DB_HOST') ?? env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), + 'port' => env('PORTAL_DB_PORT', env('DB_PORT', '3306')), 'database' => env('PORTAL_DB_DATABASE') ?? env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), + 'username' => env('PORTAL_DB_USERNAME', env('DB_USERNAME', 'root')), + 'password' => env('PORTAL_DB_PASSWORD', env('DB_PASSWORD', '')), + 'unix_socket' => env('PORTAL_DB_SOCKET', env('DB_SOCKET', '')), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_0900_ai_ci'), 'prefix' => '', diff --git a/tests/Unit/PortalDatabaseConfigurationTest.php b/tests/Unit/PortalDatabaseConfigurationTest.php new file mode 100644 index 0000000..96687e2 --- /dev/null +++ b/tests/Unit/PortalDatabaseConfigurationTest.php @@ -0,0 +1,137 @@ + */ + private array $environment = []; + + #[\Override] + protected function setUp(): void + { + parent::setUp(); + + foreach (['DB_URL', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'DB_SOCKET', 'PORTAL_DB_URL', 'PORTAL_DB_HOST', 'PORTAL_DB_PORT', 'PORTAL_DB_DATABASE', 'PORTAL_DB_USERNAME', 'PORTAL_DB_PASSWORD', 'PORTAL_DB_SOCKET'] as $key) { + $this->environment[$key] = [ + 'process' => getenv($key), + 'env' => $_ENV[$key] ?? null, + 'server' => $_SERVER[$key] ?? null, + 'has_env' => array_key_exists($key, $_ENV), + 'has_server' => array_key_exists($key, $_SERVER), + ]; + $this->setEnvironment($key, null); + } + } + + #[\Override] + protected function tearDown(): void + { + foreach ($this->environment as $key => $original) { + putenv($original['process'] === false ? $key : $key.'='.$original['process']); + unset($_ENV[$key], $_SERVER[$key]); + if ($original['has_env']) { + $_ENV[$key] = $original['env']; + } + if ($original['has_server']) { + $_SERVER[$key] = $original['server']; + } + } + + parent::tearDown(); + } + + public function test_portal_uses_dedicated_connection_values(): void + { + $this->setLegacyValues(); + $values = ['host' => 'portal-host', 'port' => '3308', 'database' => 'portal-data', 'username' => 'portal-reader', 'password' => 'portal-secret', 'socket' => '/portal/mysql.sock']; + foreach ($values as $key => $value) { + $this->setEnvironment('PORTAL_DB_'.strtoupper($key), $value); + } + + $portal = $this->configuration()['connections']['portal']; + + foreach ($values as $key => $value) { + $this->assertSame($value, $portal[$key === 'socket' ? 'unix_socket' : $key]); + } + } + + public function test_missing_portal_values_preserve_legacy_fallback(): void + { + $this->setLegacyValues(); + $config = $this->configuration(); + + foreach (['host', 'port', 'database', 'username', 'password', 'unix_socket'] as $key) { + $this->assertSame($config['connections']['mysql'][$key], $config['connections']['portal'][$key]); + } + } + + public function test_explicit_empty_credentials_and_socket_do_not_fall_back(): void + { + $this->setLegacyValues(); + foreach (['USERNAME', 'PASSWORD', 'SOCKET'] as $key) { + $this->setEnvironment('PORTAL_DB_'.$key, ''); + } + + $portal = $this->configuration()['connections']['portal']; + + foreach (['username', 'password', 'unix_socket'] as $key) { + $this->assertSame('', $portal[$key]); + } + } + + public function test_common_database_url_cannot_override_portal_connection(): void + { + $this->setLegacyValues(); + $this->setEnvironment('DB_URL', 'mysql://main-user:main-password@main-host:3307/main-data'); + $this->setEnvironment('PORTAL_DB_DATABASE', 'portal-data'); + $this->setEnvironment('PORTAL_DB_USERNAME', 'portal-reader'); + $config = $this->configuration(); + $portal = (new ConfigurationUrlParser)->parseConfiguration($config['connections']['portal']); + + $this->assertNotNull($config['connections']['mysql']['url']); + $this->assertNull($config['connections']['portal']['url']); + $this->assertSame('portal-data', $portal['database']); + $this->assertSame('portal-reader', $portal['username']); + } + + public function test_dedicated_portal_url_is_parsed_independently(): void + { + $this->setLegacyValues(); + $this->setEnvironment('DB_URL', 'mysql://main-user:main-password@main-host:3307/main-data'); + $this->setEnvironment('PORTAL_DB_URL', 'mysql://portal-reader:portal-password@portal-host:3308/portal-data'); + $portal = (new ConfigurationUrlParser)->parseConfiguration($this->configuration()['connections']['portal']); + + $this->assertSame('portal-reader', $portal['username']); + $this->assertSame('portal-password', $portal['password']); + $this->assertSame('portal-host', $portal['host']); + $this->assertSame(3308, $portal['port']); + $this->assertSame('portal-data', $portal['database']); + } + + private function setLegacyValues(): void + { + foreach (['HOST' => 'legacy-host', 'PORT' => '3307', 'DATABASE' => 'legacy-data', 'USERNAME' => 'legacy-user', 'PASSWORD' => 'legacy-password', 'SOCKET' => '/legacy/mysql.sock'] as $key => $value) { + $this->setEnvironment('DB_'.$key, $value); + } + } + + private function setEnvironment(string $key, ?string $value): void + { + unset($_ENV[$key], $_SERVER[$key]); + putenv($value === null ? $key : $key.'='.$value); + if ($value !== null) { + $_ENV[$key] = $_SERVER[$key] = $value; + } + } + + /** @return array{connections: array>} */ + private function configuration(): array + { + return require dirname(__DIR__, 2).'/config/database.php'; + } +} From 59e39ce611d55cac86e33f8b795f769308e3e7a6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Oct 2026 02:21:45 +0000 Subject: [PATCH 2/4] [rector] Rector fixes --- tests/Unit/PortalDatabaseConfigurationTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Unit/PortalDatabaseConfigurationTest.php b/tests/Unit/PortalDatabaseConfigurationTest.php index 96687e2..285b70f 100644 --- a/tests/Unit/PortalDatabaseConfigurationTest.php +++ b/tests/Unit/PortalDatabaseConfigurationTest.php @@ -37,6 +37,7 @@ protected function tearDown(): void if ($original['has_env']) { $_ENV[$key] = $original['env']; } + if ($original['has_server']) { $_SERVER[$key] = $original['server']; } From 5f07f57af4d50aadcf09d5406f3be228dd4ad355 Mon Sep 17 00:00:00 2001 From: 128Na Date: Mon, 19 Oct 2026 11:22:10 +0900 Subject: [PATCH 3/4] =?UTF-8?q?portal=E6=8E=A5=E7=B6=9AURL=E3=81=AE?= =?UTF-8?q?=E4=B8=8A=E6=9B=B8=E3=81=8D=E3=82=92=E7=84=A1=E5=8A=B9=E5=8C=96?= =?UTF-8?q?=E3=81=97=E6=97=A2=E5=AD=98=E3=81=AE=E7=A7=98=E5=AF=86=E5=80=A4?= =?UTF-8?q?=E4=BF=9D=E8=AD=B7=E3=82=92=E7=B6=AD=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 -- config/database.php | 2 +- tests/Unit/PortalDatabaseConfigurationTest.php | 14 ++++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 73182c8..a8a9ebe 100644 --- a/.env.example +++ b/.env.example @@ -19,8 +19,6 @@ PORTAL_DB_DATABASE= # PORTAL_DB_USERNAME=cross_search_readonly # PORTAL_DB_PASSWORD= # PORTAL_DB_SOCKET= -# Optional dedicated connection URL (never inherits DB_URL). -# PORTAL_DB_URL= MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" diff --git a/config/database.php b/config/database.php index 4412b27..feb3026 100644 --- a/config/database.php +++ b/config/database.php @@ -54,7 +54,7 @@ ], 'portal' => [ 'driver' => 'mysql', - 'url' => env('PORTAL_DB_URL'), + 'url' => null, 'host' => env('PORTAL_DB_HOST') ?? env('DB_HOST', '127.0.0.1'), 'port' => env('PORTAL_DB_PORT', env('DB_PORT', '3306')), 'database' => env('PORTAL_DB_DATABASE') ?? env('DB_DATABASE', 'laravel'), diff --git a/tests/Unit/PortalDatabaseConfigurationTest.php b/tests/Unit/PortalDatabaseConfigurationTest.php index 96687e2..23f8bbc 100644 --- a/tests/Unit/PortalDatabaseConfigurationTest.php +++ b/tests/Unit/PortalDatabaseConfigurationTest.php @@ -99,18 +99,20 @@ public function test_common_database_url_cannot_override_portal_connection(): vo $this->assertSame('portal-reader', $portal['username']); } - public function test_dedicated_portal_url_is_parsed_independently(): void + public function test_url_cannot_bypass_explicit_portal_credentials(): void { $this->setLegacyValues(); $this->setEnvironment('DB_URL', 'mysql://main-user:main-password@main-host:3307/main-data'); $this->setEnvironment('PORTAL_DB_URL', 'mysql://portal-reader:portal-password@portal-host:3308/portal-data'); + $this->setEnvironment('PORTAL_DB_USERNAME', 'explicit-reader'); + $this->setEnvironment('PORTAL_DB_PASSWORD', 'explicit-secret'); $portal = (new ConfigurationUrlParser)->parseConfiguration($this->configuration()['connections']['portal']); - $this->assertSame('portal-reader', $portal['username']); - $this->assertSame('portal-password', $portal['password']); - $this->assertSame('portal-host', $portal['host']); - $this->assertSame(3308, $portal['port']); - $this->assertSame('portal-data', $portal['database']); + $this->assertSame('explicit-reader', $portal['username']); + $this->assertSame('explicit-secret', $portal['password']); + $this->assertSame('legacy-host', $portal['host']); + $this->assertSame('3307', $portal['port']); + $this->assertNull($portal['url']); } private function setLegacyValues(): void From 750ace310b7895fb5c887a9275e427d7356ebbaa Mon Sep 17 00:00:00 2001 From: 128Na Date: Mon, 19 Oct 2026 11:22:26 +0900 Subject: [PATCH 4/4] =?UTF-8?q?URL=E8=A7=A3=E6=9E=90=E5=BE=8C=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=8B=E3=82=89URL=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E3=81=8C=E9=99=A4=E5=8E=BB=E3=81=95=E3=82=8C=E3=82=8B=E3=81=93?= =?UTF-8?q?=E3=81=A8=E3=82=92=E6=A4=9C=E8=A8=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Unit/PortalDatabaseConfigurationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/PortalDatabaseConfigurationTest.php b/tests/Unit/PortalDatabaseConfigurationTest.php index 23f8bbc..5d068c3 100644 --- a/tests/Unit/PortalDatabaseConfigurationTest.php +++ b/tests/Unit/PortalDatabaseConfigurationTest.php @@ -112,7 +112,7 @@ public function test_url_cannot_bypass_explicit_portal_credentials(): void $this->assertSame('explicit-secret', $portal['password']); $this->assertSame('legacy-host', $portal['host']); $this->assertSame('3307', $portal['port']); - $this->assertNull($portal['url']); + $this->assertArrayNotHasKey('url', $portal); } private function setLegacyValues(): void