# #region Test.Encryption [C:3] [TYPE Module] [SEMANTICS test,encryption,fernet,key,crypto] # @BRIEF Verify EncryptionManager encrypt/decrypt and ensure_encryption_key resolution contracts. # @RELATION BINDS_TO -> [EncryptionCore] # @RELATION BINDS_TO -> [EncryptionKeyModule] # @TEST_EDGE: missing_key -> RuntimeError when ENCRYPTION_KEY absent # @TEST_EDGE: invalid_key -> RuntimeError when ENCRYPTION_KEY is not valid Fernet format # @TEST_EDGE: external_fail -> decrypt of garbage data raises Exception # @TEST_INVARIANT: symmetric_encryption -> VERIFIED_BY: encrypt_decrypt_cycle, empty_string_encryption from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest # Hardcoded valid Fernet key — generated once, frozen as fixture. # 32 url-safe base64 bytes. Never recompute at test time (anti-tautology). _VALID_FERNET_KEY = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0=" # ── _require_fernet_key ───────────────────────────────────────────────────── class TestRequireFernetKey: """_require_fernet_key — loads and validates Fernet key from environment.""" # #region test_require_key_valid [C:2] [TYPE Function] # @BRIEF Valid ENCRYPTION_KEY returns key bytes without error. def test_require_key_valid(self, monkeypatch): from src.core.encryption import _require_fernet_key monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY) result = _require_fernet_key() assert result == _VALID_FERNET_KEY.encode() # #endregion test_require_key_valid # #region test_require_key_missing [C:2] [TYPE Function] # @BRIEF Missing ENCRYPTION_KEY raises RuntimeError with startup guidance. def test_require_key_missing(self, monkeypatch): from src.core.encryption import _require_fernet_key monkeypatch.delenv("ENCRYPTION_KEY", raising=False) with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"): _require_fernet_key() # #endregion test_require_key_missing # #region test_require_key_invalid [C:2] [TYPE Function] # @BRIEF Malformed ENCRYPTION_KEY raises RuntimeError about valid Fernet key. def test_require_key_invalid(self, monkeypatch): from src.core.encryption import _require_fernet_key monkeypatch.setenv("ENCRYPTION_KEY", "not-a-valid-fernet-key") with pytest.raises(RuntimeError, match="valid Fernet key"): _require_fernet_key() # #endregion test_require_key_invalid # #region test_require_key_whitespace_only [C:2] [TYPE Function] # @BRIEF Whitespace-only ENCRYPTION_KEY is treated as missing. def test_require_key_whitespace_only(self, monkeypatch): from src.core.encryption import _require_fernet_key monkeypatch.setenv("ENCRYPTION_KEY", " ") with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"): _require_fernet_key() # #endregion test_require_key_whitespace_only # ── EncryptionManager ─────────────────────────────────────────────────────── class TestEncryptionManager: """EncryptionManager — encrypt/decrypt operations with validated Fernet key.""" # #region test_encrypt_decrypt_cycle [C:2] [TYPE Function] # @BRIEF Encrypt then decrypt returns original plaintext (symmetric invariant). def test_encrypt_decrypt_cycle(self, monkeypatch): from src.core.encryption import EncryptionManager monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY) mgr = EncryptionManager() # Hardcoded fixture — plaintext declared before assertion, not computed. plaintext = "my_secret_key" encrypted = mgr.encrypt(plaintext) assert encrypted != plaintext assert mgr.decrypt(encrypted) == plaintext # #endregion test_encrypt_decrypt_cycle # #region test_empty_string_encryption [C:2] [TYPE Function] # @BRIEF Empty string survives encrypt/decrypt cycle. def test_empty_string_encryption(self, monkeypatch): from src.core.encryption import EncryptionManager monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY) mgr = EncryptionManager() encrypted = mgr.encrypt("") assert mgr.decrypt(encrypted) == "" # #endregion test_empty_string_encryption # #region test_decrypt_garbage_raises [C:2] [TYPE Function] # @BRIEF Decrypting non-Fernet data raises an exception (external_fail edge). def test_decrypt_garbage_raises(self, monkeypatch): from src.core.encryption import EncryptionManager monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY) mgr = EncryptionManager() with pytest.raises(Exception): mgr.decrypt("not-encrypted-data") # #endregion test_decrypt_garbage_raises # #region test_init_missing_key_raises [C:2] [TYPE Function] # @BRIEF EncryptionManager() without ENCRYPTION_KEY raises RuntimeError at init. def test_init_missing_key_raises(self, monkeypatch): from src.core.encryption import EncryptionManager monkeypatch.delenv("ENCRYPTION_KEY", raising=False) with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"): EncryptionManager() # #endregion test_init_missing_key_raises # ── ensure_encryption_key ─────────────────────────────────────────────────── class TestEnsureEncryptionKey: """ensure_encryption_key — resolve key from env or .env file, crash-early if absent.""" # #region test_ensure_key_from_env [C:2] [TYPE Function] # @BRIEF Returns key from process environment when ENCRYPTION_KEY is set. def test_ensure_key_from_env(self, monkeypatch): from src.core.encryption_key import ensure_encryption_key monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY) result = ensure_encryption_key() assert result == _VALID_FERNET_KEY # #endregion test_ensure_key_from_env # #region test_ensure_key_from_dotenv_file [C:2] [TYPE Function] # @BRIEF Loads key from .env file when not in process environment; sets os.environ. def test_ensure_key_from_dotenv_file(self, monkeypatch, tmp_path): from src.core.encryption_key import ensure_encryption_key monkeypatch.delenv("ENCRYPTION_KEY", raising=False) env_file = tmp_path / ".env" env_file.write_text(f"ENCRYPTION_KEY={_VALID_FERNET_KEY}\nOTHER=value\n") result = ensure_encryption_key(env_file) assert result == _VALID_FERNET_KEY # Side-effect: key must be propagated to process environment. import os assert os.environ.get("ENCRYPTION_KEY") == _VALID_FERNET_KEY # #endregion test_ensure_key_from_dotenv_file # #region test_ensure_key_no_file_raises [C:2] [TYPE Function] # @BRIEF Crash-early when no .env file exists and env var is unset. def test_ensure_key_no_file_raises(self, monkeypatch): from src.core.encryption_key import ensure_encryption_key monkeypatch.delenv("ENCRYPTION_KEY", raising=False) with pytest.raises(RuntimeError, match="ENCRYPTION_KEY is not set"): ensure_encryption_key(Path("/nonexistent/.env")) # #endregion test_ensure_key_no_file_raises # #region test_ensure_key_file_without_key_raises [C:2] [TYPE Function] # @BRIEF Crash-early when .env file exists but contains no ENCRYPTION_KEY line. def test_ensure_key_file_without_key_raises(self, monkeypatch, tmp_path): from src.core.encryption_key import ensure_encryption_key monkeypatch.delenv("ENCRYPTION_KEY", raising=False) env_file = tmp_path / ".env" env_file.write_text("OTHER=value\nUNRELATED=stuff\n") with pytest.raises(RuntimeError, match="ENCRYPTION_KEY is not set"): ensure_encryption_key(env_file) # #endregion test_ensure_key_file_without_key_raises # #endregion Test.Encryption