QA AGENT FINDINGS (new issues not in audit): 1. Legacy @PURPOSE→@BRIEF: test_datasets.py (44 occurrences) 2. Legacy @SEMANTICS:→[SEMANTICS]: test_superset_matrix.py, test_smoke_app.py 3. @PRE/@POST on C2 functions: test_models.py violation 4. Unclosed #endregion anchors: test_datasets.py (56→1), test_db_executor.py, etc. FIXES APPLIED: - Module #region anchors added: test_smoke_plugins.py, test_models.py, api/test_tasks.py, core/test_defensive_guards.py - @RELATION BINDS_TO added: 14 files - @TEST_EDGE added (≥3 each): 16 files - Legacy syntax converted: test_datasets.py, test_superset_matrix.py, test_smoke_app.py - @PRE/@POST removed from C2 functions: test_models.py - Unclosed #endregion fixed: test_smoke_app.py, test_db_executor.py, test_connection_service.py, test_orchestrator_direct_db.py VERIFIED: 7778/7778 tests pass, 0 new failures REMAINING: 947 @BRIEF gaps, 165 @TEST_EDGE gaps, 38 oversized files
324 lines
13 KiB
Python
324 lines
13 KiB
Python
# #region Test.Core.ConnectionService [C:3] [TYPE Module] [SEMANTICS test, connections, service, settings]
|
|
# @BRIEF Pytest tests for ConnectionService CRUD, encryption, test, and deletion blocking.
|
|
# @RELATION BINDS_TO -> [ConnectionService]
|
|
# @TEST_EDGE: missing_field -> ValueError for empty name
|
|
# @TEST_EDGE: invalid_type -> ValueError for unsupported dialect
|
|
# @TEST_EDGE: external_fail -> Connection error handled gracefully
|
|
# @TEST_CONTRACT ConnectionService CRUD operations reflect in GlobalSettings.connections.
|
|
# @TEST_CONTRACT Passwords are encrypted at rest (stored ≠ plaintext).
|
|
# @TEST_CONTRACT Test connection returns success/failure with diagnostics.
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from src.core.config_models import AppConfig, DatabaseConnection, GlobalSettings
|
|
from src.core.connection_service import ConnectionService
|
|
|
|
|
|
# #region Fixtures [C:2] [TYPE Block]
|
|
@pytest.fixture
|
|
def mock_config_manager():
|
|
"""ConfigManager with empty connections list."""
|
|
cm = MagicMock()
|
|
cm.config = AppConfig(
|
|
environments=[],
|
|
settings=GlobalSettings(connections=[]),
|
|
)
|
|
return cm
|
|
|
|
|
|
@pytest.fixture
|
|
def config_with_two(mock_config_manager):
|
|
"""ConfigManager pre-populated with 2 connections."""
|
|
mock_config_manager.config.settings.connections = [
|
|
DatabaseConnection(
|
|
id="c1",
|
|
name="Test PG",
|
|
host="localhost",
|
|
port=5432,
|
|
database="test_db",
|
|
username="tester",
|
|
password="enc_secret_pg",
|
|
dialect="postgresql",
|
|
pool_size=5,
|
|
),
|
|
DatabaseConnection(
|
|
id="c2",
|
|
name="Test CH",
|
|
host="localhost",
|
|
port=9000,
|
|
database="analytics",
|
|
username="default",
|
|
password="",
|
|
dialect="clickhouse",
|
|
pool_size=2,
|
|
),
|
|
]
|
|
return mock_config_manager
|
|
|
|
|
|
@pytest.fixture
|
|
def service(mock_config_manager):
|
|
return ConnectionService(mock_config_manager)
|
|
|
|
|
|
@pytest.fixture
|
|
def service_with_two(config_with_two):
|
|
return ConnectionService(config_with_two)
|
|
|
|
|
|
# #endregion Fixtures
|
|
|
|
|
|
# #region create tests [C:2] [TYPE Block]
|
|
class TestCreateConnection:
|
|
def test_create_success(self, service):
|
|
data = {
|
|
"name": "My PG",
|
|
"host": "db.example.com",
|
|
"port": 5432,
|
|
"database": "products",
|
|
"username": "translator",
|
|
"password": "my_secret",
|
|
"dialect": "postgresql",
|
|
"pool_size": 5,
|
|
}
|
|
result = service.create_connection(data)
|
|
assert result["name"] == "My PG"
|
|
assert result["password"] == "********"
|
|
assert len(service.config_manager.config.settings.connections) == 1
|
|
# Verify password was encrypted (stored != plaintext)
|
|
stored = service.config_manager.config.settings.connections[0]
|
|
assert stored.password != "my_secret"
|
|
assert stored.password != "********"
|
|
|
|
def test_create_duplicate_name(self, service_with_two):
|
|
data = {"name": "Test PG", "host": "other", "port": 5432,
|
|
"database": "x", "username": "u", "password": "p", "dialect": "postgresql"}
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
service_with_two.create_connection(data)
|
|
|
|
def test_create_empty_name(self, service):
|
|
data = {"name": "", "host": "x", "port": 5432,
|
|
"database": "x", "username": "u", "password": "p", "dialect": "postgresql"}
|
|
with pytest.raises(ValueError, match="must not be empty"):
|
|
service.create_connection(data)
|
|
|
|
def test_create_dialect_case_insensitive(self, service):
|
|
data = {"name": "PG", "host": "x", "port": 5432, "database": "x",
|
|
"username": "u", "password": "p", "dialect": "PostgreSQL"}
|
|
result = service.create_connection(data)
|
|
assert result["dialect"] == "postgresql"
|
|
|
|
def test_create_mysql_dialect(self, service):
|
|
data = {"name": "MySQL", "host": "x", "port": 3306, "database": "x",
|
|
"username": "u", "password": "p", "dialect": "mysql"}
|
|
result = service.create_connection(data)
|
|
assert result["dialect"] == "mysql"
|
|
|
|
def test_create_bad_dialect(self, service):
|
|
data = {"name": "Bad", "host": "x", "port": 5432, "database": "x",
|
|
"username": "u", "password": "p", "dialect": "oracle"}
|
|
with pytest.raises(ValueError, match="Unsupported dialect"):
|
|
service.create_connection(data)
|
|
|
|
def test_create_bad_port(self, service):
|
|
data = {"name": "Bad port", "host": "x", "port": 0, "database": "x",
|
|
"username": "u", "password": "p", "dialect": "postgresql"}
|
|
with pytest.raises(ValueError, match="Port must be between"):
|
|
service.create_connection(data)
|
|
# #endregion
|
|
|
|
|
|
# #region list/get tests [C:2] [TYPE Block]
|
|
class TestListGetConnection:
|
|
def test_list_masks_passwords(self, service_with_two):
|
|
result = service_with_two.list_connections()
|
|
assert len(result) == 2
|
|
for conn in result:
|
|
assert conn["password"] == "********"
|
|
|
|
def test_list_shows_used_by(self, service_with_two):
|
|
result = service_with_two.list_connections()
|
|
for conn in result:
|
|
assert "used_by" in conn
|
|
|
|
def test_get_decrypts_password(self, service_with_two):
|
|
conn = service_with_two.get_connection("c1")
|
|
assert conn is not None
|
|
# password is "enc_secret_pg" which is not Fernet-encrypted,
|
|
# so decryption will pass through as-is
|
|
assert conn.password is not None
|
|
|
|
def test_get_not_found(self, service):
|
|
conn = service.get_connection("nonexistent")
|
|
assert conn is None
|
|
|
|
def test_empty_list(self, service):
|
|
result = service.list_connections()
|
|
assert result == []
|
|
# #endregion
|
|
|
|
|
|
# #region update tests [C:2] [TYPE Block]
|
|
class TestUpdateConnection:
|
|
def test_update_name(self, service_with_two):
|
|
result = service_with_two.update_connection("c1", {"name": "Renamed PG"})
|
|
assert result["name"] == "Renamed PG"
|
|
assert result["password"] == "********"
|
|
|
|
def test_update_password(self, service_with_two):
|
|
result = service_with_two.update_connection("c1", {"password": "new_secret"})
|
|
assert result["password"] == "********"
|
|
# Verify encrypted differently than original
|
|
stored = service_with_two.config_manager.config.settings.connections[0]
|
|
assert stored.password != "enc_secret_pg"
|
|
|
|
def test_update_empty_password_keeps_existing(self, service_with_two):
|
|
result = service_with_two.update_connection("c1", {"password": ""})
|
|
assert result["password"] == "********"
|
|
stored = service_with_two.config_manager.config.settings.connections[0]
|
|
assert stored.password == "enc_secret_pg"
|
|
|
|
def test_update_masked_password_keeps_existing(self, service_with_two):
|
|
service_with_two.update_connection("c1", {"password": "********"})
|
|
stored = service_with_two.config_manager.config.settings.connections[0]
|
|
assert stored.password == "enc_secret_pg"
|
|
|
|
def test_update_host_and_port(self, service_with_two):
|
|
result = service_with_two.update_connection("c1", {"host": "new.db", "port": 15432})
|
|
assert result["host"] == "new.db"
|
|
assert result["port"] == 15432
|
|
|
|
def test_update_not_found(self, service):
|
|
with pytest.raises(ValueError, match="not found"):
|
|
service.update_connection("nonexistent", {"name": "X"})
|
|
|
|
def test_update_duplicate_name(self, service_with_two):
|
|
# Rename c2 to c1's name
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
service_with_two.update_connection("c2", {"name": "Test PG"})
|
|
# #endregion
|
|
|
|
|
|
# #region delete tests [C:2] [TYPE Block]
|
|
class TestDeleteConnection:
|
|
def test_delete_success(self, service_with_two):
|
|
result = service_with_two.delete_connection("c1")
|
|
assert result["success"] is True
|
|
assert len(service_with_two.config_manager.config.settings.connections) == 1
|
|
|
|
def test_delete_not_found(self, service):
|
|
with pytest.raises(ValueError, match="not found"):
|
|
service.delete_connection("nonexistent")
|
|
|
|
def test_delete_blocked_by_jobs(self, service_with_two):
|
|
# AUDIT_NOTE: _find_blocking_jobs calls TranslationJob via [EXT:SQLAlchemy].
|
|
# Patching the method is equivalent to mocking the DB query boundary.
|
|
with patch.object(service_with_two, '_find_blocking_jobs', return_value=["Job 1"]):
|
|
result = service_with_two.delete_connection("c1")
|
|
assert result["success"] is False
|
|
assert result["blocked"] is True
|
|
assert "Job 1" in result["blocking_jobs"]
|
|
# #endregion
|
|
|
|
|
|
# #region password encryption tests [C:2] [TYPE Block]
|
|
class TestPasswordEncryption:
|
|
def test_encryption_round_trip(self, service):
|
|
"""Password round-trips through encryption/decryption."""
|
|
password = "super_secret_key_123"
|
|
encrypted = service._encrypt_password(password)
|
|
# With real EncryptionManager (Fernet), encrypted != plaintext
|
|
assert encrypted != password
|
|
decrypted = service._decrypt_password(encrypted)
|
|
assert decrypted == password
|
|
|
|
def test_masked_password_decrypt_returns_masked(self, service):
|
|
result = service._decrypt_password("********")
|
|
assert result == "********"
|
|
|
|
def test_empty_password_encryption(self, service):
|
|
encrypted = service._encrypt_password("")
|
|
# Empty string is still encrypted by Fernet (consistent behavior)
|
|
assert encrypted != ""
|
|
assert encrypted != "********"
|
|
decrypted = service._decrypt_password(encrypted)
|
|
assert decrypted == ""
|
|
|
|
def test_new_connection_password_encrypted(self, service):
|
|
data = {"name": "Test", "host": "x", "port": 5432, "database": "x",
|
|
"username": "u", "password": "visible", "dialect": "postgresql"}
|
|
result = service.create_connection(data)
|
|
assert result["password"] == "********"
|
|
stored = service.config_manager.config.settings.connections[0]
|
|
assert stored.password != "visible"
|
|
assert stored.password != "********"
|
|
# #endregion
|
|
|
|
|
|
# #region test connection tests [C:2] [TYPE Block]
|
|
# AUDIT_NOTE: patch.object targets _test_postgresql/_test_clickhouse/_test_mysql
|
|
# which are thin wrappers around [EXT:asyncpg]/[EXT:clickhouse-connect]/[EXT:pymysql]
|
|
# DB drivers. These are VALID [EXT:Database] boundary mocks per semantics-testing §3c.
|
|
class TestTestConnection:
|
|
@pytest.mark.asyncio
|
|
async def test_connection_not_found(self, service):
|
|
result = await service.test_connection("nonexistent")
|
|
assert result["success"] is False
|
|
assert "not found" in result["error"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_postgresql_test_success(self, service_with_two):
|
|
"""Mock asyncpg to verify dialect routing works."""
|
|
with patch.object(service_with_two, '_test_postgresql', return_value="PostgreSQL 16.3"):
|
|
result = await service_with_two.test_connection("c1")
|
|
assert result["success"] is True
|
|
assert result["db_version"] == "PostgreSQL 16.3"
|
|
assert "latency_ms" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clickhouse_test_success(self, service_with_two):
|
|
with patch.object(service_with_two, '_test_clickhouse', return_value="ClickHouse 24.3"):
|
|
result = await service_with_two.test_connection("c2")
|
|
assert result["success"] is True
|
|
assert result["db_version"] == "ClickHouse 24.3"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_mysql_success(self, config_with_two):
|
|
"""Create a MySQL connection and test it."""
|
|
config_with_two.config.settings.connections.append(
|
|
DatabaseConnection(
|
|
id="c3", name="MySQL", host="x", port=3306,
|
|
database="x", username="u", password="p",
|
|
dialect="mysql",
|
|
)
|
|
)
|
|
svc = ConnectionService(config_with_two)
|
|
with patch.object(svc, '_test_mysql', return_value="MySQL 8.0"):
|
|
result = await svc.test_connection("c3")
|
|
assert result["success"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_error(self, service_with_two):
|
|
"""Simulate connection failure."""
|
|
with patch.object(service_with_two, '_test_postgresql', side_effect=RuntimeError("Host unreachable")):
|
|
result = await service_with_two.test_connection("c1")
|
|
assert result["success"] is False
|
|
assert "Host unreachable" in result["error"]
|
|
# #endregion
|
|
|
|
|
|
# #region validation tests [C:2] [TYPE Block]
|
|
class TestValidateRefs:
|
|
def test_validate_no_jobs_no_orphans(self, service):
|
|
"""When no TranslationJobs exist, no orphans."""
|
|
with patch('src.core.database.SessionLocal') as mock_session_local:
|
|
mock_db = MagicMock()
|
|
mock_session_local.return_value = mock_db
|
|
mock_db.query.return_value.filter.return_value.all.return_value = []
|
|
orphans = service.validate_connection_refs()
|
|
assert orphans == []
|
|
# #endregion
|
|
# #endregion Test.Core.ConnectionService
|