testVariables = [ 'APP_NAME' => 'TestApp', 'APP_ENV' => 'development', 'APP_DEBUG' => 'true', 'DB_HOST' => 'localhost', 'DB_PORT' => '3306', 'DB_NAME' => 'testdb', 'CACHE_TTL' => '3600', 'API_TIMEOUT' => '30.5', 'FEATURE_FLAGS' => 'feature1,feature2,feature3', 'EMPTY_STRING' => '', 'NULL_VALUE' => 'null', 'FALSE_VALUE' => 'false', 'ZERO_VALUE' => '0', ]; $this->dockerSecretsResolver = new DockerSecretsResolver(); $this->environment = new Environment($this->testVariables, $this->dockerSecretsResolver); }); describe('get()', function () { it('returns string value for existing key', function () { $value = $this->environment->get('APP_NAME'); expect($value)->toBe('TestApp'); }); it('returns null for non-existent key', function () { $value = $this->environment->get('NON_EXISTENT'); expect($value)->toBeNull(); }); it('returns default value for non-existent key', function () { $value = $this->environment->get('NON_EXISTENT', 'default_value'); expect($value)->toBe('default_value'); }); it('returns empty string when value is empty', function () { $value = $this->environment->get('EMPTY_STRING'); expect($value)->toBe(''); }); it('returns string "null" for null value', function () { $value = $this->environment->get('NULL_VALUE'); expect($value)->toBe('null'); }); it('accepts EnvKey enum', function () { $value = $this->environment->get(EnvKey::APP_NAME); expect($value)->toBe('TestApp'); }); }); describe('getInt()', function () { it('returns integer for numeric string', function () { $value = $this->environment->getInt('DB_PORT'); expect($value)->toBe(3306); expect($value)->toBeInt(); }); it('returns null for non-existent key', function () { $value = $this->environment->getInt('NON_EXISTENT'); expect($value)->toBeNull(); }); it('returns default value for non-existent key', function () { $value = $this->environment->getInt('NON_EXISTENT', 9999); expect($value)->toBe(9999); }); it('returns zero for "0" string', function () { $value = $this->environment->getInt('ZERO_VALUE'); expect($value)->toBe(0); expect($value)->toBeInt(); }); it('accepts EnvKey enum', function () { $value = $this->environment->getInt(EnvKey::DB_PORT); expect($value)->toBe(3306); }); it('handles negative integers', function () { $env = new Environment(['NEGATIVE' => '-42'], $this->dockerSecretsResolver); $value = $env->getInt('NEGATIVE'); expect($value)->toBe(-42); }); }); describe('getBool()', function () { it('returns true for "true" string', function () { $value = $this->environment->getBool('APP_DEBUG'); expect($value)->toBeTrue(); }); it('returns false for "false" string', function () { $value = $this->environment->getBool('FALSE_VALUE'); expect($value)->toBeFalse(); }); it('returns null for non-existent key', function () { $value = $this->environment->getBool('NON_EXISTENT'); expect($value)->toBeNull(); }); it('returns default value for non-existent key', function () { $value = $this->environment->getBool('NON_EXISTENT', true); expect($value)->toBeTrue(); }); it('handles case-insensitive true values', function () { $env = new Environment([ 'BOOL1' => 'true', 'BOOL2' => 'True', 'BOOL3' => 'TRUE', 'BOOL4' => '1', ], $this->dockerSecretsResolver); expect($env->getBool('BOOL1'))->toBeTrue(); expect($env->getBool('BOOL2'))->toBeTrue(); expect($env->getBool('BOOL3'))->toBeTrue(); expect($env->getBool('BOOL4'))->toBeTrue(); }); it('handles case-insensitive false values', function () { $env = new Environment([ 'BOOL1' => 'false', 'BOOL2' => 'False', 'BOOL3' => 'FALSE', 'BOOL4' => '0', 'BOOL5' => '', ], $this->dockerSecretsResolver); expect($env->getBool('BOOL1'))->toBeFalse(); expect($env->getBool('BOOL2'))->toBeFalse(); expect($env->getBool('BOOL3'))->toBeFalse(); expect($env->getBool('BOOL4'))->toBeFalse(); expect($env->getBool('BOOL5'))->toBeFalse(); }); it('accepts EnvKey enum', function () { $value = $this->environment->getBool(EnvKey::APP_DEBUG); expect($value)->toBeTrue(); }); }); describe('getFloat()', function () { it('returns float for decimal string', function () { $value = $this->environment->getFloat('API_TIMEOUT'); expect($value)->toBe(30.5); expect($value)->toBeFloat(); }); it('returns null for non-existent key', function () { $value = $this->environment->getFloat('NON_EXISTENT'); expect($value)->toBeNull(); }); it('returns default value for non-existent key', function () { $value = $this->environment->getFloat('NON_EXISTENT', 99.99); expect($value)->toBe(99.99); }); it('converts integer string to float', function () { $value = $this->environment->getFloat('DB_PORT'); expect($value)->toBe(3306.0); expect($value)->toBeFloat(); }); it('handles negative floats', function () { $env = new Environment(['NEGATIVE' => '-42.5'], $this->dockerSecretsResolver); $value = $env->getFloat('NEGATIVE'); expect($value)->toBe(-42.5); }); it('accepts EnvKey enum', function () { $value = $this->environment->getFloat(EnvKey::API_TIMEOUT); expect($value)->toBe(30.5); }); }); describe('getArray()', function () { it('returns array from comma-separated string', function () { $value = $this->environment->getArray('FEATURE_FLAGS'); expect($value)->toBe(['feature1', 'feature2', 'feature3']); expect($value)->toBeArray(); }); it('returns null for non-existent key', function () { $value = $this->environment->getArray('NON_EXISTENT'); expect($value)->toBeNull(); }); it('returns default value for non-existent key', function () { $value = $this->environment->getArray('NON_EXISTENT', ['default1', 'default2']); expect($value)->toBe(['default1', 'default2']); }); it('returns single-element array for non-comma value', function () { $value = $this->environment->getArray('APP_NAME'); expect($value)->toBe(['TestApp']); }); it('trims whitespace from array elements', function () { $env = new Environment([ 'WHITESPACE_ARRAY' => 'value1 , value2 , value3 ', ], $this->dockerSecretsResolver); $value = $env->getArray('WHITESPACE_ARRAY'); expect($value)->toBe(['value1', 'value2', 'value3']); }); it('returns empty array for empty string', function () { $value = $this->environment->getArray('EMPTY_STRING'); expect($value)->toBe(['']); }); it('accepts custom separator', function () { $env = new Environment([ 'PIPE_SEPARATED' => 'value1|value2|value3', ], $this->dockerSecretsResolver); $value = $env->getArray('PIPE_SEPARATED', separator: '|'); expect($value)->toBe(['value1', 'value2', 'value3']); }); it('accepts EnvKey enum', function () { $value = $this->environment->getArray(EnvKey::FEATURE_FLAGS); expect($value)->toBe(['feature1', 'feature2', 'feature3']); }); }); describe('getEnum()', function () { it('returns enum value for valid string', function () { $value = $this->environment->getEnum('APP_ENV', AppEnvironment::class); expect($value)->toBeInstanceOf(AppEnvironment::class); expect($value)->toBe(AppEnvironment::DEVELOPMENT); }); it('returns null for non-existent key', function () { $value = $this->environment->getEnum('NON_EXISTENT', AppEnvironment::class); expect($value)->toBeNull(); }); it('returns default value for non-existent key', function () { $value = $this->environment->getEnum( 'NON_EXISTENT', AppEnvironment::class, AppEnvironment::PRODUCTION ); expect($value)->toBe(AppEnvironment::PRODUCTION); }); it('handles case-insensitive enum matching', function () { $env = new Environment([ 'ENV1' => 'development', 'ENV2' => 'Development', 'ENV3' => 'DEVELOPMENT', ], $this->dockerSecretsResolver); expect($env->getEnum('ENV1', AppEnvironment::class))->toBe(AppEnvironment::DEVELOPMENT); expect($env->getEnum('ENV2', AppEnvironment::class))->toBe(AppEnvironment::DEVELOPMENT); expect($env->getEnum('ENV3', AppEnvironment::class))->toBe(AppEnvironment::DEVELOPMENT); }); it('accepts EnvKey enum for key parameter', function () { $value = $this->environment->getEnum(EnvKey::APP_ENV, AppEnvironment::class); expect($value)->toBe(AppEnvironment::DEVELOPMENT); }); it('handles backed enum with integer value', function () { $env = new Environment(['STATUS' => '1'], $this->dockerSecretsResolver); $value = $env->getEnum('STATUS', TestIntEnum::class); expect($value)->toBe(TestIntEnum::ACTIVE); }); }); describe('require()', function () { it('returns value for existing key', function () { $value = $this->environment->require('APP_NAME'); expect($value)->toBe('TestApp'); }); it('throws exception for non-existent key', function () { $this->environment->require('NON_EXISTENT'); })->throws(EnvironmentVariableNotFoundException::class); it('accepts EnvKey enum', function () { $value = $this->environment->require(EnvKey::APP_NAME); expect($value)->toBe('TestApp'); }); it('throws exception with helpful message', function () { try { $this->environment->require('MISSING_VARIABLE'); $this->fail('Expected EnvironmentVariableNotFoundException'); } catch (EnvironmentVariableNotFoundException $e) { expect($e->getMessage())->toContain('MISSING_VARIABLE'); expect($e->getMessage())->toContain('required'); } }); }); describe('requireInt()', function () { it('returns integer for existing numeric key', function () { $value = $this->environment->requireInt('DB_PORT'); expect($value)->toBe(3306); expect($value)->toBeInt(); }); it('throws exception for non-existent key', function () { $this->environment->requireInt('NON_EXISTENT'); })->throws(EnvironmentVariableNotFoundException::class); it('accepts EnvKey enum', function () { $value = $this->environment->requireInt(EnvKey::DB_PORT); expect($value)->toBe(3306); }); }); describe('requireBool()', function () { it('returns boolean for existing boolean key', function () { $value = $this->environment->requireBool('APP_DEBUG'); expect($value)->toBeTrue(); }); it('throws exception for non-existent key', function () { $this->environment->requireBool('NON_EXISTENT'); })->throws(EnvironmentVariableNotFoundException::class); it('accepts EnvKey enum', function () { $value = $this->environment->requireBool(EnvKey::APP_DEBUG); expect($value)->toBeTrue(); }); }); describe('requireFloat()', function () { it('returns float for existing numeric key', function () { $value = $this->environment->requireFloat('API_TIMEOUT'); expect($value)->toBe(30.5); expect($value)->toBeFloat(); }); it('throws exception for non-existent key', function () { $this->environment->requireFloat('NON_EXISTENT'); })->throws(EnvironmentVariableNotFoundException::class); it('accepts EnvKey enum', function () { $value = $this->environment->requireFloat(EnvKey::API_TIMEOUT); expect($value)->toBe(30.5); }); }); describe('requireArray()', function () { it('returns array for existing comma-separated key', function () { $value = $this->environment->requireArray('FEATURE_FLAGS'); expect($value)->toBe(['feature1', 'feature2', 'feature3']); expect($value)->toBeArray(); }); it('throws exception for non-existent key', function () { $this->environment->requireArray('NON_EXISTENT'); })->throws(EnvironmentVariableNotFoundException::class); it('accepts EnvKey enum', function () { $value = $this->environment->requireArray(EnvKey::FEATURE_FLAGS); expect($value)->toBe(['feature1', 'feature2', 'feature3']); }); it('accepts custom separator', function () { $env = new Environment([ 'PIPE_SEPARATED' => 'value1|value2|value3', ], $this->dockerSecretsResolver); $value = $env->requireArray('PIPE_SEPARATED', separator: '|'); expect($value)->toBe(['value1', 'value2', 'value3']); }); }); describe('requireEnum()', function () { it('returns enum value for valid string', function () { $value = $this->environment->requireEnum('APP_ENV', AppEnvironment::class); expect($value)->toBeInstanceOf(AppEnvironment::class); expect($value)->toBe(AppEnvironment::DEVELOPMENT); }); it('throws exception for non-existent key', function () { $this->environment->requireEnum('NON_EXISTENT', AppEnvironment::class); })->throws(EnvironmentVariableNotFoundException::class); it('accepts EnvKey enum for key parameter', function () { $value = $this->environment->requireEnum(EnvKey::APP_ENV, AppEnvironment::class); expect($value)->toBe(AppEnvironment::DEVELOPMENT); }); }); describe('has()', function () { it('returns true for existing key', function () { $exists = $this->environment->has('APP_NAME'); expect($exists)->toBeTrue(); }); it('returns false for non-existent key', function () { $exists = $this->environment->has('NON_EXISTENT'); expect($exists)->toBeFalse(); }); it('returns true for empty string value', function () { $exists = $this->environment->has('EMPTY_STRING'); expect($exists)->toBeTrue(); }); it('accepts EnvKey enum', function () { $exists = $this->environment->has(EnvKey::APP_NAME); expect($exists)->toBeTrue(); }); }); describe('all()', function () { it('returns all environment variables', function () { $allVars = $this->environment->all(); expect($allVars)->toBe($this->testVariables); expect($allVars)->toBeArray(); }); it('returns empty array for empty environment', function () { $emptyEnv = new Environment([], $this->dockerSecretsResolver); $allVars = $emptyEnv->all(); expect($allVars)->toBe([]); }); }); describe('Docker Secrets Integration', function () { it('resolves Docker secret when file exists', function () { // Create temporary Docker secret file $secretPath = '/tmp/docker_secret_test'; file_put_contents($secretPath, 'secret_value_from_file'); $env = new Environment([ 'DB_PASSWORD_FILE' => $secretPath, ], $this->dockerSecretsResolver); $value = $env->get('DB_PASSWORD'); expect($value)->toBe('secret_value_from_file'); // Cleanup unlink($secretPath); }); it('returns null when Docker secret file does not exist', function () { $env = new Environment([ 'DB_PASSWORD_FILE' => '/nonexistent/path/to/secret', ], $this->dockerSecretsResolver); $value = $env->get('DB_PASSWORD'); expect($value)->toBeNull(); }); it('prioritizes direct variable over Docker secret', function () { // Create temporary Docker secret file $secretPath = '/tmp/docker_secret_test'; file_put_contents($secretPath, 'secret_from_file'); $env = new Environment([ 'DB_PASSWORD' => 'direct_value', 'DB_PASSWORD_FILE' => $secretPath, ], $this->dockerSecretsResolver); $value = $env->get('DB_PASSWORD'); // Direct variable should win expect($value)->toBe('direct_value'); // Cleanup unlink($secretPath); }); it('trims whitespace from Docker secret content', function () { // Create temporary Docker secret file with whitespace $secretPath = '/tmp/docker_secret_test'; file_put_contents($secretPath, " secret_value_with_whitespace \n"); $env = new Environment([ 'API_KEY_FILE' => $secretPath, ], $this->dockerSecretsResolver); $value = $env->get('API_KEY'); expect($value)->toBe('secret_value_with_whitespace'); // Cleanup unlink($secretPath); }); it('handles multiple Docker secrets', function () { // Create multiple secret files $dbPasswordPath = '/tmp/db_password_secret'; $apiKeyPath = '/tmp/api_key_secret'; file_put_contents($dbPasswordPath, 'db_secret_123'); file_put_contents($apiKeyPath, 'api_secret_456'); $env = new Environment([ 'DB_PASSWORD_FILE' => $dbPasswordPath, 'API_KEY_FILE' => $apiKeyPath, ], $this->dockerSecretsResolver); expect($env->get('DB_PASSWORD'))->toBe('db_secret_123'); expect($env->get('API_KEY'))->toBe('api_secret_456'); // Cleanup unlink($dbPasswordPath); unlink($apiKeyPath); }); }); describe('Edge Cases', function () { it('handles keys with underscores', function () { $env = new Environment([ 'SOME_LONG_KEY_NAME' => 'value', ], $this->dockerSecretsResolver); $value = $env->get('SOME_LONG_KEY_NAME'); expect($value)->toBe('value'); }); it('handles keys with numbers', function () { $env = new Environment([ 'VAR_123' => 'value', ], $this->dockerSecretsResolver); $value = $env->get('VAR_123'); expect($value)->toBe('value'); }); it('handles very long values', function () { $longValue = str_repeat('a', 10000); $env = new Environment([ 'LONG_VALUE' => $longValue, ], $this->dockerSecretsResolver); $value = $env->get('LONG_VALUE'); expect($value)->toBe($longValue); }); it('handles special characters in values', function () { $env = new Environment([ 'SPECIAL' => 'value!@#$%^&*()_+-=[]{}|;:\'",.<>?/`~', ], $this->dockerSecretsResolver); $value = $env->get('SPECIAL'); expect($value)->toBe('value!@#$%^&*()_+-=[]{}|;:\'",.<>?/`~'); }); it('handles unicode characters', function () { $env = new Environment([ 'UNICODE' => 'Hello δΈ–η•Œ 🌍', ], $this->dockerSecretsResolver); $value = $env->get('UNICODE'); expect($value)->toBe('Hello δΈ–η•Œ 🌍'); }); it('handles JSON strings as values', function () { $jsonString = '{"key":"value","nested":{"foo":"bar"}}'; $env = new Environment([ 'JSON_CONFIG' => $jsonString, ], $this->dockerSecretsResolver); $value = $env->get('JSON_CONFIG'); expect($value)->toBe($jsonString); expect(json_decode($value, true))->toBe([ 'key' => 'value', 'nested' => ['foo' => 'bar'], ]); }); it('handles URL strings as values', function () { $env = new Environment([ 'DATABASE_URL' => 'mysql://user:pass@localhost:3306/dbname', ], $this->dockerSecretsResolver); $value = $env->get('DATABASE_URL'); expect($value)->toBe('mysql://user:pass@localhost:3306/dbname'); }); it('handles base64-encoded values', function () { $base64Value = base64_encode('secret_data'); $env = new Environment([ 'ENCODED_SECRET' => $base64Value, ], $this->dockerSecretsResolver); $value = $env->get('ENCODED_SECRET'); expect($value)->toBe($base64Value); expect(base64_decode($value))->toBe('secret_data'); }); }); describe('Type Coercion Edge Cases', function () { it('handles non-numeric string for getInt', function () { $env = new Environment([ 'NOT_A_NUMBER' => 'abc', ], $this->dockerSecretsResolver); $value = $env->getInt('NOT_A_NUMBER'); expect($value)->toBe(0); // PHP's (int) cast behavior }); it('handles non-boolean string for getBool', function () { $env = new Environment([ 'NOT_A_BOOL' => 'maybe', ], $this->dockerSecretsResolver); $value = $env->getBool('NOT_A_BOOL'); expect($value)->toBeFalse(); // Non-empty string that's not "true" or "1" }); it('handles scientific notation for getFloat', function () { $env = new Environment([ 'SCIENTIFIC' => '1.5e3', ], $this->dockerSecretsResolver); $value = $env->getFloat('SCIENTIFIC'); expect($value)->toBe(1500.0); }); it('handles hexadecimal strings for getInt', function () { $env = new Environment([ 'HEX_VALUE' => '0xFF', ], $this->dockerSecretsResolver); $value = $env->getInt('HEX_VALUE'); expect($value)->toBe(0); // String "0xFF" casts to 0 }); }); }); // Test enums for getEnum() testing enum AppEnvironment: string { case DEVELOPMENT = 'development'; case STAGING = 'staging'; case PRODUCTION = 'production'; } enum TestIntEnum: int { case INACTIVE = 0; case ACTIVE = 1; case PENDING = 2; }