feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
770
tests/Unit/Framework/Config/EncryptedEnvLoaderTest.php
Normal file
770
tests/Unit/Framework/Config/EncryptedEnvLoaderTest.php
Normal file
@@ -0,0 +1,770 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Config\EncryptedEnvLoader;
|
||||
use App\Framework\Config\EnvFileParser;
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
|
||||
describe('EncryptedEnvLoader', function () {
|
||||
beforeEach(function () {
|
||||
$this->encryptionFactory = new EncryptionFactory();
|
||||
$this->parser = new EnvFileParser();
|
||||
$this->loader = new EncryptedEnvLoader($this->encryptionFactory, $this->parser);
|
||||
|
||||
// Create temporary test directory
|
||||
$this->testDir = '/home/michael/dev/michaelschiemer/tests/tmp';
|
||||
if (!is_dir($this->testDir)) {
|
||||
mkdir($this->testDir, 0777, true);
|
||||
}
|
||||
|
||||
// Backup and clear real environment variables
|
||||
$this->originalEnv = $_ENV;
|
||||
$this->originalServer = $_SERVER;
|
||||
$_ENV = [];
|
||||
$_SERVER = [];
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Restore original environment
|
||||
$_ENV = $this->originalEnv;
|
||||
$_SERVER = $this->originalServer;
|
||||
|
||||
// Clean up test files
|
||||
if (isset($this->envFile) && file_exists($this->envFile)) {
|
||||
unlink($this->envFile);
|
||||
}
|
||||
if (isset($this->secretsFile) && file_exists($this->secretsFile)) {
|
||||
unlink($this->secretsFile);
|
||||
}
|
||||
if (isset($this->envProductionFile) && file_exists($this->envProductionFile)) {
|
||||
unlink($this->envProductionFile);
|
||||
}
|
||||
if (isset($this->envDevelopmentFile) && file_exists($this->envDevelopmentFile)) {
|
||||
unlink($this->envDevelopmentFile);
|
||||
}
|
||||
});
|
||||
|
||||
describe('load()', function () {
|
||||
it('loads environment from .env file', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=TestApp
|
||||
APP_ENV=development
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->get('APP_ENV'))->toBe('development');
|
||||
expect($env->get('DB_HOST'))->toBe('localhost');
|
||||
expect($env->getInt('DB_PORT'))->toBe(3306);
|
||||
});
|
||||
|
||||
it('loads environment with FilePath object', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=TestApp
|
||||
APP_ENV=development
|
||||
ENV);
|
||||
|
||||
$filePath = FilePath::create($this->testDir);
|
||||
$env = $this->loader->load($filePath);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->get('APP_ENV'))->toBe('development');
|
||||
});
|
||||
|
||||
it('returns empty environment when no .env file exists', function () {
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBeNull();
|
||||
});
|
||||
|
||||
it('performs two-pass loading when ENCRYPTION_KEY present', function () {
|
||||
// First pass: Load .env with ENCRYPTION_KEY
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=TestApp
|
||||
ENCRYPTION_KEY=test_encryption_key_32_chars_long
|
||||
ENV);
|
||||
|
||||
// Second pass: Load .env.secrets
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_API_KEY=my_secret_api_key
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
// Should have loaded from both files
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->get('ENCRYPTION_KEY'))->toBe('test_encryption_key_32_chars_long');
|
||||
expect($env->get('SECRET_API_KEY'))->toBe('my_secret_api_key');
|
||||
});
|
||||
|
||||
it('continues without secrets when encryption fails', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=TestApp
|
||||
ENCRYPTION_KEY=invalid_key
|
||||
ENV);
|
||||
|
||||
// Corrupted secrets file that will fail decryption
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, 'CORRUPTED_DATA');
|
||||
|
||||
// Should not throw - graceful degradation
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->get('ENCRYPTION_KEY'))->toBe('invalid_key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadEnvironment() - Production Priority', function () {
|
||||
it('prioritizes Docker ENV vars over .env file in production', function () {
|
||||
// Simulate Docker environment variables
|
||||
$_ENV['APP_ENV'] = 'production';
|
||||
$_ENV['DB_HOST'] = 'docker_mysql';
|
||||
$_ENV['DB_PORT'] = '3306';
|
||||
|
||||
// .env file with different values
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_ENV=production
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3307
|
||||
DB_NAME=mydb
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
// Docker ENV vars should take precedence
|
||||
expect($env->get('DB_HOST'))->toBe('docker_mysql');
|
||||
expect($env->getInt('DB_PORT'))->toBe(3306);
|
||||
|
||||
// .env values only used if not in Docker ENV
|
||||
expect($env->get('DB_NAME'))->toBe('mydb');
|
||||
});
|
||||
|
||||
it('loads environment-specific file in production', function () {
|
||||
$_ENV['APP_ENV'] = 'production';
|
||||
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=TestApp
|
||||
APP_ENV=production
|
||||
ENV);
|
||||
|
||||
$this->envProductionFile = $this->testDir . '/.env.production';
|
||||
file_put_contents($this->envProductionFile, <<<ENV
|
||||
PRODUCTION_SETTING=enabled
|
||||
DB_HOST=prod_host
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->get('PRODUCTION_SETTING'))->toBe('enabled');
|
||||
expect($env->get('DB_HOST'))->toBe('prod_host');
|
||||
});
|
||||
|
||||
it('does not override Docker ENV with production file', function () {
|
||||
$_ENV['APP_ENV'] = 'production';
|
||||
$_ENV['DB_HOST'] = 'docker_production_host';
|
||||
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, 'APP_ENV=production');
|
||||
|
||||
$this->envProductionFile = $this->testDir . '/.env.production';
|
||||
file_put_contents($this->envProductionFile, <<<ENV
|
||||
DB_HOST=file_production_host
|
||||
DB_PORT=3306
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
// Docker ENV should win
|
||||
expect($env->get('DB_HOST'))->toBe('docker_production_host');
|
||||
|
||||
// New variables from production file should be added
|
||||
expect($env->getInt('DB_PORT'))->toBe(3306);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadEnvironment() - Development Priority', function () {
|
||||
it('allows .env file to override system environment in development', function () {
|
||||
// Simulate system environment
|
||||
$_ENV['APP_ENV'] = 'development';
|
||||
$_ENV['DB_HOST'] = 'system_host';
|
||||
|
||||
// .env file with different values
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_ENV=development
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3307
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
// .env file should override in development
|
||||
expect($env->get('DB_HOST'))->toBe('localhost');
|
||||
expect($env->getInt('DB_PORT'))->toBe(3307);
|
||||
});
|
||||
|
||||
it('loads environment-specific file in development', function () {
|
||||
$_ENV['APP_ENV'] = 'development';
|
||||
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=TestApp
|
||||
APP_ENV=development
|
||||
ENV);
|
||||
|
||||
$this->envDevelopmentFile = $this->testDir . '/.env.development';
|
||||
file_put_contents($this->envDevelopmentFile, <<<ENV
|
||||
DEBUG=true
|
||||
DB_HOST=localhost
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->getBool('DEBUG'))->toBeTrue();
|
||||
expect($env->get('DB_HOST'))->toBe('localhost');
|
||||
});
|
||||
|
||||
it('allows development file to override .env in development', function () {
|
||||
$_ENV['APP_ENV'] = 'development';
|
||||
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_ENV=development
|
||||
DB_HOST=localhost
|
||||
DEBUG=false
|
||||
ENV);
|
||||
|
||||
$this->envDevelopmentFile = $this->testDir . '/.env.development';
|
||||
file_put_contents($this->envDevelopmentFile, <<<ENV
|
||||
DB_HOST=dev_override
|
||||
DEBUG=true
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
// Development file should override .env
|
||||
expect($env->get('DB_HOST'))->toBe('dev_override');
|
||||
expect($env->getBool('DEBUG'))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadSystemEnvironment() - getenv() Priority Fix', function () {
|
||||
it('loads from getenv() first (PHP-FPM compatibility)', function () {
|
||||
// Simulate PHP-FPM scenario where $_ENV is empty but getenv() works
|
||||
$_ENV = [];
|
||||
$_SERVER = [];
|
||||
|
||||
// getenv() returns values (simulated by putenv for test)
|
||||
putenv('TEST_VAR=from_getenv');
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
// Should have loaded from getenv()
|
||||
expect($env->get('TEST_VAR'))->toBe('from_getenv');
|
||||
|
||||
// Cleanup
|
||||
putenv('TEST_VAR');
|
||||
});
|
||||
|
||||
it('falls back to $_ENV when getenv() is empty', function () {
|
||||
$_ENV['FALLBACK_VAR'] = 'from_env_array';
|
||||
$_SERVER = [];
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
expect($env->get('FALLBACK_VAR'))->toBe('from_env_array');
|
||||
});
|
||||
|
||||
it('falls back to $_SERVER when both getenv() and $_ENV are empty', function () {
|
||||
$_ENV = [];
|
||||
$_SERVER['SERVER_VAR'] = 'from_server_array';
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
expect($env->get('SERVER_VAR'))->toBe('from_server_array');
|
||||
});
|
||||
|
||||
it('filters non-string values from $_SERVER', function () {
|
||||
$_ENV = [];
|
||||
$_SERVER['STRING_VAR'] = 'valid_string';
|
||||
$_SERVER['ARRAY_VAR'] = ['invalid', 'array'];
|
||||
$_SERVER['INT_VAR'] = 123; // Will be converted to string
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
expect($env->get('STRING_VAR'))->toBe('valid_string');
|
||||
expect($env->get('ARRAY_VAR'))->toBeNull(); // Filtered out
|
||||
});
|
||||
|
||||
it('prefers getenv() over $_ENV and $_SERVER', function () {
|
||||
// Simulate all three sources with different values
|
||||
putenv('PRIORITY_TEST=from_getenv');
|
||||
$_ENV['PRIORITY_TEST'] = 'from_env_array';
|
||||
$_SERVER['PRIORITY_TEST'] = 'from_server_array';
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
// getenv() should win
|
||||
expect($env->get('PRIORITY_TEST'))->toBe('from_getenv');
|
||||
|
||||
// Cleanup
|
||||
putenv('PRIORITY_TEST');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadEnvironment() - .env.secrets Support', function () {
|
||||
it('loads secrets file when encryption key provided', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, 'APP_ENV=development');
|
||||
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_API_KEY=my_secret
|
||||
SECRET_DB_PASSWORD=db_secret
|
||||
ENV);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir, $encryptionKey);
|
||||
|
||||
expect($env->get('SECRET_API_KEY'))->toBe('my_secret');
|
||||
expect($env->get('SECRET_DB_PASSWORD'))->toBe('db_secret');
|
||||
});
|
||||
|
||||
it('skips secrets file when encryption key is null', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, 'APP_ENV=development');
|
||||
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, 'SECRET_API_KEY=my_secret');
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir, encryptionKey: null);
|
||||
|
||||
expect($env->get('SECRET_API_KEY'))->toBeNull();
|
||||
});
|
||||
|
||||
it('skips secrets file when it does not exist', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, 'APP_ENV=development');
|
||||
|
||||
// No secrets file created
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
// Should not throw
|
||||
$env = $this->loader->loadEnvironment($this->testDir, $encryptionKey);
|
||||
|
||||
expect($env->get('APP_ENV'))->toBe('development');
|
||||
});
|
||||
|
||||
it('merges secrets with existing variables', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_ENV=development
|
||||
DB_HOST=localhost
|
||||
ENV);
|
||||
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_API_KEY=my_secret
|
||||
SECRET_DB_PASSWORD=db_secret
|
||||
ENV);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir, $encryptionKey);
|
||||
|
||||
// Base .env variables
|
||||
expect($env->get('APP_ENV'))->toBe('development');
|
||||
expect($env->get('DB_HOST'))->toBe('localhost');
|
||||
|
||||
// Secrets
|
||||
expect($env->get('SECRET_API_KEY'))->toBe('my_secret');
|
||||
expect($env->get('SECRET_DB_PASSWORD'))->toBe('db_secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSecretsTemplate()', function () {
|
||||
it('generates secrets template file', function () {
|
||||
$filePath = $this->loader->generateSecretsTemplate($this->testDir);
|
||||
|
||||
$this->secretsFile = $filePath->toString();
|
||||
|
||||
expect(file_exists($this->secretsFile))->toBeTrue();
|
||||
|
||||
$content = file_get_contents($this->secretsFile);
|
||||
|
||||
// Check header
|
||||
expect($content)->toContain('# .env.secrets - Encrypted secrets file');
|
||||
expect($content)->toContain('# Generated on');
|
||||
|
||||
// Check instructions
|
||||
expect($content)->toContain('# Instructions:');
|
||||
expect($content)->toContain('# 1. Set ENCRYPTION_KEY in your main .env file');
|
||||
|
||||
// Check example format
|
||||
expect($content)->toContain('# Example:');
|
||||
expect($content)->toContain('# SECRET_API_KEY=ENC[base64encodedencryptedvalue]');
|
||||
|
||||
// Check default secret keys
|
||||
expect($content)->toContain('# SECRET_ENCRYPTION_KEY=');
|
||||
expect($content)->toContain('# SECRET_DATABASE_PASSWORD=');
|
||||
expect($content)->toContain('# SECRET_API_KEY=');
|
||||
expect($content)->toContain('# SECRET_JWT_SECRET=');
|
||||
});
|
||||
|
||||
it('includes custom secret keys in template', function () {
|
||||
$customKeys = [
|
||||
'SECRET_STRIPE_KEY',
|
||||
'SECRET_AWS_ACCESS_KEY',
|
||||
'SECRET_MAILGUN_KEY'
|
||||
];
|
||||
|
||||
$filePath = $this->loader->generateSecretsTemplate($this->testDir, $customKeys);
|
||||
|
||||
$this->secretsFile = $filePath->toString();
|
||||
|
||||
$content = file_get_contents($this->secretsFile);
|
||||
|
||||
// Check custom keys are included
|
||||
expect($content)->toContain('# SECRET_STRIPE_KEY=');
|
||||
expect($content)->toContain('# SECRET_AWS_ACCESS_KEY=');
|
||||
expect($content)->toContain('# SECRET_MAILGUN_KEY=');
|
||||
|
||||
// Default keys should still be present
|
||||
expect($content)->toContain('# SECRET_API_KEY=');
|
||||
});
|
||||
|
||||
it('removes duplicate keys in template', function () {
|
||||
$customKeys = [
|
||||
'SECRET_API_KEY', // Duplicate of default
|
||||
'SECRET_CUSTOM_KEY'
|
||||
];
|
||||
|
||||
$filePath = $this->loader->generateSecretsTemplate($this->testDir, $customKeys);
|
||||
|
||||
$this->secretsFile = $filePath->toString();
|
||||
|
||||
$content = file_get_contents($this->secretsFile);
|
||||
|
||||
// Count occurrences of SECRET_API_KEY
|
||||
$count = substr_count($content, 'SECRET_API_KEY');
|
||||
|
||||
// Should only appear once (no duplicates)
|
||||
expect($count)->toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encryptSecretsInFile()', function () {
|
||||
it('encrypts secrets with SECRET_ prefix', function () {
|
||||
$this->secretsFile = $this->testDir . '/.env.test';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
# Test secrets file
|
||||
SECRET_API_KEY=my_plain_secret
|
||||
SECRET_DB_PASSWORD=plain_password
|
||||
NORMAL_VAR=not_encrypted
|
||||
ENV);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$encryptedCount = $this->loader->encryptSecretsInFile(
|
||||
$this->secretsFile,
|
||||
$encryptionKey
|
||||
);
|
||||
|
||||
expect($encryptedCount)->toBe(2);
|
||||
|
||||
$content = file_get_contents($this->secretsFile);
|
||||
|
||||
// Secrets should be encrypted (not plain text)
|
||||
expect($content)->not->toContain('my_plain_secret');
|
||||
expect($content)->not->toContain('plain_password');
|
||||
|
||||
// Normal var should remain unchanged
|
||||
expect($content)->toContain('NORMAL_VAR=not_encrypted');
|
||||
|
||||
// Comments should be preserved
|
||||
expect($content)->toContain('# Test secrets file');
|
||||
});
|
||||
|
||||
it('encrypts only specified keys when provided', function () {
|
||||
$this->secretsFile = $this->testDir . '/.env.test';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_API_KEY=my_plain_secret
|
||||
SECRET_DB_PASSWORD=plain_password
|
||||
SECRET_OTHER=other_secret
|
||||
ENV);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
$keysToEncrypt = ['SECRET_API_KEY', 'SECRET_OTHER'];
|
||||
|
||||
$encryptedCount = $this->loader->encryptSecretsInFile(
|
||||
$this->secretsFile,
|
||||
$encryptionKey,
|
||||
$keysToEncrypt
|
||||
);
|
||||
|
||||
expect($encryptedCount)->toBe(2);
|
||||
|
||||
$content = file_get_contents($this->secretsFile);
|
||||
|
||||
// Specified secrets should be encrypted
|
||||
expect($content)->not->toContain('my_plain_secret');
|
||||
expect($content)->not->toContain('other_secret');
|
||||
|
||||
// Non-specified secret should remain plain (but still there)
|
||||
expect($content)->toContain('SECRET_DB_PASSWORD=plain_password');
|
||||
});
|
||||
|
||||
it('skips already encrypted values', function () {
|
||||
$this->secretsFile = $this->testDir . '/.env.test';
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
$encryption = $this->encryptionFactory->createBest($encryptionKey);
|
||||
|
||||
$alreadyEncrypted = $encryption->encrypt('already_encrypted_value');
|
||||
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_API_KEY={$alreadyEncrypted}
|
||||
SECRET_DB_PASSWORD=plain_password
|
||||
ENV);
|
||||
|
||||
$encryptedCount = $this->loader->encryptSecretsInFile(
|
||||
$this->secretsFile,
|
||||
$encryptionKey
|
||||
);
|
||||
|
||||
// Only 1 should be encrypted (the plain one)
|
||||
expect($encryptedCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('returns 0 when file does not exist', function () {
|
||||
$nonExistentFile = $this->testDir . '/non_existent.env';
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$encryptedCount = $this->loader->encryptSecretsInFile(
|
||||
$nonExistentFile,
|
||||
$encryptionKey
|
||||
);
|
||||
|
||||
expect($encryptedCount)->toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 when file is not readable', function () {
|
||||
$this->secretsFile = $this->testDir . '/.env.unreadable';
|
||||
file_put_contents($this->secretsFile, 'SECRET_KEY=value');
|
||||
chmod($this->secretsFile, 0000);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$encryptedCount = $this->loader->encryptSecretsInFile(
|
||||
$this->secretsFile,
|
||||
$encryptionKey
|
||||
);
|
||||
|
||||
expect($encryptedCount)->toBe(0);
|
||||
|
||||
// Restore permissions for cleanup
|
||||
chmod($this->secretsFile, 0644);
|
||||
});
|
||||
|
||||
it('removes quotes before encryption', function () {
|
||||
$this->secretsFile = $this->testDir . '/.env.test';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_API_KEY="quoted_value"
|
||||
SECRET_DB_PASSWORD='single_quoted'
|
||||
ENV);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$encryptedCount = $this->loader->encryptSecretsInFile(
|
||||
$this->secretsFile,
|
||||
$encryptionKey
|
||||
);
|
||||
|
||||
expect($encryptedCount)->toBe(2);
|
||||
|
||||
// Values should be encrypted without quotes
|
||||
$content = file_get_contents($this->secretsFile);
|
||||
expect($content)->not->toContain('"quoted_value"');
|
||||
expect($content)->not->toContain("'single_quoted'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEncryptionSetup()', function () {
|
||||
it('returns no issues when setup is valid', function () {
|
||||
$encryptionKey = str_repeat('a', 32); // 32 characters
|
||||
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, 'SECRET_KEY=value');
|
||||
|
||||
$issues = $this->loader->validateEncryptionSetup($this->testDir, $encryptionKey);
|
||||
|
||||
expect($issues)->toBe([]);
|
||||
});
|
||||
|
||||
it('reports missing encryption key', function () {
|
||||
$issues = $this->loader->validateEncryptionSetup($this->testDir, encryptionKey: null);
|
||||
|
||||
expect($issues)->toHaveCount(1);
|
||||
expect($issues[0]['type'])->toBe('missing_key');
|
||||
expect($issues[0]['severity'])->toBe('high');
|
||||
expect($issues[0]['message'])->toBe('No encryption key provided');
|
||||
});
|
||||
|
||||
it('reports weak encryption key', function () {
|
||||
$weakKey = 'short_key'; // Less than 32 characters
|
||||
|
||||
$issues = $this->loader->validateEncryptionSetup($this->testDir, $weakKey);
|
||||
|
||||
expect($issues)->toHaveCount(1);
|
||||
expect($issues[0]['type'])->toBe('weak_key');
|
||||
expect($issues[0]['severity'])->toBe('high');
|
||||
expect($issues[0]['message'])->toContain('at least 32 characters');
|
||||
});
|
||||
|
||||
it('reports missing secrets file', function () {
|
||||
$encryptionKey = str_repeat('a', 32);
|
||||
|
||||
// No secrets file created
|
||||
|
||||
$issues = $this->loader->validateEncryptionSetup($this->testDir, $encryptionKey);
|
||||
|
||||
expect($issues)->toHaveCount(1);
|
||||
expect($issues[0]['type'])->toBe('missing_secrets_file');
|
||||
expect($issues[0]['severity'])->toBe('medium');
|
||||
expect($issues[0]['message'])->toBe('.env.secrets file not found');
|
||||
});
|
||||
|
||||
it('reports multiple issues', function () {
|
||||
$weakKey = 'weak'; // Weak key
|
||||
// No secrets file
|
||||
|
||||
$issues = $this->loader->validateEncryptionSetup($this->testDir, $weakKey);
|
||||
|
||||
expect($issues)->toHaveCount(2);
|
||||
|
||||
$types = array_column($issues, 'type');
|
||||
expect($types)->toContain('weak_key');
|
||||
expect($types)->toContain('missing_secrets_file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', function () {
|
||||
it('handles empty .env file', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, '');
|
||||
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBeNull();
|
||||
});
|
||||
|
||||
it('handles .env file with only comments', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
# Comment line 1
|
||||
# Comment line 2
|
||||
# Comment line 3
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBeNull();
|
||||
});
|
||||
|
||||
it('handles .env file with blank lines', function () {
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
|
||||
APP_NAME=TestApp
|
||||
|
||||
|
||||
DB_HOST=localhost
|
||||
|
||||
ENV);
|
||||
|
||||
$env = $this->loader->load($this->testDir);
|
||||
|
||||
expect($env->get('APP_NAME'))->toBe('TestApp');
|
||||
expect($env->get('DB_HOST'))->toBe('localhost');
|
||||
});
|
||||
|
||||
it('re-checks APP_ENV after .env loading', function () {
|
||||
// System says production
|
||||
$_ENV['APP_ENV'] = 'production';
|
||||
|
||||
// But .env overrides to development (in development mode, this works)
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_ENV=development
|
||||
ENV);
|
||||
|
||||
$this->envDevelopmentFile = $this->testDir . '/.env.development';
|
||||
file_put_contents($this->envDevelopmentFile, <<<ENV
|
||||
DEV_SETTING=enabled
|
||||
ENV);
|
||||
|
||||
// Since .env says development, should load .env.development
|
||||
$env = $this->loader->loadEnvironment($this->testDir);
|
||||
|
||||
expect($env->get('APP_ENV'))->toBe('production'); // System ENV wins in production check
|
||||
// But development file should be loaded based on re-check
|
||||
});
|
||||
|
||||
it('handles multiple environment file layers', function () {
|
||||
$_ENV['APP_ENV'] = 'development';
|
||||
|
||||
// Layer 1: Base .env
|
||||
$this->envFile = $this->testDir . '/.env';
|
||||
file_put_contents($this->envFile, <<<ENV
|
||||
APP_NAME=BaseApp
|
||||
DB_HOST=localhost
|
||||
DEBUG=false
|
||||
ENV);
|
||||
|
||||
// Layer 2: Environment-specific
|
||||
$this->envDevelopmentFile = $this->testDir . '/.env.development';
|
||||
file_put_contents($this->envDevelopmentFile, <<<ENV
|
||||
DB_HOST=dev_host
|
||||
DEBUG=true
|
||||
ENV);
|
||||
|
||||
// Layer 3: Secrets
|
||||
$this->secretsFile = $this->testDir . '/.env.secrets';
|
||||
file_put_contents($this->secretsFile, <<<ENV
|
||||
SECRET_KEY=my_secret
|
||||
ENV);
|
||||
|
||||
$encryptionKey = 'test_encryption_key_32_chars_long';
|
||||
|
||||
$env = $this->loader->loadEnvironment($this->testDir, $encryptionKey);
|
||||
|
||||
// Base layer
|
||||
expect($env->get('APP_NAME'))->toBe('BaseApp');
|
||||
|
||||
// Override by development
|
||||
expect($env->get('DB_HOST'))->toBe('dev_host');
|
||||
expect($env->getBool('DEBUG'))->toBeTrue();
|
||||
|
||||
// Secrets layer
|
||||
expect($env->get('SECRET_KEY'))->toBe('my_secret');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user