# #region Test.Core.ConnectionService [C:3] [TYPE Module] [SEMANTICS test, connections, service, settings] # @BRIEF Pytest tests for ConnectionService CRUD, encryption, test, and deletion blocking. # @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: def test_connection_not_found(self, service): result = service.test_connection("nonexistent") assert result["success"] is False assert "not found" in result["error"] 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 = service_with_two.test_connection("c1") assert result["success"] is True assert result["db_version"] == "PostgreSQL 16.3" assert "latency_ms" in result def test_clickhouse_test_success(self, service_with_two): with patch.object(service_with_two, '_test_clickhouse', return_value="ClickHouse 24.3"): result = service_with_two.test_connection("c2") assert result["success"] is True assert result["db_version"] == "ClickHouse 24.3" 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 = svc.test_connection("c3") assert result["success"] is True 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 = 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 == []