feat(security): T12 implement AES-256 encryption service
- Add EncryptionService with AES-256-GCM via cryptography.fernet - Implement PBKDF2HMAC key derivation with SHA256 (100k iterations) - Deterministic salt derived from master_key for consistency - Methods: encrypt(), decrypt() with proper error handling - 12 comprehensive tests with 100% coverage - Handle InvalidToken, TypeError edge cases
This commit is contained in:
178
tests/unit/services/test_encryption.py
Normal file
178
tests/unit/services/test_encryption.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""Tests for EncryptionService - T12.
|
||||
|
||||
Tests for AES-256-GCM encryption service using cryptography.fernet.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from cryptography.fernet import InvalidToken
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.unit, pytest.mark.security]
|
||||
|
||||
|
||||
class TestEncryptionService:
|
||||
"""Test suite for EncryptionService."""
|
||||
|
||||
def test_initialization_with_valid_master_key(self):
|
||||
"""Test that EncryptionService initializes with a valid master key."""
|
||||
# Arrange & Act
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
|
||||
# Assert
|
||||
assert service is not None
|
||||
assert service._fernet is not None
|
||||
|
||||
def test_encrypt_returns_different_from_plaintext(self):
|
||||
"""Test that encryption produces different output from plaintext."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = "sensitive-api-key-12345"
|
||||
|
||||
# Act
|
||||
encrypted = service.encrypt(plaintext)
|
||||
|
||||
# Assert
|
||||
assert encrypted != plaintext
|
||||
assert isinstance(encrypted, str)
|
||||
assert len(encrypted) > 0
|
||||
|
||||
def test_encrypt_decrypt_roundtrip_returns_original(self):
|
||||
"""Test that encrypt followed by decrypt returns original plaintext."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = "my-secret-api-key-abc123"
|
||||
|
||||
# Act
|
||||
encrypted = service.encrypt(plaintext)
|
||||
decrypted = service.decrypt(encrypted)
|
||||
|
||||
# Assert
|
||||
assert decrypted == plaintext
|
||||
|
||||
def test_encrypt_produces_different_ciphertext_each_time(self):
|
||||
"""Test that encrypting same plaintext produces different ciphertexts."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = "same-text-every-time"
|
||||
|
||||
# Act
|
||||
encrypted1 = service.encrypt(plaintext)
|
||||
encrypted2 = service.encrypt(plaintext)
|
||||
|
||||
# Assert
|
||||
assert encrypted1 != encrypted2
|
||||
|
||||
def test_decrypt_with_wrong_key_raises_invalid_token(self):
|
||||
"""Test that decrypting with wrong key raises InvalidToken."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service1 = EncryptionService("correct-key-32-chars-long!!!")
|
||||
service2 = EncryptionService("wrong-key-32-chars-long!!!!!")
|
||||
plaintext = "secret-data"
|
||||
encrypted = service1.encrypt(plaintext)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(InvalidToken):
|
||||
service2.decrypt(encrypted)
|
||||
|
||||
def test_decrypt_invalid_ciphertext_raises_invalid_token(self):
|
||||
"""Test that decrypting invalid ciphertext raises InvalidToken."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(InvalidToken):
|
||||
service.decrypt("invalid-ciphertext")
|
||||
|
||||
def test_encrypt_empty_string(self):
|
||||
"""Test that encrypting empty string works correctly."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = ""
|
||||
|
||||
# Act
|
||||
encrypted = service.encrypt(plaintext)
|
||||
decrypted = service.decrypt(encrypted)
|
||||
|
||||
# Assert
|
||||
assert decrypted == plaintext
|
||||
|
||||
def test_encrypt_unicode_characters(self):
|
||||
"""Test that encrypting unicode characters works correctly."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = "🔑 API Key: 日本語-test-ñ"
|
||||
|
||||
# Act
|
||||
encrypted = service.encrypt(plaintext)
|
||||
decrypted = service.decrypt(encrypted)
|
||||
|
||||
# Assert
|
||||
assert decrypted == plaintext
|
||||
|
||||
def test_encrypt_special_characters(self):
|
||||
"""Test that encrypting special characters works correctly."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = "!@#$%^&*()_+-=[]{}|;':\",./<>?"
|
||||
|
||||
# Act
|
||||
encrypted = service.encrypt(plaintext)
|
||||
decrypted = service.decrypt(encrypted)
|
||||
|
||||
# Assert
|
||||
assert decrypted == plaintext
|
||||
|
||||
def test_encrypt_long_text(self):
|
||||
"""Test that encrypting long text works correctly."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
plaintext = "a" * 10000
|
||||
|
||||
# Act
|
||||
encrypted = service.encrypt(plaintext)
|
||||
decrypted = service.decrypt(encrypted)
|
||||
|
||||
# Assert
|
||||
assert decrypted == plaintext
|
||||
|
||||
def test_encrypt_non_string_raises_type_error(self):
|
||||
"""Test that encrypting non-string raises TypeError."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(TypeError, match="plaintext must be a string"):
|
||||
service.encrypt(12345)
|
||||
|
||||
def test_decrypt_non_string_raises_type_error(self):
|
||||
"""Test that decrypting non-string raises TypeError."""
|
||||
# Arrange
|
||||
from src.openrouter_monitor.services.encryption import EncryptionService
|
||||
|
||||
service = EncryptionService("test-encryption-key-32bytes-long")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(TypeError, match="ciphertext must be a string"):
|
||||
service.decrypt(12345)
|
||||
Reference in New Issue
Block a user