testDir = '/home/michael/dev/michaelschiemer/tests/tmp'; // Create test directory if it doesn't exist if (!is_dir($this->testDir)) { mkdir($this->testDir, 0777, true); } // Initialize services $this->parser = new EnvFileParser(); $this->encryptionFactory = new EncryptionFactory(new SecureRandomGenerator()); $this->loader = new EncryptedEnvLoader($this->encryptionFactory, $this->parser); // Store original $_ENV and getenv() for restoration $this->originalEnv = $_ENV; $this->originalGetenv = getenv(); }); afterEach(function () { // Restore original environment $_ENV = $this->originalEnv; // Clear test directory if (is_dir($this->testDir)) { $files = glob($this->testDir . '/*'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } } }); describe('Basic .env File Loading', function () { it('loads environment from simple .env file', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('TestApp'); expect($env->get('APP_ENV'))->toBe('production'); expect($env->getBool('APP_DEBUG'))->toBeFalse(); expect($env->get('DB_HOST'))->toBe('localhost'); expect($env->getInt('DB_PORT'))->toBe(3306); }); it('handles quoted values correctly', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); expect($env->get('SINGLE_QUOTED'))->toBe('single quoted value'); expect($env->get('DOUBLE_QUOTED'))->toBe('double quoted value'); expect($env->get('UNQUOTED'))->toBe('unquoted value'); }); it('handles type casting correctly', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); expect($env->getBool('BOOL_TRUE'))->toBeTrue(); expect($env->getBool('BOOL_FALSE'))->toBeFalse(); expect($env->getInt('INT_VALUE'))->toBe(123); expect($env->getFloat('FLOAT_VALUE'))->toBe(123.45); expect($env->get('NULL_VALUE'))->toBeNull(); expect($env->getArray('ARRAY_VALUE'))->toBe(['item1', 'item2', 'item3']); }); it('handles comments and empty lines', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('TestApp'); expect($env->get('APP_ENV'))->toBe('development'); }); }); describe('Environment-Specific File Loading', function () { it('loads environment-specific .env file', function () { // Base .env file_put_contents($this->testDir . '/.env', <<testDir . '/.env.development', <<loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('BaseApp'); expect($env->get('APP_ENV'))->toBe('development'); expect($env->get('DB_HOST'))->toBe('dev-database'); // Overridden expect($env->getBool('DB_DEBUG'))->toBeTrue(); // Added }); it('loads production-specific configuration', function () { // Base .env file_put_contents($this->testDir . '/.env', <<testDir . '/.env.production', <<loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('MyApp'); expect($env->get('APP_ENV'))->toBe('production'); expect($env->get('DB_HOST'))->toBe('prod-database'); expect($env->get('CACHE_DRIVER'))->toBe('redis'); }); }); describe('Docker Environment Variables Priority', function () { it('prioritizes Docker ENV vars over .env files in production', function () { // Simulate Docker ENV vars via $_ENV $_ENV['APP_ENV'] = 'production'; $_ENV['DB_HOST'] = 'docker-mysql'; $_ENV['DB_PORT'] = '3306'; // .env file with different values file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); // Docker ENV vars should win in production expect($env->get('DB_HOST'))->toBe('docker-mysql'); expect($env->getInt('DB_PORT'))->toBe(3306); // But .env can add new variables expect($env->get('DB_NAME'))->toBe('mydb'); }); it('allows .env files to override in development', function () { // Simulate system ENV vars $_ENV['APP_ENV'] = 'development'; $_ENV['DB_HOST'] = 'system-host'; // .env file with override file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); // .env should override in development expect($env->get('DB_HOST'))->toBe('local-override'); }); }); describe('PHP-FPM getenv() Priority Fix', function () { it('prioritizes getenv() over $_ENV in PHP-FPM scenario', function () { // Simulate PHP-FPM scenario where $_ENV is empty but getenv() works $_ENV = []; // Empty like in PHP-FPM // Create .env file file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); // Should load from .env file since getenv() is also empty expect($env->get('APP_NAME'))->toBe('FromFile'); expect($env->get('APP_ENV'))->toBe('development'); }); }); describe('Docker Secrets Integration', function () { it('resolves Docker secrets from _FILE variables', function () { // Create secret files $dbPasswordPath = $this->testDir . '/db_password_secret'; $apiKeyPath = $this->testDir . '/api_key_secret'; file_put_contents($dbPasswordPath, 'super_secret_password'); file_put_contents($apiKeyPath, 'api_key_12345'); // .env file with _FILE variables file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('TestApp'); expect($env->get('DB_PASSWORD'))->toBe('super_secret_password'); expect($env->get('API_KEY'))->toBe('api_key_12345'); // Cleanup secret files unlink($dbPasswordPath); unlink($apiKeyPath); }); it('prioritizes direct variables over Docker secrets', function () { // Create secret file $secretPath = $this->testDir . '/secret'; file_put_contents($secretPath, 'secret_from_file'); // .env file with both direct and _FILE file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); // Direct variable should win expect($env->get('DB_PASSWORD'))->toBe('direct_password'); // Cleanup unlink($secretPath); }); it('handles missing Docker secret files gracefully', function () { // .env file with _FILE pointing to non-existent file file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('TestApp'); expect($env->get('DB_PASSWORD'))->toBeNull(); // Secret not resolved }); }); describe('Two-Pass Loading with Encryption', function () { it('loads encryption key in first pass and secrets in second pass', function () { // Generate encryption key $encryptionKey = bin2hex(random_bytes(32)); // Base .env with encryption key file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('TestApp'); expect($env->get('ENCRYPTION_KEY'))->toBe($encryptionKey); }); it('handles missing encryption key gracefully', function () { // .env without encryption key file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); // Should load successfully without encryption expect($env->get('APP_NAME'))->toBe('TestApp'); expect($env->get('ENCRYPTION_KEY'))->toBeNull(); }); }); describe('Complex Real-World Scenarios', function () { it('handles multi-layer environment configuration', function () { // Simulate Docker ENV vars $_ENV['APP_ENV'] = 'production'; $_ENV['CONTAINER_NAME'] = 'web-01'; // Base .env file_put_contents($this->testDir . '/.env', <<testDir . '/.env.production', <<testDir . '/db_password'; file_put_contents($dbPasswordPath, 'production_password'); $_ENV['DB_PASSWORD_FILE'] = $dbPasswordPath; $env = $this->loader->load($this->testDir); // Verify layered loading expect($env->get('APP_NAME'))->toBe('MyApp'); // From .env expect($env->get('APP_ENV'))->toBe('production'); // From Docker ENV (priority) expect($env->get('CONTAINER_NAME'))->toBe('web-01'); // From Docker ENV only expect($env->get('DB_HOST'))->toBe('prod-db-cluster'); // From .env.production expect($env->getInt('DB_PORT'))->toBe(3306); // From .env expect($env->get('CACHE_DRIVER'))->toBe('redis'); // From .env.production (override) expect($env->get('REDIS_HOST'))->toBe('redis-cluster'); // From .env.production expect($env->get('DB_PASSWORD'))->toBe('production_password'); // From Docker secret // Cleanup unlink($dbPasswordPath); }); it('handles database connection string configuration', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); // Verify all database configuration expect($env->get('DB_CONNECTION'))->toBe('mysql'); expect($env->get('DB_HOST'))->toBe('127.0.0.1'); expect($env->getInt('DB_PORT'))->toBe(3306); expect($env->get('DB_DATABASE'))->toBe('myapp_db'); expect($env->get('DB_USERNAME'))->toBe('dbuser'); expect($env->get('DB_PASSWORD'))->toBe('dbpass'); expect($env->get('DB_CHARSET'))->toBe('utf8mb4'); expect($env->get('DB_COLLATION'))->toBe('utf8mb4_unicode_ci'); }); it('handles application-wide feature flags', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); expect($env->get('APP_NAME'))->toBe('MyApp'); expect($env->getBool('FEATURE_NEW_UI'))->toBeTrue(); expect($env->getBool('FEATURE_BETA_ANALYTICS'))->toBeFalse(); expect($env->getBool('FEATURE_EXPERIMENTAL_CACHE'))->toBeTrue(); expect($env->getBool('MAINTENANCE_MODE'))->toBeFalse(); expect($env->getBool('DEBUG_TOOLBAR'))->toBeTrue(); }); it('handles URL and connection strings', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); expect($env->get('APP_URL'))->toBe('https://myapp.com'); expect($env->get('REDIS_URL'))->toBe('redis://localhost:6379/0'); expect($env->get('DATABASE_URL'))->toBe('mysql://user:pass@localhost:3306/dbname'); expect($env->get('ELASTICSEARCH_URL'))->toBe('http://localhost:9200'); }); }); describe('Error Handling', function () { it('handles missing .env file gracefully', function () { // No .env file created $env = $this->loader->load($this->testDir); // Should return environment with only system variables expect($env)->toBeInstanceOf(Environment::class); }); it('handles corrupted .env file gracefully', function () { // Create invalid .env file file_put_contents($this->testDir . '/.env', <<loader->load($this->testDir); // Should skip invalid lines and load valid ones expect($env->get('VALID_VAR'))->toBe('value'); expect($env->get('ANOTHER_VALID'))->toBe('value2'); }); it('handles unreadable .env file gracefully', function () { // Create .env file and make it unreadable file_put_contents($this->testDir . '/.env', 'APP_NAME=Test'); chmod($this->testDir . '/.env', 0000); $env = $this->loader->load($this->testDir); // Should handle gracefully expect($env)->toBeInstanceOf(Environment::class); // Restore permissions for cleanup chmod($this->testDir . '/.env', 0644); }); }); describe('EnvKey Enum Integration', function () { it('works with EnvKey enum throughout loading', function () { $envContent = <<testDir . '/.env', $envContent); $env = $this->loader->load($this->testDir); // Use EnvKey enum for access expect($env->get(EnvKey::APP_NAME))->toBe('TestApp'); expect($env->get(EnvKey::APP_ENV))->toBe('production'); expect($env->get(EnvKey::DB_HOST))->toBe('localhost'); expect($env->getInt(EnvKey::DB_PORT))->toBe(3306); }); }); describe('Performance and Memory', function () { it('handles large .env files efficiently', function () { // Generate large .env file $lines = []; for ($i = 0; $i < 1000; $i++) { $lines[] = "VAR_{$i}=value_{$i}"; } file_put_contents($this->testDir . '/.env', implode("\n", $lines)); $startMemory = memory_get_usage(); $startTime = microtime(true); $env = $this->loader->load($this->testDir); $endTime = microtime(true); $endMemory = memory_get_usage(); $duration = ($endTime - $startTime) * 1000; // Convert to ms $memoryUsed = ($endMemory - $startMemory) / 1024 / 1024; // Convert to MB // Verify all variables loaded expect($env->get('VAR_0'))->toBe('value_0'); expect($env->get('VAR_999'))->toBe('value_999'); // Performance assertions (generous limits for CI environments) expect($duration)->toBeLessThan(1000); // Should load in under 1 second expect($memoryUsed)->toBeLessThan(10); // Should use less than 10MB }); }); });