# Copyright (c) 2026 Patrick Motsch # All rights reserved. """Unit tests: PostgreSQL connector raises DatabaseQueryError on real failures. Historical regression: ``getRecordset`` and friends used to swallow every exception (``except Exception: log; return []``), which turned every kind of broken query into "no rows found". That hid bugs like: * dict passed where Postgres expected a UUID string ("can't adapt type 'dict'"), * missing/renamed columns after an incomplete schema migration, * dropped tables, lost connections, etc. These tests pin the new contract: empty result sets still return ``[]`` / ``None`` (normal), but any exception inside the query path propagates as ``DatabaseQueryError`` with the table name attached. The transaction is rolled back so the connection is usable for subsequent queries. """ from __future__ import annotations from unittest.mock import MagicMock import pytest import psycopg2.errors from modules.connectors.connectorDbPostgre import ( DatabaseConnector, DatabaseQueryError, _rollbackQuietly, ) class DummyTable: """Stand-in for a Pydantic model so we can drive the connector without a real DB. The connector reads ``model_class.__name__`` to derive the SQL table name, so the class name itself becomes the asserted table name in tests. """ model_fields = {} def _makeConnector(cursorBehavior): """Build a ``DatabaseConnector`` skeleton with mocked connection/cursor. ``cursorBehavior`` is a callable invoked with the cursor mock so the test can configure ``execute``/``fetchall``/``fetchone`` per scenario. """ connector = DatabaseConnector.__new__(DatabaseConnector) cursor = MagicMock() cursorContext = MagicMock() cursorContext.__enter__ = MagicMock(return_value=cursor) cursorContext.__exit__ = MagicMock(return_value=False) connection = MagicMock() connection.cursor.return_value = cursorContext connector.connection = connection connector._ensureTableExists = MagicMock(return_value=True) connector._systemTableName = "_system" cursorBehavior(cursor) return connector, connection, cursor class TestGetRecordsetFailLoud: def test_emptyResultStillReturnsList(self): """No rows → []; this is the normal happy path, not a failure.""" def behavior(cursor): cursor.execute.return_value = None cursor.fetchall.return_value = [] connector, connection, _ = _makeConnector(behavior) result = connector.getRecordset(DummyTable) assert result == [] connection.rollback.assert_not_called() def test_dictAdaptErrorRaisesDatabaseQueryError(self): """Reproduces the Trustee bug: passing a dict in WHERE → can't adapt → raise.""" def behavior(cursor): cursor.execute.side_effect = psycopg2.ProgrammingError( "can't adapt type 'dict'" ) connector, connection, _ = _makeConnector(behavior) with pytest.raises(DatabaseQueryError) as excinfo: connector.getRecordset( DummyTable, recordFilter={"featureInstanceId": {"id": "uuid", "featureCode": "trustee"}}, ) assert excinfo.value.table == "DummyTable" assert "can't adapt type 'dict'" in str(excinfo.value) assert isinstance(excinfo.value.original, psycopg2.ProgrammingError) connection.rollback.assert_called_once() def test_missingColumnRaisesDatabaseQueryError(self): def behavior(cursor): cursor.execute.side_effect = psycopg2.errors.UndefinedColumn( 'column "wat" does not exist' ) connector, connection, _ = _makeConnector(behavior) with pytest.raises(DatabaseQueryError) as excinfo: connector.getRecordset(DummyTable, recordFilter={"wat": "x"}) assert "wat" in str(excinfo.value) connection.rollback.assert_called_once() def test_operationalErrorRaisesDatabaseQueryError(self): """Connection lost mid-query is also a real failure that must propagate.""" def behavior(cursor): cursor.execute.side_effect = psycopg2.OperationalError("connection lost") connector, connection, _ = _makeConnector(behavior) with pytest.raises(DatabaseQueryError): connector.getRecordset(DummyTable) connection.rollback.assert_called_once() class TestGetRecordFailLoud: def test_recordNotFoundReturnsNone(self): """`fetchone()` returning None is "row missing", not an error.""" def behavior(cursor): cursor.execute.return_value = None cursor.fetchone.return_value = None connector, connection, _ = _makeConnector(behavior) result = connector.getRecord(DummyTable, "missing-id") assert result is None connection.rollback.assert_not_called() def test_queryErrorRaisesDatabaseQueryError(self): def behavior(cursor): cursor.execute.side_effect = psycopg2.errors.UndefinedTable( 'relation "DummyTable" does not exist' ) connector, connection, _ = _makeConnector(behavior) with pytest.raises(DatabaseQueryError) as excinfo: connector.getRecord(DummyTable, "any-id") assert excinfo.value.table == "DummyTable" connection.rollback.assert_called_once() class TestRollbackQuietly: def test_rollsBackOnLiveConnection(self): connection = MagicMock() _rollbackQuietly(connection) connection.rollback.assert_called_once() def test_swallowsRollbackError(self): """Rollback failure must not mask the original query error.""" connection = MagicMock() connection.rollback.side_effect = RuntimeError("rollback failed") _rollbackQuietly(connection) def test_noopOnNoneConnection(self): _rollbackQuietly(None)