- 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.
360 lines
13 KiB
PHP
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');
|
|
});
|
|
});
|
|
});
|