diff --git a/tests/unit/test_metadata_repository_dsn_decrypt.py b/tests/unit/test_metadata_repository_dsn_decrypt.py new file mode 100644 index 0000000..02796e1 --- /dev/null +++ b/tests/unit/test_metadata_repository_dsn_decrypt.py @@ -0,0 +1,119 @@ +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock +from uuid import uuid4 + +import pytest +from cryptography.fernet import InvalidToken + +from app.infra.repositories.metadata_repository import MetadataRepository + + +class _DummyResult: + def __init__(self, record): + self._record = record + + def scalar_one_or_none(self): + return self._record + + +class _DummyEncryptor: + def __init__(self, decrypted=None, raise_invalid_token=False): + self._decrypted = decrypted + self._raise_invalid_token = raise_invalid_token + self.encrypted_values = [] + + def decrypt(self, _value): + if self._raise_invalid_token: + raise InvalidToken() + return self._decrypted + +def _build_record(dsn_encrypted: str): + return SimpleNamespace( + project_id=uuid4(), + db_role="biz_data", + db_type="postgresql", + dsn_encrypted=dsn_encrypted, + pool_min_size=1, + pool_max_size=5, + ) + + +def test_invalid_token_with_plaintext_dsn_value_raises_clear_error(monkeypatch): + record = _build_record("postgresql://user:p@ss@localhost:5432/db") + session = SimpleNamespace( + execute=None, + commit=None, + ) + session.execute = AsyncMock(return_value=_DummyResult(record)) + session.commit = AsyncMock() + encryptor = _DummyEncryptor(raise_invalid_token=True) + repo = MetadataRepository(session) + + monkeypatch.setattr( + "app.infra.repositories.metadata_repository.is_database_encryption_configured", + lambda: True, + ) + monkeypatch.setattr( + "app.infra.repositories.metadata_repository.get_database_encryptor", + lambda: encryptor, + ) + + with pytest.raises( + ValueError, + match="DATABASE_ENCRYPTION_KEY mismatch or invalid dsn_encrypted value", + ): + asyncio.run(repo.get_project_db_routing(record.project_id, "biz_data")) + session.commit.assert_not_awaited() + + +def test_invalid_token_with_non_dsn_value_raises_clear_error(monkeypatch): + record = _build_record("gAAAAABinvalidciphertext") + session = SimpleNamespace( + execute=None, + commit=None, + ) + session.execute = AsyncMock(return_value=_DummyResult(record)) + session.commit = AsyncMock() + repo = MetadataRepository(session) + + monkeypatch.setattr( + "app.infra.repositories.metadata_repository.is_database_encryption_configured", + lambda: True, + ) + monkeypatch.setattr( + "app.infra.repositories.metadata_repository.get_database_encryptor", + lambda: _DummyEncryptor(raise_invalid_token=True), + ) + + with pytest.raises( + ValueError, + match="DATABASE_ENCRYPTION_KEY mismatch or invalid dsn_encrypted value", + ): + asyncio.run(repo.get_project_db_routing(record.project_id, "biz_data")) + session.commit.assert_not_awaited() + + +def test_encrypted_dsn_decrypts_without_migration(monkeypatch): + record = _build_record("encrypted-value") + session = SimpleNamespace( + execute=None, + commit=None, + ) + session.execute = AsyncMock(return_value=_DummyResult(record)) + session.commit = AsyncMock() + repo = MetadataRepository(session) + + monkeypatch.setattr( + "app.infra.repositories.metadata_repository.is_database_encryption_configured", + lambda: True, + ) + monkeypatch.setattr( + "app.infra.repositories.metadata_repository.get_database_encryptor", + lambda: _DummyEncryptor(decrypted="postgresql://u:p@ss@host/db"), + ) + + routing = asyncio.run(repo.get_project_db_routing(record.project_id, "biz_data")) + + assert routing.dsn == "postgresql://u:p%40ss@host/db" + session.commit.assert_not_awaited()