secretsDir = sys_get_temp_dir() . '/test-secrets-' . uniqid(); mkdir($this->secretsDir, 0700, true); }); afterEach(function () { // Cleanup test directory if (is_dir($this->secretsDir)) { array_map('unlink', glob($this->secretsDir . '/*')); rmdir($this->secretsDir); } }); it('returns direct environment variable when available', function () { $env = new Environment(['DB_PASSWORD' => 'direct_password']); $result = $env->get('DB_PASSWORD'); expect($result)->toBe('direct_password'); }); it('reads from *_FILE when direct variable not available', function () { // Create secret file $secretFile = $this->secretsDir . '/db_password'; file_put_contents($secretFile, 'file_password'); $env = new Environment(['DB_PASSWORD_FILE' => $secretFile]); $result = $env->get('DB_PASSWORD'); expect($result)->toBe('file_password'); }); it('trims whitespace from file contents', function () { // Docker secrets often have trailing newlines $secretFile = $this->secretsDir . '/db_password'; file_put_contents($secretFile, "file_password\n"); $env = new Environment(['DB_PASSWORD_FILE' => $secretFile]); $result = $env->get('DB_PASSWORD'); expect($result)->toBe('file_password'); }); it('prioritizes direct variable over *_FILE', function () { $secretFile = $this->secretsDir . '/db_password'; file_put_contents($secretFile, 'file_password'); $env = new Environment([ 'DB_PASSWORD' => 'direct_password', 'DB_PASSWORD_FILE' => $secretFile ]); $result = $env->get('DB_PASSWORD'); expect($result)->toBe('direct_password') ->and($result)->not->toBe('file_password'); }); it('returns default when neither direct nor *_FILE available', function () { $env = new Environment([]); $result = $env->get('DB_PASSWORD', 'default_value'); expect($result)->toBe('default_value'); }); it('returns null when file path does not exist', function () { $env = new Environment(['DB_PASSWORD_FILE' => '/nonexistent/path']); $result = $env->get('DB_PASSWORD'); expect($result)->toBeNull(); }); it('returns null when file path is not readable', function () { $secretFile = $this->secretsDir . '/unreadable'; file_put_contents($secretFile, 'password'); chmod($secretFile, 0000); // Make unreadable $env = new Environment(['DB_PASSWORD_FILE' => $secretFile]); $result = $env->get('DB_PASSWORD'); expect($result)->toBeNull(); chmod($secretFile, 0600); // Restore for cleanup }); it('handles multiple secrets from files', function () { // Create multiple secret files $dbPasswordFile = $this->secretsDir . '/db_password'; $apiKeyFile = $this->secretsDir . '/api_key'; file_put_contents($dbPasswordFile, 'db_secret'); file_put_contents($apiKeyFile, 'api_secret'); $env = new Environment([ 'DB_PASSWORD_FILE' => $dbPasswordFile, 'API_KEY_FILE' => $apiKeyFile ]); expect($env->get('DB_PASSWORD'))->toBe('db_secret') ->and($env->get('API_KEY'))->toBe('api_secret'); }); it('works with EnvKey enum', function () { $secretFile = $this->secretsDir . '/db_password'; file_put_contents($secretFile, 'enum_password'); $env = new Environment(['DB_PASSWORD_FILE' => $secretFile]); // Assuming DB_PASSWORD exists in EnvKey enum $result = $env->get('DB_PASSWORD'); expect($result)->toBe('enum_password'); }); it('handles empty secret files gracefully', function () { $secretFile = $this->secretsDir . '/empty'; file_put_contents($secretFile, ''); $env = new Environment(['SECRET_FILE' => $secretFile]); $result = $env->get('SECRET'); expect($result)->toBe(''); // Empty string, not null }); it('ignores non-string *_FILE values', function () { $env = new Environment(['DB_PASSWORD_FILE' => 12345]); // Invalid type $result = $env->get('DB_PASSWORD'); expect($result)->toBeNull(); }); it('real-world Docker Swarm secrets scenario', function () { // Simulate Docker Swarm secrets mount $dbPasswordFile = $this->secretsDir . '/db_password'; $appKeyFile = $this->secretsDir . '/app_key'; file_put_contents($dbPasswordFile, "MyS3cur3P@ssw0rd\n"); // With newline file_put_contents($appKeyFile, "base64:abcdef123456"); $env = new Environment([ 'DB_HOST' => 'localhost', // Direct env var 'DB_PORT' => '3306', // Direct env var 'DB_PASSWORD_FILE' => $dbPasswordFile, // From Docker secret 'APP_KEY_FILE' => $appKeyFile, // From Docker secret ]); // Verify all values work correctly expect($env->get('DB_HOST'))->toBe('localhost') ->and($env->get('DB_PORT'))->toBe('3306') ->and($env->get('DB_PASSWORD'))->toBe('MyS3cur3P@ssw0rd') ->and($env->get('APP_KEY'))->toBe('base64:abcdef123456'); }); it('works with getRequired() for secrets from files', function () { $secretFile = $this->secretsDir . '/required_secret'; file_put_contents($secretFile, 'required_value'); $env = new Environment(['REQUIRED_SECRET_FILE' => $secretFile]); $result = $env->getRequired('REQUIRED_SECRET'); expect($result)->toBe('required_value'); }); it('throws exception with getRequired() when secret file not found', function () { $env = new Environment(['REQUIRED_SECRET_FILE' => '/nonexistent/path']); expect(fn() => $env->getRequired('REQUIRED_SECRET')) ->toThrow(App\Framework\Config\Exceptions\RequiredEnvironmentVariableException::class); }); });