fix(agent): resolve ModuleNotFoundError for backend, add E2E test infra
- Dockerfile.agent: fix CMD (python -m src.agent.run), use backend/requirements.txt, minimal COPY (only src.agent + src.core.cot_logger), add GRACE contract - docker-compose.yml: SERVICE_TOKEN_SECRET -> SERVICE_JWT (match code) - docker-compose.enterprise-clean.yml: same env var fix - docker/.env.agent.example: same env var fix - build.sh: same env var fix - chore: semantics-testing SKILL.md, backend tests, pyproject.toml
This commit is contained in:
@@ -1,112 +1,171 @@
|
||||
# #region Test.Encryption [C:3] [TYPE Module] [SEMANTICS test,encryption,fernet,key,crypto]
|
||||
# @BRIEF Tests for core/encryption.py and core/encryption_key.py — Fernet key loading, encrypt/decrypt.
|
||||
# @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 os
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
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."""
|
||||
|
||||
def test_valid_key(self):
|
||||
# #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
|
||||
key = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": key}, clear=True):
|
||||
result = _require_fernet_key()
|
||||
assert result == key.encode()
|
||||
|
||||
def test_missing_key_raises(self):
|
||||
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
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
|
||||
_require_fernet_key()
|
||||
|
||||
def test_invalid_key_raises(self):
|
||||
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
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": "not-a-valid-fernet-key"}, clear=True):
|
||||
with pytest.raises(RuntimeError, match="valid Fernet key"):
|
||||
_require_fernet_key()
|
||||
|
||||
def test_empty_key_raises(self):
|
||||
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
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": " "}, clear=True):
|
||||
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
|
||||
_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 and decrypt operations."""
|
||||
"""EncryptionManager — encrypt/decrypt operations with validated Fernet key."""
|
||||
|
||||
def test_encrypt_decrypt_cycle(self):
|
||||
# #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
|
||||
key = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": key}, clear=True):
|
||||
mgr = EncryptionManager()
|
||||
encrypted = mgr.encrypt("my_secret_data")
|
||||
assert encrypted != "my_secret_data"
|
||||
decrypted = mgr.decrypt(encrypted)
|
||||
assert decrypted == "my_secret_data"
|
||||
|
||||
def test_empty_string_encryption(self):
|
||||
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
|
||||
key = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": key}, clear=True):
|
||||
mgr = EncryptionManager()
|
||||
encrypted = mgr.encrypt("")
|
||||
decrypted = mgr.decrypt(encrypted)
|
||||
assert decrypted == ""
|
||||
|
||||
def test_decrypt_invalid_data_raises(self):
|
||||
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
|
||||
key = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": key}, clear=True):
|
||||
mgr = EncryptionManager()
|
||||
with pytest.raises(Exception):
|
||||
mgr.decrypt("not-encrypted-data")
|
||||
|
||||
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."""
|
||||
"""ensure_encryption_key — resolve key from env or .env file, crash-early if absent."""
|
||||
|
||||
def test_from_environment(self):
|
||||
# #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
|
||||
key = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="
|
||||
with patch.dict(os.environ, {"ENCRYPTION_KEY": key}, clear=True):
|
||||
result = ensure_encryption_key()
|
||||
assert result == key
|
||||
|
||||
def test_from_env_file(self):
|
||||
from src.core.encryption_key import ensure_encryption_key
|
||||
key = "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0="
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
env_file = Path(tmpdir) / ".env"
|
||||
env_file.write_text(f"ENCRYPTION_KEY={key}\nOTHER=value\n")
|
||||
result = ensure_encryption_key(env_file)
|
||||
assert result == key
|
||||
# Should also set it in environment
|
||||
assert os.environ.get("ENCRYPTION_KEY") == key
|
||||
monkeypatch.setenv("ENCRYPTION_KEY", _VALID_FERNET_KEY)
|
||||
result = ensure_encryption_key()
|
||||
assert result == _VALID_FERNET_KEY
|
||||
# #endregion test_ensure_key_from_env
|
||||
|
||||
def test_env_file_not_found_raises(self):
|
||||
# #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
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY is not set"):
|
||||
ensure_encryption_key(Path("/nonexistent/.env"))
|
||||
|
||||
def test_env_file_without_key_raises(self):
|
||||
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
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
env_file = Path(tmpdir) / ".env"
|
||||
env_file.write_text("OTHER=value\n")
|
||||
with pytest.raises(RuntimeError, match="ENCRYPTION_KEY is not set"):
|
||||
ensure_encryption_key(env_file)
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user