mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
test_crypto's round-trip tests called the real encrypt/decrypt path, which needs settings.encryption_key configured — so they silently depended on ROBOCO_ENCRYPTION_KEY being present in the environment. In agent gate containers it is not, so four tests failed there and the agent mis-read it as a code regression. An autouse fixture now monkeypatches a valid generated Fernet key, so the tests pass without any ambient secret and agents never need the production key injected to gate.
157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
"""utils.crypto coverage — Fernet encrypt/decrypt round-trip + error paths."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from cryptography.fernet import Fernet
|
|
from roboco.config import settings
|
|
from roboco.utils.crypto import (
|
|
EncryptionError,
|
|
_get_fernet,
|
|
decrypt_token,
|
|
encrypt_token,
|
|
is_encryption_configured,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _configured_encryption_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Supply a valid Fernet key for the tests that exercise real crypto.
|
|
|
|
The round-trip tests need a configured key; without this they depend on
|
|
``ROBOCO_ENCRYPTION_KEY`` being set in the environment — it is not in agent
|
|
gate containers, which is exactly why they failed there. The patch-based
|
|
tests below replace ``settings`` wholesale inside their ``with`` blocks, so
|
|
this autouse default never interferes with them.
|
|
"""
|
|
monkeypatch.setattr(settings, "encryption_key", Fernet.generate_key().decode())
|
|
|
|
|
|
def test_encrypt_decrypt_round_trip() -> None:
|
|
plaintext = "sk-secret-test-token-12345"
|
|
encrypted = encrypt_token(plaintext)
|
|
assert encrypted != plaintext
|
|
decrypted = decrypt_token(encrypted)
|
|
assert decrypted == plaintext
|
|
|
|
|
|
def test_encrypt_empty_string_raises() -> None:
|
|
with pytest.raises(EncryptionError, match="empty"):
|
|
encrypt_token("")
|
|
|
|
|
|
def test_decrypt_empty_string_raises() -> None:
|
|
with pytest.raises(EncryptionError, match="empty"):
|
|
decrypt_token("")
|
|
|
|
|
|
def test_decrypt_invalid_token_raises() -> None:
|
|
with pytest.raises(EncryptionError):
|
|
decrypt_token("not-a-valid-token-at-all")
|
|
|
|
|
|
def test_is_encryption_configured() -> None:
|
|
"""Encryption must be configured for tests to run."""
|
|
assert is_encryption_configured() is True
|
|
|
|
|
|
def test_encrypt_long_string() -> None:
|
|
plaintext = "a" * 1000
|
|
encrypted = encrypt_token(plaintext)
|
|
decrypted = decrypt_token(encrypted)
|
|
assert decrypted == plaintext
|
|
|
|
|
|
def test_encrypt_unicode() -> None:
|
|
plaintext = "héllo wörld 中文 🚀"
|
|
encrypted = encrypt_token(plaintext)
|
|
decrypted = decrypt_token(encrypted)
|
|
assert decrypted == plaintext
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fail paths — cover the guards in _get_fernet, encrypt_token, decrypt_token.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_fernet_raises_when_key_unset() -> None:
|
|
"""_get_fernet must fail-closed when settings.encryption_key is empty."""
|
|
with (
|
|
patch("roboco.utils.crypto.settings") as mock_settings,
|
|
pytest.raises(EncryptionError, match="ROBOCO_ENCRYPTION_KEY"),
|
|
):
|
|
mock_settings.encryption_key = ""
|
|
_get_fernet()
|
|
|
|
|
|
def test_get_fernet_raises_on_invalid_key_format() -> None:
|
|
"""Bad key format → InvalidKey is wrapped in EncryptionError."""
|
|
with (
|
|
patch("roboco.utils.crypto.settings") as mock_settings,
|
|
pytest.raises(EncryptionError, match="Invalid encryption key format"),
|
|
):
|
|
mock_settings.encryption_key = "not-a-valid-fernet-key"
|
|
_get_fernet()
|
|
|
|
|
|
class _FailingFernet:
|
|
"""Fernet stand-in whose encrypt/decrypt always raise RuntimeError."""
|
|
|
|
def encrypt(self, _data: bytes) -> bytes:
|
|
raise RuntimeError("boom")
|
|
|
|
def decrypt(self, _data: bytes) -> bytes:
|
|
raise RuntimeError("boom")
|
|
|
|
|
|
def test_encrypt_token_wraps_unexpected_error() -> None:
|
|
"""Mock Fernet.encrypt to raise — encrypt_token must wrap as EncryptionError."""
|
|
with (
|
|
patch("roboco.utils.crypto._get_fernet", return_value=_FailingFernet()),
|
|
pytest.raises(EncryptionError, match="Failed to encrypt"),
|
|
):
|
|
encrypt_token("anything")
|
|
|
|
|
|
def test_decrypt_token_wraps_unexpected_error() -> None:
|
|
"""Non-InvalidToken errors get wrapped as EncryptionError."""
|
|
with (
|
|
patch("roboco.utils.crypto._get_fernet", return_value=_FailingFernet()),
|
|
pytest.raises(EncryptionError, match="Failed to decrypt"),
|
|
):
|
|
decrypt_token("any-encrypted-value")
|
|
|
|
|
|
def test_encrypt_token_reraises_encryption_error_unchanged() -> None:
|
|
"""EncryptionError from _get_fernet bubbles up without re-wrapping."""
|
|
err = EncryptionError("inner")
|
|
with (
|
|
patch("roboco.utils.crypto._get_fernet", side_effect=err),
|
|
pytest.raises(EncryptionError, match="inner"),
|
|
):
|
|
encrypt_token("anything")
|
|
|
|
|
|
def test_decrypt_token_reraises_encryption_error_unchanged() -> None:
|
|
"""EncryptionError from _get_fernet bubbles up unchanged from decrypt."""
|
|
err = EncryptionError("inner")
|
|
with (
|
|
patch("roboco.utils.crypto._get_fernet", side_effect=err),
|
|
pytest.raises(EncryptionError, match="inner"),
|
|
):
|
|
decrypt_token("any-encrypted-value")
|
|
|
|
|
|
def test_is_encryption_configured_returns_false_on_empty_key() -> None:
|
|
with patch("roboco.utils.crypto.settings") as mock_settings:
|
|
mock_settings.encryption_key = ""
|
|
assert is_encryption_configured() is False
|
|
|
|
|
|
def test_is_encryption_configured_returns_false_on_bad_key() -> None:
|
|
with patch("roboco.utils.crypto.settings") as mock_settings:
|
|
mock_settings.encryption_key = "not-a-valid-fernet-key"
|
|
assert is_encryption_configured() is False
|