- Introduce `EnumResolver` to centralize and cache enum value conversions. - Enhance `DockerSecretsResolver` with result caching to avoid redundant file reads and improve performance. - Update `Environment` to integrate `EnumResolver` for enriched enum resolution support and improved maintainability. - Adjust unit tests to validate caching mechanisms and error handling improvements.
818 lines
27 KiB
PHP
818 lines
27 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Config\DockerSecretsResolver;
|
|
use App\Framework\Config\EnvKey;
|
|
use App\Framework\Config\Environment;
|
|
use App\Framework\Exception\Config\EnvironmentVariableNotFoundException;
|
|
|
|
describe('Environment', function () {
|
|
beforeEach(function () {
|
|
// Create test environment variables
|
|
$this->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);
|
|
});
|
|
|
|
it('uses Docker secret when variable is set to empty string', function () {
|
|
// Create temporary Docker secret file
|
|
$secretPath = '/tmp/docker_secret_test';
|
|
file_put_contents($secretPath, 'secret_from_file');
|
|
|
|
$env = new Environment([
|
|
'REDIS_PASSWORD' => '', // Empty string
|
|
'REDIS_PASSWORD_FILE' => $secretPath,
|
|
], $this->dockerSecretsResolver);
|
|
|
|
$value = $env->get('REDIS_PASSWORD');
|
|
|
|
// Docker Secret should override empty string
|
|
expect($value)->toBe('secret_from_file');
|
|
|
|
// Cleanup
|
|
unlink($secretPath);
|
|
});
|
|
|
|
it('returns empty string when variable is empty and no Docker secret exists', function () {
|
|
$env = new Environment([
|
|
'REDIS_PASSWORD' => '', // Empty string, no FILE
|
|
], $this->dockerSecretsResolver);
|
|
|
|
$value = $env->get('REDIS_PASSWORD');
|
|
|
|
// Empty string should be returned when explicitly set
|
|
expect($value)->toBe('');
|
|
});
|
|
|
|
it('caches Docker secret resolution results', function () {
|
|
// Create temporary Docker secret file
|
|
$secretPath = '/tmp/docker_secret_test_caching';
|
|
file_put_contents($secretPath, 'cached_secret_value');
|
|
|
|
$env = new Environment([
|
|
'DB_PASSWORD_FILE' => $secretPath,
|
|
], $this->dockerSecretsResolver);
|
|
|
|
// First call - should read from file
|
|
$value1 = $env->get('DB_PASSWORD');
|
|
expect($value1)->toBe('cached_secret_value');
|
|
|
|
// Modify file content (should not affect cached value)
|
|
file_put_contents($secretPath, 'modified_secret_value');
|
|
|
|
// Second call - should use cache, not re-read file
|
|
$value2 = $env->get('DB_PASSWORD');
|
|
expect($value2)->toBe('cached_secret_value');
|
|
|
|
// Cleanup
|
|
unlink($secretPath);
|
|
});
|
|
|
|
it('caches enum conversion results', function () {
|
|
// Create a test enum
|
|
if (!enum_exists('TestEnum')) {
|
|
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
|
}
|
|
|
|
$env = new Environment([
|
|
'APP_ENV' => 'foo',
|
|
], $this->dockerSecretsResolver);
|
|
|
|
$default = TestEnum::BAR;
|
|
|
|
// First call - should convert
|
|
$enum1 = $env->getEnum('APP_ENV', TestEnum::class, $default);
|
|
expect($enum1)->toBe(TestEnum::FOO);
|
|
|
|
// Second call to same instance should use cache
|
|
$enum2 = $env->getEnum('APP_ENV', TestEnum::class, $default);
|
|
expect($enum2)->toBe(TestEnum::FOO);
|
|
expect($enum1)->toBe($enum2);
|
|
|
|
// Different enum class should have different cache entry
|
|
if (!enum_exists('AnotherTestEnum')) {
|
|
eval('enum AnotherTestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
|
}
|
|
$enum3 = $env->getEnum('APP_ENV', AnotherTestEnum::class, AnotherTestEnum::BAR);
|
|
expect($enum3)->toBe(AnotherTestEnum::FOO);
|
|
});
|
|
});
|
|
|
|
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;
|
|
}
|