"""Unit tests for the new capacity full-text DB methods on TrinoClient. Covers single-row and batch variants for capacity description, certificates, and references including the LEFT JOIN on `teamlandkarte_v_partners_latest`. """ from __future__ import annotations import re import pytest from teamlandkarte_mcp.database.trino_client import TrinoClient class _FakeCursor: """Minimal cursor double recording executes and returning canned rows.""" def __init__(self, rows: list[tuple] | None = None) -> None: self.executed: list[tuple[str, tuple[object, ...] | None]] = [] self._rows: list[tuple] = rows or [] def execute(self, sql: str, params=None) -> None: # noqa: ANN001 if params is None: params_tuple: tuple[object, ...] | None = None elif isinstance(params, tuple): params_tuple = params else: params_tuple = tuple(params) self.executed.append((sql, params_tuple)) def fetchone(self): return self._rows[0] if self._rows else None def fetchall(self): return list(self._rows) class _FakeCursorContext: def __init__(self, cursor: _FakeCursor): self._cursor = cursor def __enter__(self) -> _FakeCursor: return self._cursor def __exit__(self, exc_type, exc, tb) -> None: # noqa: ANN001 return None class _FakeConfig: host = "x" port = 1 username = "u" password = "p" http_scheme = "http" verify_ssl = False catalog = "c" schema = "s" pool_size = 1 def _mk_client(cur: _FakeCursor) -> TrinoClient: client = TrinoClient(_FakeConfig()) client._cursor = lambda: _FakeCursorContext(cur) # type: ignore[method-assign] return client def _norm_sql(sql: str) -> str: return re.sub(r"\s+", " ", sql).strip() def _assert_select_only(sql: str) -> None: upper = sql.strip().upper() assert upper.startswith("SELECT"), f"expected SELECT, got: {sql!r}" for kw in ("INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE"): assert kw not in upper, f"unexpected write keyword {kw} in SQL: {sql!r}" # --------------------------------------------------------------------------- # Single-row methods # --------------------------------------------------------------------------- def test_get_capacity_description_one_query_select_only_and_trims() -> None: cur = _FakeCursor(rows=[(" hello world ",)]) c = _mk_client(cur) out = c.get_capacity_description(42) assert len(cur.executed) == 1, "exactly one SQL query expected" sql, params = cur.executed[0] sql_n = _norm_sql(sql) _assert_select_only(sql) assert "FROM teamlandkarte_v_capacities_latest" in sql_n assert "WHERE id = ?" in sql_n assert params == (42,) assert out == "hello world" def test_get_capacity_description_null_returns_none() -> None: cur = _FakeCursor(rows=[(None,)]) c = _mk_client(cur) assert c.get_capacity_description(1) is None def test_get_capacity_description_whitespace_returns_none() -> None: cur = _FakeCursor(rows=[(" \t ",)]) c = _mk_client(cur) assert c.get_capacity_description(1) is None def test_get_capacity_description_no_row_returns_none() -> None: cur = _FakeCursor(rows=[]) c = _mk_client(cur) assert c.get_capacity_description(99) is None def test_get_capacity_certificates_one_query_filters_empty() -> None: cur = _FakeCursor( rows=[ ("ISTQB Foundation",), ("",), (" ",), (None,), ("PMP ",), ] ) c = _mk_client(cur) out = c.get_capacity_certificates(7) assert len(cur.executed) == 1 sql, params = cur.executed[0] sql_n = _norm_sql(sql) _assert_select_only(sql) assert "FROM teamlandkarte_v_capacity_certificates_latest" in sql_n assert "WHERE capacity_id = ?" in sql_n assert params == (7,) assert out == ["ISTQB Foundation", "PMP"] def test_get_capacity_certificates_no_rows_returns_empty_list() -> None: cur = _FakeCursor(rows=[]) c = _mk_client(cur) assert c.get_capacity_certificates(123) == [] assert len(cur.executed) == 1 def test_get_capacity_references_single_query_includes_partner_left_join() -> None: cur = _FakeCursor( rows=[ ("Project Alpha", "Acme GmbH"), ("Project Beta", ""), # NULL partner_id → COALESCE → "" ] ) c = _mk_client(cur) out = c.get_capacity_references(11) assert len(cur.executed) == 1, "partner LEFT JOIN must be in the same query" sql, params = cur.executed[0] sql_n = _norm_sql(sql) _assert_select_only(sql) assert "FROM teamlandkarte_v_capacity_references_latest" in sql_n assert "LEFT JOIN teamlandkarte_v_partners_latest" in sql_n assert "r.partner_id = p.id" in sql_n assert "WHERE r.capacity_id = ?" in sql_n assert params == (11,) assert out == [ {"projects": "Project Alpha", "partner_name": "Acme GmbH"}, {"projects": "Project Beta", "partner_name": ""}, ] def test_get_capacity_references_filters_empty_projects_keeps_empty_partner() -> None: cur = _FakeCursor( rows=[ ("", "Acme GmbH"), # empty projects → drop (" ", "X"), # whitespace only → drop (None, "Y"), # non-string → drop ("Migration", ""), # empty partner is allowed ] ) c = _mk_client(cur) out = c.get_capacity_references(11) assert out == [{"projects": "Migration", "partner_name": ""}] def test_get_capacity_references_partner_name_none_normalizes_to_empty() -> None: """Defensive: if the driver returns None for partner_name, treat as ''.""" cur = _FakeCursor(rows=[("Project Alpha", None)]) c = _mk_client(cur) out = c.get_capacity_references(11) assert out == [{"projects": "Project Alpha", "partner_name": ""}] # --------------------------------------------------------------------------- # Batch variants # --------------------------------------------------------------------------- def test_batch_get_capacity_descriptions_empty_input_returns_empty_no_sql() -> None: cur = _FakeCursor(rows=[]) c = _mk_client(cur) out = c.batch_get_capacity_descriptions([]) assert out == {} assert cur.executed == [] def test_batch_get_capacity_descriptions_one_query_groups_and_defaults() -> None: cur = _FakeCursor( rows=[ (1, " desc one "), (2, None), (3, " "), ] ) c = _mk_client(cur) out = c.batch_get_capacity_descriptions([1, 2, 3, 4]) assert len(cur.executed) == 1 sql, params = cur.executed[0] sql_n = _norm_sql(sql) _assert_select_only(sql) assert "FROM teamlandkarte_v_capacities_latest" in sql_n assert "WHERE id IN (?, ?, ?, ?)" in sql_n assert params == (1, 2, 3, 4) assert out == {"1": "desc one", "2": None, "3": None, "4": None} def test_batch_get_capacity_certificates_empty_input_returns_empty_no_sql() -> None: cur = _FakeCursor(rows=[]) c = _mk_client(cur) out = c.batch_get_capacity_certificates([]) assert out == {} assert cur.executed == [] def test_batch_get_capacity_certificates_groups_n_to_1_and_defaults() -> None: cur = _FakeCursor( rows=[ (1, "ISTQB"), (1, " PMP "), (1, ""), # filtered (2, "AWS Solutions Architect"), (1, None), # filtered ] ) c = _mk_client(cur) out = c.batch_get_capacity_certificates([1, 2, 3]) assert len(cur.executed) == 1 sql, params = cur.executed[0] sql_n = _norm_sql(sql) _assert_select_only(sql) assert "FROM teamlandkarte_v_capacity_certificates_latest" in sql_n assert "WHERE capacity_id IN (?, ?, ?)" in sql_n assert params == (1, 2, 3) assert out == { "1": ["ISTQB", "PMP"], "2": ["AWS Solutions Architect"], "3": [], # default for missing id } def test_batch_get_capacity_references_empty_input_returns_empty_no_sql() -> None: cur = _FakeCursor(rows=[]) c = _mk_client(cur) out = c.batch_get_capacity_references([]) assert out == {} assert cur.executed == [] def test_batch_get_capacity_references_partner_join_same_statement_and_groups() -> None: cur = _FakeCursor( rows=[ (1, "Project Alpha", "Acme GmbH"), (1, "Project Beta", ""), # NULL partner_id → kept with empty partner (2, "Migration", "BetaCorp"), (1, " ", "X"), # filtered (empty projects) (1, None, "Y"), # filtered (non-string projects) ] ) c = _mk_client(cur) out = c.batch_get_capacity_references([1, 2, 3]) assert len(cur.executed) == 1, ( "partner LEFT JOIN must be part of the same single batch query" ) sql, params = cur.executed[0] sql_n = _norm_sql(sql) _assert_select_only(sql) assert "FROM teamlandkarte_v_capacity_references_latest" in sql_n assert "LEFT JOIN teamlandkarte_v_partners_latest" in sql_n assert "r.partner_id = p.id" in sql_n assert "WHERE r.capacity_id IN (?, ?, ?)" in sql_n assert params == (1, 2, 3) assert out == { "1": [ {"projects": "Project Alpha", "partner_name": "Acme GmbH"}, {"projects": "Project Beta", "partner_name": ""}, ], "2": [{"projects": "Migration", "partner_name": "BetaCorp"}], "3": [], } def test_batch_get_capacity_references_partner_name_none_normalizes_to_empty() -> None: """Defensive: driver returns None for partner_name → treated as ''.""" cur = _FakeCursor(rows=[(1, "Project Alpha", None)]) c = _mk_client(cur) out = c.batch_get_capacity_references([1]) assert out == { "1": [{"projects": "Project Alpha", "partner_name": ""}], } # --------------------------------------------------------------------------- # SELECT-only guard parametric coverage # --------------------------------------------------------------------------- @pytest.mark.parametrize( "method,args,rows", [ ("get_capacity_description", (1,), [("x",)]), ("get_capacity_certificates", (1,), [("x",)]), ("get_capacity_references", (1,), [("x", "")]), ("batch_get_capacity_descriptions", ([1, 2],), [(1, "x")]), ("batch_get_capacity_certificates", ([1, 2],), [(1, "x")]), ("batch_get_capacity_references", ([1, 2],), [(1, "x", "")]), ], ) def test_capacity_fulltext_methods_emit_select_only( method: str, args: tuple, rows: list[tuple] ) -> None: cur = _FakeCursor(rows=rows) c = _mk_client(cur) getattr(c, method)(*args) assert cur.executed, f"{method} should have executed a query" assert len(cur.executed) == 1, f"{method} should issue exactly one query" sql, _params = cur.executed[0] _assert_select_only(sql)