$this->name, 'value' => $this->value, ]; } public static function fromArray(array $data): self { return new self( name: $data['name'] ?? '', value: $data['value'] ?? 0 ); } } describe('EncryptionTransformer', function () { beforeEach(function () { $random = new SecureRandomGenerator(); $crypto = new CryptographicUtilities($random); $encryptionKey = $random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $encryptor = new StateEncryptor($encryptionKey, $crypto, $random); $serializer = new EncryptedStateSerializer($encryptor); $this->transformer = new EncryptionTransformer( $serializer, TransformerTestState::class ); $this->testState = new TransformerTestState( name: 'Test User', value: 42 ); }); it('has correct name', function () { expect($this->transformer->getName())->toBe('encryption'); }); it('has medium priority', function () { expect($this->transformer->getPriority())->toBe(50); }); it('transforms state to encrypted string', function () { $transformed = $this->transformer->transformIn($this->testState); expect($transformed)->toBeString(); expect($transformed)->not->toBeEmpty(); // Should be base64-encoded encrypted data $decoded = base64_decode($transformed, strict: true); expect($decoded)->not->toBeFalse(); }); it('transforms encrypted string back to state', function () { $encrypted = $this->transformer->transformIn($this->testState); $state = $this->transformer->transformOut($encrypted); expect($state)->toBeInstanceOf(TransformerTestState::class); expect($state->name)->toBe('Test User'); expect($state->value)->toBe(42); }); it('round-trip transformation preserves state', function () { $original = new TransformerTestState( name: 'Round Trip Test', value: 999 ); $encrypted = $this->transformer->transformIn($original); $restored = $this->transformer->transformOut($encrypted); expect($restored)->toBeInstanceOf(TransformerTestState::class); expect($restored->name)->toBe($original->name); expect($restored->value)->toBe($original->value); }); it('throws exception for non-string data in transformOut', function () { $this->transformer->transformOut(['not' => 'string']); })->throws(\InvalidArgumentException::class, 'expects string data'); it('throws exception for non-SerializableState in transformOut', function () { // Create invalid encrypted data that decrypts to non-SerializableState // This would happen if stateClass is wrong $transformer = new EncryptionTransformer( $this->transformer->serializer, \stdClass::class // Wrong class ); $encrypted = $this->transformer->transformIn($this->testState); $transformer->transformOut($encrypted); })->throws(\RuntimeException::class, 'must implement SerializableState'); it('produces different encrypted output for same state', function () { $encrypted1 = $this->transformer->transformIn($this->testState); $encrypted2 = $this->transformer->transformIn($this->testState); // Should be different due to unique nonces expect($encrypted1)->not->toBe($encrypted2); // But both should decrypt to same state $state1 = $this->transformer->transformOut($encrypted1); $state2 = $this->transformer->transformOut($encrypted2); expect($state1->name)->toBe($state2->name); expect($state1->value)->toBe($state2->value); }); it('handles state with special characters', function () { $state = new TransformerTestState( name: "Special: 🔐 ", value: 123 ); $encrypted = $this->transformer->transformIn($state); $restored = $this->transformer->transformOut($encrypted); expect($restored->name)->toBe($state->name); }); }); describe('EncryptionTransformer Integration', function () { it('works in transformer pipeline with multiple transformers', function () { // Simulate transformer pipeline $random = new SecureRandomGenerator(); $crypto = new CryptographicUtilities($random); $encryptionKey = $random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $encryptor = new StateEncryptor($encryptionKey, $crypto, $random); $serializer = new EncryptedStateSerializer($encryptor); $encryptionTransformer = new EncryptionTransformer( $serializer, TransformerTestState::class ); $state = new TransformerTestState('Pipeline Test', 789); // transformIn pipeline (high to low priority) $data = $encryptionTransformer->transformIn($state); // transformOut pipeline (reverse order) $restored = $encryptionTransformer->transformOut($data); expect($restored)->toBeInstanceOf(TransformerTestState::class); expect($restored->name)->toBe('Pipeline Test'); expect($restored->value)->toBe(789); }); it('maintains type safety through transformation', function () { $random = new SecureRandomGenerator(); $crypto = new CryptographicUtilities($random); $encryptionKey = $random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $encryptor = new StateEncryptor($encryptionKey, $crypto, $random); $serializer = new EncryptedStateSerializer($encryptor); $transformer = new EncryptionTransformer( $serializer, TransformerTestState::class ); $state = new TransformerTestState('Type Safety', 0); $encrypted = $transformer->transformIn($state); $restored = $transformer->transformOut($encrypted); // Type checks expect($restored)->toBeInstanceOf(SerializableState::class); expect($restored)->toBeInstanceOf(TransformerTestState::class); expect($restored->value)->toBeInt(); expect($restored->name)->toBeString(); }); });