sys_get_temp_dir(), 'MESHVAULT_DB_HOST' => 'db', 'MESHVAULT_DB_NAME' => 'meshvault', 'MESHVAULT_DB_USER' => 'meshvault', 'MESHVAULT_DB_PASSWORD' => ' private-secret ', ]; } public function testDefaultsAndImmutableExplicitInput(): void { self::assertTrue(class_exists(AppConfig::class), 'AppConfig must exist'); $config = AppConfig::fromEnvironment(self::environment()); self::assertSame('production', $config->environment); self::assertSame('info', $config->logLevel); self::assertSame(5432, $config->dbPort); self::assertSame(sys_get_temp_dir(), $config->libraryPath); self::assertSame('db', $config->dbHost); self::assertSame('meshvault', $config->dbName); self::assertSame('meshvault', $config->dbUser); self::assertSame(' private-secret ', $config->dbPassword); self::assertTrue((new \ReflectionClass($config))->isReadOnly()); $this->expectException(\Error::class); $config->dbPort = 1234; } public static function invalidValues(): iterable { foreach (array_keys(self::environment()) as $key) { foreach ([null, '', ' ', false, 42, [], new \stdClass()] as $value) { yield [$key, $value]; } } foreach (['0', '65536', '-1', '+5432', '1.2', '1e3', ' 5432', '5432x', '', null, 5432] as $port) { yield ['MESHVAULT_DB_PORT', $port]; } foreach (['', 'prod', 'Production', 'development ', null, []] as $env) { yield ['MESHVAULT_ENV', $env]; } foreach (['', 'verbose', 'INFO', null, false] as $level) { yield ['MESHVAULT_LOG_LEVEL', $level]; } foreach ([__FILE__, __DIR__ . '/absent-library', 'relative/path', "bad\0path"] as $path) { yield ['MESHVAULT_LIBRARY_DIR', $path]; } yield ['MESHVAULT_DB_HOST', "db\nsecret"]; yield ['MESHVAULT_DB_NAME', "name\0secret"]; yield ['MESHVAULT_DB_USER', ' user ']; } #[DataProvider('invalidValues')] public function testInvalidValuesAreRejectedWithoutEchoingInput(string $key, mixed $value): void { self::assertTrue(class_exists(AppConfig::class), 'AppConfig must exist'); $environment = self::environment(); $environment[$key] = $value; try { AppConfig::fromEnvironment($environment); self::fail('Expected invalid configuration: ' . $key); } catch (InvalidArgumentException $error) { self::assertStringContainsString($key, $error->getMessage()); self::assertStringNotContainsString('private-secret', (string) $error); if (is_string($value) && strlen($value) > 5) { self::assertStringNotContainsString($value, $error->getMessage()); } } } public function testRequiredKeysMustBePresent(): void { self::assertTrue(class_exists(AppConfig::class), 'AppConfig must exist'); foreach (array_keys(self::environment()) as $key) { $environment = self::environment(); unset($environment[$key]); try { AppConfig::fromEnvironment($environment); self::fail('Missing key accepted'); } catch (InvalidArgumentException $error) { self::assertStringContainsString($key, $error->getMessage()); } } } public function testSupportedEnvironmentsLevelsAndPortBoundaries(): void { self::assertTrue(class_exists(AppConfig::class), 'AppConfig must exist'); foreach (['development', 'test', 'production'] as $env) { foreach (['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'] as $level) { foreach (['1', '65535'] as $port) { $config = AppConfig::fromEnvironment(self::environment() + [ 'MESHVAULT_ENV' => $env, 'MESHVAULT_LOG_LEVEL' => $level, 'MESHVAULT_DB_PORT' => $port, ]); self::assertSame($env, $config->environment); self::assertSame($level, $config->logLevel); self::assertSame((int) $port, $config->dbPort); } } } } public function testDiagnosticsAndSerializationDoNotExposePassword(): void { self::assertTrue(class_exists(AppConfig::class), 'AppConfig must exist'); $config = AppConfig::fromEnvironment(self::environment()); self::assertStringNotContainsString('private-secret', json_encode($config, JSON_THROW_ON_ERROR)); self::assertStringNotContainsString('private-secret', print_r($config, true)); self::assertStringNotContainsString('private-secret', serialize($config)); } }