refactor(config): add EnumResolver for cache-backed enum resolution and extend DockerSecretsResolver with caching
- 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.
This commit is contained in:
146
tests/Unit/Framework/Config/EnumResolverTest.php
Normal file
146
tests/Unit/Framework/Config/EnumResolverTest.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Config\EnumResolver;
|
||||
use BackedEnum;
|
||||
|
||||
describe('EnumResolver', function () {
|
||||
beforeEach(function () {
|
||||
$this->resolver = new EnumResolver();
|
||||
});
|
||||
|
||||
describe('resolve()', function () {
|
||||
it('converts string value to enum', function () {
|
||||
// Create a test enum
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default = TestEnum::BAR;
|
||||
$result = $this->resolver->resolve('foo', TestEnum::class, $default);
|
||||
|
||||
expect($result)->toBe(TestEnum::FOO);
|
||||
});
|
||||
|
||||
it('converts int value to enum', function () {
|
||||
// Create a test enum with int backing
|
||||
if (!enum_exists('IntTestEnum')) {
|
||||
eval('enum IntTestEnum: int { case ONE = 1; case TWO = 2; }');
|
||||
}
|
||||
|
||||
$default = IntTestEnum::TWO;
|
||||
$result = $this->resolver->resolve(1, IntTestEnum::class, $default);
|
||||
|
||||
expect($result)->toBe(IntTestEnum::ONE);
|
||||
});
|
||||
|
||||
it('returns default when value cannot be converted', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default = TestEnum::BAR;
|
||||
$result = $this->resolver->resolve('invalid', TestEnum::class, $default);
|
||||
|
||||
expect($result)->toBe($default);
|
||||
});
|
||||
|
||||
it('returns default when value is null', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default = TestEnum::BAR;
|
||||
$result = $this->resolver->resolve(null, TestEnum::class, $default);
|
||||
|
||||
expect($result)->toBe($default);
|
||||
});
|
||||
|
||||
it('throws exception when default is not instance of enum class', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
if (!enum_exists('AnotherEnum')) {
|
||||
eval('enum AnotherEnum: string { case BAZ = "baz"; }');
|
||||
}
|
||||
|
||||
$wrongDefault = AnotherEnum::BAZ;
|
||||
|
||||
expect(function () use ($wrongDefault) {
|
||||
$this->resolver->resolve('foo', TestEnum::class, $wrongDefault);
|
||||
})->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('caching', function () {
|
||||
it('caches enum conversion results', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default = TestEnum::BAR;
|
||||
|
||||
// First call - should convert
|
||||
$enum1 = $this->resolver->resolve('foo', TestEnum::class, $default);
|
||||
expect($enum1)->toBe(TestEnum::FOO);
|
||||
|
||||
// Second call to same resolver instance should use cache
|
||||
// (We can't directly verify cache, but we can verify it returns the same instance)
|
||||
$enum2 = $this->resolver->resolve('foo', TestEnum::class, $default);
|
||||
expect($enum2)->toBe(TestEnum::FOO);
|
||||
expect($enum1)->toBe($enum2);
|
||||
});
|
||||
|
||||
it('caches different enum classes separately', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
if (!enum_exists('AnotherTestEnum')) {
|
||||
eval('enum AnotherTestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default1 = TestEnum::BAR;
|
||||
$default2 = AnotherTestEnum::BAR;
|
||||
|
||||
$enum1 = $this->resolver->resolve('foo', TestEnum::class, $default1);
|
||||
$enum2 = $this->resolver->resolve('foo', AnotherTestEnum::class, $default2);
|
||||
|
||||
expect($enum1)->toBe(TestEnum::FOO);
|
||||
expect($enum2)->toBe(AnotherTestEnum::FOO);
|
||||
expect($enum1)->not->toBe($enum2); // Different enum classes
|
||||
});
|
||||
|
||||
it('caches different values separately', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default = TestEnum::BAR;
|
||||
|
||||
$enum1 = $this->resolver->resolve('foo', TestEnum::class, $default);
|
||||
$enum2 = $this->resolver->resolve('bar', TestEnum::class, $default);
|
||||
|
||||
expect($enum1)->toBe(TestEnum::FOO);
|
||||
expect($enum2)->toBe(TestEnum::BAR);
|
||||
});
|
||||
|
||||
it('caches default values', function () {
|
||||
if (!enum_exists('TestEnum')) {
|
||||
eval('enum TestEnum: string { case FOO = "foo"; case BAR = "bar"; }');
|
||||
}
|
||||
|
||||
$default = TestEnum::BAR;
|
||||
|
||||
// First call with null
|
||||
$enum1 = $this->resolver->resolve(null, TestEnum::class, $default);
|
||||
expect($enum1)->toBe($default);
|
||||
|
||||
// Second call with null should return cached default
|
||||
$enum2 = $this->resolver->resolve(null, TestEnum::class, $default);
|
||||
expect($enum2)->toBe($default);
|
||||
expect($enum1)->toBe($enum2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -583,6 +583,89 @@ describe('Environment', function () {
|
||||
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 () {
|
||||
|
||||
Reference in New Issue
Block a user