771 lines
26 KiB
PHP
771 lines
26 KiB
PHP
<?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');
|
|
});
|
|
});
|
|
});
|