168 lines
5.2 KiB
Python
168 lines
5.2 KiB
Python
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.db.metadb.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 encrypt(self, value):
|
|
encrypted = f"encrypted::{value}"
|
|
self.encrypted_values.append(value)
|
|
return encrypted
|
|
|
|
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.db.metadb.repositories.metadata_repository.is_database_encryption_configured",
|
|
lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.infra.db.metadb.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.db.metadb.repositories.metadata_repository.is_database_encryption_configured",
|
|
lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.infra.db.metadb.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.db.metadb.repositories.metadata_repository.is_database_encryption_configured",
|
|
lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.infra.db.metadb.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()
|
|
|
|
|
|
def test_upsert_project_database_config_encrypts_plaintext_dsn(monkeypatch):
|
|
project_id = uuid4()
|
|
session = SimpleNamespace(
|
|
execute=None,
|
|
add=None,
|
|
commit=None,
|
|
refresh=None,
|
|
)
|
|
added = []
|
|
session.execute = AsyncMock(return_value=_DummyResult(None))
|
|
session.add = lambda item: added.append(item)
|
|
session.commit = AsyncMock()
|
|
session.refresh = AsyncMock()
|
|
encryptor = _DummyEncryptor()
|
|
repo = MetadataRepository(session)
|
|
|
|
monkeypatch.setattr(
|
|
"app.infra.db.metadb.repositories.metadata_repository.is_database_encryption_configured",
|
|
lambda: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.infra.db.metadb.repositories.metadata_repository.get_database_encryptor",
|
|
lambda: encryptor,
|
|
)
|
|
|
|
record = asyncio.run(
|
|
repo.upsert_project_database_config(
|
|
project_id,
|
|
db_role="biz_data",
|
|
db_type="postgresql",
|
|
dsn="postgresql://user:secret@localhost/db",
|
|
pool_min_size=1,
|
|
pool_max_size=5,
|
|
)
|
|
)
|
|
|
|
assert encryptor.encrypted_values == ["postgresql://user:secret@localhost/db"]
|
|
assert record.dsn_encrypted == "encrypted::postgresql://user:secret@localhost/db"
|
|
assert added == [record]
|
|
session.commit.assert_awaited_once()
|
|
session.refresh.assert_awaited_once_with(record)
|