""" Tests for app/utils/encryption.py Tests encryption/decryption functionality for sensitive settings. """ from unittest.mock import Mock, patch import pytest @pytest.mark.unit class TestEncryption: """Test encryption utility functions""" def test_encrypt_value_with_none(self): """Test that None values are returned as-is""" from app.utils.encryption import encrypt_value result = encrypt_value(None) assert result is None def test_encrypt_value_with_empty_string(self): """Test that empty strings are returned as-is""" from app.utils.encryption import encrypt_value result = encrypt_value("") assert result == "" @patch("app.utils.encryption._get_cipher_suite") def test_encrypt_value_when_encryption_unavailable(self, mock_cipher): """Test that plaintext is returned when encryption is unavailable""" from app.utils.encryption import encrypt_value mock_cipher.return_value = None result = encrypt_value("secret_value") # Should return plaintext with warning logged assert result == "secret_value" @patch("app.utils.encryption._get_cipher_suite") def test_encrypt_value_success(self, mock_cipher): """Test successful encryption""" from app.utils.encryption import encrypt_value # Mock cipher that returns encrypted bytes mock_fernet = Mock() mock_fernet.encrypt.return_value = b"encrypted_data" mock_cipher.return_value = mock_fernet result = encrypt_value("secret_value") # Should have "enc:" prefix assert result.startswith("enc:") assert "encrypted_data" in result mock_fernet.encrypt.assert_called_once() @patch("app.utils.encryption._get_cipher_suite") def test_encrypt_value_encryption_failure(self, mock_cipher): """Test that encryption failures fall back to plaintext""" from app.utils.encryption import encrypt_value # Mock cipher that raises exception mock_fernet = Mock() mock_fernet.encrypt.side_effect = Exception("Encryption error") mock_cipher.return_value = mock_fernet result = encrypt_value("secret_value") # Should fall back to plaintext assert result == "secret_value" def test_decrypt_value_with_none(self): """Test that None values are returned as-is""" from app.utils.encryption import decrypt_value result = decrypt_value(None) assert result is None def test_decrypt_value_with_empty_string(self): """Test that empty strings are returned as-is""" from app.utils.encryption import decrypt_value result = decrypt_value("") assert result == "" def test_decrypt_value_plaintext(self): """Test that plaintext values without enc: prefix are returned as-is""" from app.utils.encryption import decrypt_value result = decrypt_value("plain_value") assert result == "plain_value" @patch("app.utils.encryption._get_cipher_suite") def test_decrypt_value_when_encryption_unavailable(self, mock_cipher): """Test decryption when cipher is unavailable""" from app.utils.encryption import decrypt_value mock_cipher.return_value = None result = decrypt_value("enc:encrypted_data") # Should return error message assert result == "[ENCRYPTED - Cannot decrypt]" @patch("app.utils.encryption._get_cipher_suite") def test_decrypt_value_success(self, mock_cipher): """Test successful decryption""" from app.utils.encryption import decrypt_value # Mock cipher that returns decrypted bytes mock_fernet = Mock() mock_fernet.decrypt.return_value = b"decrypted_value" mock_cipher.return_value = mock_fernet result = decrypt_value("enc:encrypted_data") assert result == "decrypted_value" mock_fernet.decrypt.assert_called_once() @patch("app.utils.encryption._get_cipher_suite") def test_decrypt_value_decryption_failure(self, mock_cipher): """Test that decryption failures return error message""" from app.utils.encryption import decrypt_value # Mock cipher that raises exception mock_fernet = Mock() mock_fernet.decrypt.side_effect = Exception("Decryption error") mock_cipher.return_value = mock_fernet result = decrypt_value("enc:bad_data") # Should return error message assert result == "[DECRYPTION FAILED]" def test_is_encrypted_with_encrypted_value(self): """Test is_encrypted returns True for encrypted values""" from app.utils.encryption import is_encrypted assert is_encrypted("enc:some_encrypted_data") is True def test_is_encrypted_with_plaintext(self): """Test is_encrypted returns False for plaintext""" from app.utils.encryption import is_encrypted assert is_encrypted("plain_value") is False def test_is_encrypted_with_none(self): """Test is_encrypted returns False for None""" from app.utils.encryption import is_encrypted assert is_encrypted(None) is False def test_is_encrypted_with_empty_string(self): """Test is_encrypted returns False for empty string""" from app.utils.encryption import is_encrypted assert is_encrypted("") is False def test_is_encrypted_with_non_string(self): """Test is_encrypted returns False for non-string types""" from app.utils.encryption import is_encrypted assert is_encrypted(123) is False assert is_encrypted([]) is False assert is_encrypted({}) is False @patch("app.utils.encryption._get_cipher_suite") def test_is_encryption_available_true(self, mock_cipher): """Test is_encryption_available when cryptography is available""" from app.utils.encryption import is_encryption_available mock_cipher.return_value = Mock() # Non-None cipher assert is_encryption_available() is True @patch("app.utils.encryption._get_cipher_suite") def test_is_encryption_available_false(self, mock_cipher): """Test is_encryption_available when cryptography is not available""" from app.utils.encryption import is_encryption_available mock_cipher.return_value = None assert is_encryption_available() is False @pytest.mark.unit class TestGetCipherSuite: """Test the _get_cipher_suite internal function""" def test_cipher_suite_caching(self): """Test that cipher suite is cached after first call""" import app.utils.encryption # First call result1 = app.utils.encryption._get_cipher_suite() # Second call should return same instance (cached) result2 = app.utils.encryption._get_cipher_suite() # Both calls should return the same object (cached) assert result1 is result2 @pytest.mark.unit class TestEncryptionIntegration: """Integration tests for encrypt/decrypt cycle""" @patch("app.utils.encryption._get_cipher_suite") def test_encrypt_decrypt_cycle(self, mock_cipher): """Test that encrypting and then decrypting returns original value""" from app.utils.encryption import decrypt_value, encrypt_value # Mock a simple reversible encryption mock_fernet = Mock() # Simulate encryption: just add a prefix def mock_encrypt(data): return b"ENCRYPTED_" + data # Simulate decryption: remove the prefix def mock_decrypt(data): return data.replace(b"ENCRYPTED_", b"") mock_fernet.encrypt = mock_encrypt mock_fernet.decrypt = mock_decrypt mock_cipher.return_value = mock_fernet original = "my_secret_password" encrypted = encrypt_value(original) decrypted = decrypt_value(encrypted) assert encrypted != original assert encrypted.startswith("enc:") assert decrypted == original def test_real_encryption_round_trip(self): """Test actual encryption/decryption with real cryptography library.""" from app.utils.encryption import decrypt_value, encrypt_value, is_encryption_available # Skip if encryption is not available if not is_encryption_available(): pytest.skip("Encryption not available (cryptography library not installed)") original_value = "my_super_secret_password_123" # Encrypt the value encrypted = encrypt_value(original_value) # Should be encrypted (has enc: prefix) assert encrypted.startswith("enc:") assert encrypted != original_value # Decrypt should return original value decrypted = decrypt_value(encrypted) assert decrypted == original_value def test_encryption_with_special_characters(self): """Test encryption with special characters and symbols.""" from app.utils.encryption import decrypt_value, encrypt_value, is_encryption_available if not is_encryption_available(): pytest.skip("Encryption not available") original = "P@ssw0rd!#$%^&*()_+-=[]{}|;:',.<>?/~`" encrypted = encrypt_value(original) decrypted = decrypt_value(encrypted) assert decrypted == original def test_encryption_with_unicode(self): """Test encryption with unicode characters.""" from app.utils.encryption import decrypt_value, encrypt_value, is_encryption_available if not is_encryption_available(): pytest.skip("Encryption not available") original = "Hello 世界 🌍 Привет мир" encrypted = encrypt_value(original) decrypted = decrypt_value(encrypted) assert decrypted == original def test_encryption_with_long_string(self): """Test encryption with very long strings.""" from app.utils.encryption import decrypt_value, encrypt_value, is_encryption_available if not is_encryption_available(): pytest.skip("Encryption not available") # Create a long string (1000 characters) original = "A" * 1000 encrypted = encrypt_value(original) decrypted = decrypt_value(encrypted) assert decrypted == original assert len(decrypted) == 1000 def test_encryption_with_newlines_and_whitespace(self): """Test encryption preserves newlines and whitespace.""" from app.utils.encryption import decrypt_value, encrypt_value, is_encryption_available if not is_encryption_available(): pytest.skip("Encryption not available") original = "line1\n line2\t\ttabbed\r\nline3 " encrypted = encrypt_value(original) decrypted = decrypt_value(encrypted) assert decrypted == original def test_encryption_with_json_string(self): """Test encryption with JSON string.""" from app.utils.encryption import decrypt_value, encrypt_value, is_encryption_available if not is_encryption_available(): pytest.skip("Encryption not available") original = '{"key": "value", "nested": {"array": [1, 2, 3]}}' encrypted = encrypt_value(original) decrypted = decrypt_value(encrypted) assert decrypted == original def test_multiple_encrypt_same_value_produces_different_ciphertext(self): """Test that encrypting the same value twice produces different ciphertext (if using random IV).""" from app.utils.encryption import encrypt_value, is_encryption_available if not is_encryption_available(): pytest.skip("Encryption not available") original = "same_value" encrypted1 = encrypt_value(original) encrypted2 = encrypt_value(original) # Both should be encrypted assert encrypted1.startswith("enc:") assert encrypted2.startswith("enc:") # Fernet uses timestamp-based encryption, so they might be different # (depending on timing). This test documents the behavior. # We'll just verify both decrypt correctly from app.utils.encryption import decrypt_value assert decrypt_value(encrypted1) == original assert decrypt_value(encrypted2) == original