Files
michaelschiemer/tests/Integration/Framework/LiveComponents/StateEncryptionIntegrationTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

360 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/**
* State Encryption Integration Tests
*
* Integration tests for state encryption with security layers.
* Tests the complete encryption flow from serialization to cache storage.
*/
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheItem;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\LiveComponents\Serialization\EncryptedStateSerializer;
use App\Framework\LiveComponents\Serialization\StateEncryptor;
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\StateManagement\SerializableState;
// Test State with Sensitive Data
final readonly class PaymentState implements SerializableState
{
public function __construct(
public string $paymentId,
public string $cardNumber, // Sensitive - PCI data
public string $cvv, // Sensitive - must be encrypted
public float $amount,
public string $currency
) {}
public function toArray(): array
{
return [
'payment_id' => $this->paymentId,
'card_number' => $this->cardNumber,
'cvv' => $this->cvv,
'amount' => $this->amount,
'currency' => $this->currency,
];
}
public static function fromArray(array $data): self
{
return new self(
paymentId: $data['payment_id'],
cardNumber: $data['card_number'],
cvv: $data['cvv'],
amount: $data['amount'],
currency: $data['currency']
);
}
}
describe('State Encryption Integration', function () {
beforeEach(function () {
// Setup encryption infrastructure
$this->random = new SecureRandomGenerator();
$this->crypto = new CryptographicUtilities($this->random);
$this->encryptionKey = $this->random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$this->encryptor = new StateEncryptor(
$this->encryptionKey,
$this->crypto,
$this->random
);
$this->serializer = new EncryptedStateSerializer($this->encryptor);
$this->cache = new InMemoryCache();
// Test payment state
$this->paymentState = new PaymentState(
paymentId: 'pay_12345',
cardNumber: '4111111111111111',
cvv: '123',
amount: 99.99,
currency: 'USD'
);
});
describe('End-to-End Encryption Flow', function () {
it('encrypts state and stores in cache', function () {
// 1. Encrypt state
$encrypted = $this->serializer->serialize($this->paymentState);
// 2. Store in cache
$cacheKey = CacheKey::fromString('payment:session:abc123');
$cacheItem = CacheItem::forSet(
key: $cacheKey,
value: $encrypted,
ttl: Duration::fromMinutes(15)
);
$this->cache->set($cacheItem);
// 3. Retrieve from cache
$retrieved = $this->cache->get($cacheKey);
expect($retrieved->isHit)->toBeTrue();
expect($retrieved->value)->toBeString();
// 4. Verify encrypted (no plaintext)
expect($retrieved->value)->not->toContain('4111111111111111');
expect($retrieved->value)->not->toContain('123');
});
it('decrypts state correctly from cache', function () {
// Encrypt and store
$encrypted = $this->serializer->serialize($this->paymentState);
$cacheKey = CacheKey::fromString('payment:session:xyz789');
$this->cache->set(CacheItem::forSet(
key: $cacheKey,
value: $encrypted
));
// Retrieve and decrypt
$cached = $this->cache->get($cacheKey);
$decrypted = $this->serializer->deserialize(
$cached->value,
PaymentState::class
);
// Verify decryption
expect($decrypted)->toBeInstanceOf(PaymentState::class);
expect($decrypted->paymentId)->toBe('pay_12345');
expect($decrypted->cardNumber)->toBe('4111111111111111');
expect($decrypted->cvv)->toBe('123');
expect($decrypted->amount)->toBe(99.99);
});
it('handles cache expiration correctly', function () {
// Store with short TTL
$encrypted = $this->serializer->serialize($this->paymentState);
$cacheKey = CacheKey::fromString('payment:session:expired');
// Manually expire by not setting (or setting expired)
// InMemoryCache doesn't auto-expire, so we simulate by not storing
$retrieved = $this->cache->get($cacheKey);
expect($retrieved->isHit)->toBeFalse();
});
});
describe('Security Properties', function () {
it('prevents tampering with MAC verification', function () {
// Encrypt and store
$encrypted = $this->serializer->serialize($this->paymentState);
$cacheKey = CacheKey::fromString('payment:tamper-test');
$this->cache->set(CacheItem::forSet(
key: $cacheKey,
value: $encrypted
));
// Retrieve and tamper
$cached = $this->cache->get($cacheKey);
$tampered = substr($cached->value, 0, -5) . 'XXXXX';
// Store tampered data
$this->cache->set(CacheItem::forSet(
key: $cacheKey,
value: $tampered
));
// Attempt to decrypt should fail
$tamperedData = $this->cache->get($cacheKey);
expect(fn() => $this->serializer->deserialize(
$tamperedData->value,
PaymentState::class
))->toThrow(StateEncryptionException::class);
});
it('uses unique nonces for each encryption', function () {
// Encrypt same state twice
$encrypted1 = $this->serializer->serialize($this->paymentState);
$encrypted2 = $this->serializer->serialize($this->paymentState);
// Should be different due to unique nonces
expect($encrypted1)->not->toBe($encrypted2);
// Both should decrypt to same state
$decrypted1 = $this->serializer->deserialize($encrypted1, PaymentState::class);
$decrypted2 = $this->serializer->deserialize($encrypted2, PaymentState::class);
expect($decrypted1->cardNumber)->toBe($decrypted2->cardNumber);
});
it('does not leak sensitive data in cache storage', function () {
// Store multiple encrypted states
for ($i = 1; $i <= 5; $i++) {
$state = new PaymentState(
paymentId: "pay_{$i}",
cardNumber: '4111111111111111',
cvv: '123',
amount: 100.00 * $i,
currency: 'USD'
);
$encrypted = $this->serializer->serialize($state);
$cacheKey = CacheKey::fromString("payment:session:{$i}");
$this->cache->set(CacheItem::forSet(
key: $cacheKey,
value: $encrypted
));
}
// Scan all cache entries
$allKeys = $this->cache->scan('payment:session:*');
foreach ($allKeys as $keyString) {
$cacheKey = CacheKey::fromString($keyString);
$item = $this->cache->get($cacheKey);
// No plaintext card data should be visible
expect($item->value)->not->toContain('4111111111111111');
expect($item->value)->not->toContain('123');
}
});
});
describe('Performance Characteristics', function () {
it('maintains acceptable encryption overhead', function () {
$startTime = microtime(true);
for ($i = 0; $i < 100; $i++) {
$encrypted = $this->serializer->serialize($this->paymentState);
$this->serializer->deserialize($encrypted, PaymentState::class);
}
$duration = (microtime(true) - $startTime) * 1000;
$avgTimeMs = $duration / 100;
// Should be < 2ms per encrypt+decrypt cycle (as per docs)
expect($avgTimeMs)->toBeLessThan(2.0);
});
it('handles concurrent cache operations', function () {
// Simulate concurrent writes
$operations = [];
for ($i = 0; $i < 10; $i++) {
$state = new PaymentState(
paymentId: "concurrent_{$i}",
cardNumber: '4111111111111111',
cvv: '123',
amount: 50.00,
currency: 'USD'
);
$encrypted = $this->serializer->serialize($state);
$cacheKey = CacheKey::fromString("payment:concurrent:{$i}");
$this->cache->set(CacheItem::forSet(
key: $cacheKey,
value: $encrypted
));
$operations[] = ['key' => $cacheKey, 'paymentId' => "concurrent_{$i}"];
}
// Verify all operations succeeded
foreach ($operations as $op) {
$cached = $this->cache->get($op['key']);
$decrypted = $this->serializer->deserialize(
$cached->value,
PaymentState::class
);
expect($decrypted->paymentId)->toBe($op['paymentId']);
}
});
});
describe('Error Handling', function () {
it('throws StateEncryptionException on decryption failure', function () {
// Store corrupted data
$cacheKey = CacheKey::fromString('payment:corrupted');
$this->cache->set(CacheItem::forSet(
key: $cacheKey,
value: 'corrupted-encrypted-data'
));
$cached = $this->cache->get($cacheKey);
expect(fn() => $this->serializer->deserialize(
$cached->value,
PaymentState::class
))->toThrow(StateEncryptionException::class);
});
it('does not leak encryption key in error messages', function () {
try {
$this->serializer->deserialize(
'invalid-data',
PaymentState::class
);
} catch (StateEncryptionException $e) {
// Error message should not contain encryption key
expect($e->getMessage())->not->toContain(bin2hex($this->encryptionKey));
}
});
it('handles invalid state class gracefully', function () {
$encrypted = $this->serializer->serialize($this->paymentState);
expect(fn() => $this->serializer->deserialize(
$encrypted,
\stdClass::class
))->toThrow(StateEncryptionException::class);
});
});
describe('Multi-Component Isolation', function () {
it('maintains separate encrypted states per component', function () {
$component1Key = CacheKey::fromString('payment:component1');
$component2Key = CacheKey::fromString('payment:component2');
$state1 = new PaymentState(
paymentId: 'pay_comp1',
cardNumber: '4111111111111111',
cvv: '111',
amount: 50.00,
currency: 'USD'
);
$state2 = new PaymentState(
paymentId: 'pay_comp2',
cardNumber: '5555555555554444',
cvv: '222',
amount: 75.00,
currency: 'EUR'
);
// Encrypt and store both
$encrypted1 = $this->serializer->serialize($state1);
$encrypted2 = $this->serializer->serialize($state2);
$this->cache->set(CacheItem::forSet(key: $component1Key, value: $encrypted1));
$this->cache->set(CacheItem::forSet(key: $component2Key, value: $encrypted2));
// Verify different encrypted data
$cached1 = $this->cache->get($component1Key);
$cached2 = $this->cache->get($component2Key);
expect($cached1->value)->not->toBe($cached2->value);
// Decrypt and verify correct isolation
$decrypted1 = $this->serializer->deserialize($cached1->value, PaymentState::class);
$decrypted2 = $this->serializer->deserialize($cached2->value, PaymentState::class);
expect($decrypted1->paymentId)->toBe('pay_comp1');
expect($decrypted1->currency)->toBe('USD');
expect($decrypted2->paymentId)->toBe('pay_comp2');
expect($decrypted2->currency)->toBe('EUR');
});
});
});