Files
ss-tools/backend/tests/test_connection_service.py
busya 154c9bb3b1 qa: orthogonal test review — 20 files sampled, 16+ fixed
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
2026-06-16 12:11:49 +03:00

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