Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

646 lines
24 KiB
Python

"""Unit tests for MariaDB integration."""
from __future__ import annotations
import os
import sys
import types
from unittest.mock import MagicMock, patch
import pytest
from integrations.catalog import classify_integrations as _classify_integrations
from integrations.mariadb import (
_QUERY_TRUNCATE_LEN,
DEFAULT_MARIADB_PORT,
MariaDBConfig,
build_mariadb_config,
get_global_status,
get_innodb_status,
get_process_list,
get_replication_status,
get_slow_queries,
mariadb_config_from_env,
mariadb_extract_params,
mariadb_is_available,
validate_mariadb_config,
)
from platform.common.truncation import truncate as _truncate_shared
def _truncate(text: str, max_len: int = _QUERY_TRUNCATE_LEN) -> str:
return _truncate_shared(text, max_len)
# ---------------------------------------------------------------------------
# Shared mock helpers
# ---------------------------------------------------------------------------
def _make_mock_conn(
fetchall_rows: list | None = None,
fetchone_row: tuple | None = None,
description: list | None = None,
) -> tuple[MagicMock, MagicMock]:
"""Return (mock_conn, mock_cursor) wired for use with `with conn.cursor() as cur:`."""
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.fetchall.return_value = fetchall_rows or []
mock_cursor.fetchone.return_value = fetchone_row
mock_cursor.description = description
mock_conn.cursor.return_value.__enter__ = lambda _self: mock_cursor
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
return mock_conn, mock_cursor
class TestMariaDBConfig:
def test_default_values(self):
config = MariaDBConfig(host="localhost", database="testdb")
assert config.port == 3306
assert config.ssl is True
assert config.timeout_seconds == 5
assert config.max_results == 50
assert config.username == ""
assert config.password == ""
def test_normalization(self):
config = MariaDBConfig(
host=" db.example.com ",
database=" testdb ",
username=" admin ",
)
assert config.host == "db.example.com"
assert config.database == "testdb"
assert config.username == "admin"
def test_is_configured(self):
assert MariaDBConfig(host="host", database="db").is_configured is True
assert MariaDBConfig(host="", database="db").is_configured is False
assert MariaDBConfig(host="host", database="").is_configured is False
class TestMariaDBBuild:
def test_build_mariadb_config(self):
raw = {
"host": "db.example.com",
"port": 3307,
"database": "foo",
"username": "admin",
"password": "secret",
"ssl": False,
}
config = build_mariadb_config(raw)
assert config.host == "db.example.com"
assert config.port == 3307
assert config.database == "foo"
assert config.username == "admin"
assert config.ssl is False
@patch.dict(
os.environ,
{
"MARIADB_HOST": "env-host",
"MARIADB_PORT": "3307",
"MARIADB_DATABASE": "env-db",
"MARIADB_USERNAME": "env-user",
"MARIADB_PASSWORD": "env-pass",
"MARIADB_SSL": "false",
},
)
def test_mariadb_config_from_env(self):
config = mariadb_config_from_env()
assert config is not None
assert config.host == "env-host"
assert config.port == 3307
assert config.database == "env-db"
assert config.username == "env-user"
assert config.ssl is False
@patch.dict(os.environ, {}, clear=True)
def test_mariadb_config_from_env_missing(self):
assert mariadb_config_from_env() is None
class TestMariaDBValidation:
@patch("integrations.mariadb._get_connection")
def test_validate_success(self, mock_get_conn):
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = ("10.11.6-MariaDB",)
mock_conn.cursor.return_value.__enter__ = lambda _self: mock_cursor
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
mock_get_conn.return_value = mock_conn
config = MariaDBConfig(host="host", database="test", username="user")
result = validate_mariadb_config(config)
assert result.ok is True
assert "10.11.6-MariaDB" in result.detail
assert "test" in result.detail
mock_conn.close.assert_called_once()
@patch("integrations.mariadb._get_connection", side_effect=Exception("Conn error"))
def test_validate_exception(self, _):
config = MariaDBConfig(host="host", database="test", username="user")
result = validate_mariadb_config(config)
assert result.ok is False
assert "Conn error" in result.detail
def test_validate_missing_host(self):
config = MariaDBConfig(host="", database="test")
result = validate_mariadb_config(config)
assert result.ok is False
assert "required" in result.detail
def test_validate_missing_database(self):
config = MariaDBConfig(host="host", database="")
result = validate_mariadb_config(config)
assert result.ok is False
assert "required" in result.detail
class TestResolveIntegrations:
def test_classify_mariadb(self):
integrations = [
{
"id": "123",
"service": "mariadb",
"status": "active",
"credentials": {
"host": "db.example.com",
"port": 3306,
"database": "prod",
"username": "admin",
"password": "secret",
},
}
]
resolved = _classify_integrations(integrations)
assert "mariadb" in resolved
assert resolved["mariadb"].host == "db.example.com"
assert resolved["mariadb"].database == "prod"
assert resolved["mariadb"].port == 3306
def test_classify_mariadb_skipped_without_host(self):
integrations = [
{
"id": "789",
"service": "mariadb",
"status": "active",
"credentials": {
"host": "",
"database": "prod",
"username": "admin",
},
}
]
resolved = _classify_integrations(integrations)
assert "mariadb" not in resolved
def test_classify_mariadb_skipped_without_database(self):
integrations = [
{
"id": "456",
"service": "mariadb",
"status": "active",
"credentials": {
"host": "db.example.com",
"database": "",
"username": "admin",
},
}
]
resolved = _classify_integrations(integrations)
assert "mariadb" not in resolved
# ---------------------------------------------------------------------------
# Truncation helper
# ---------------------------------------------------------------------------
class TestTruncate:
def test_short_string_unchanged(self) -> None:
assert _truncate("SELECT 1") == "SELECT 1"
def test_exact_limit_unchanged(self) -> None:
text = "x" * 200
assert _truncate(text) == text
def test_over_limit_truncated(self) -> None:
result = _truncate("x" * 201)
assert result.endswith("...")
assert len(result) == 200 # capped at limit
def test_empty_string(self) -> None:
assert _truncate("") == ""
def test_custom_max_len(self) -> None:
assert _truncate("hello world", max_len=5) == "he..."
def test_very_long_string(self) -> None:
result = _truncate("A" * 10_000)
assert result.endswith("...")
assert len(result) == 200 # capped at limit
# ---------------------------------------------------------------------------
# Helper utilities
# ---------------------------------------------------------------------------
class TestMariaDBIsAvailable:
def test_true_with_host_and_database(self) -> None:
assert (
mariadb_is_available({"mariadb": {"host": "db.example.com", "database": "prod"}})
is True
)
def test_false_when_missing_host(self) -> None:
assert mariadb_is_available({"mariadb": {"host": "", "database": "prod"}}) is False
def test_false_when_missing_database(self) -> None:
assert mariadb_is_available({"mariadb": {"host": "h", "database": ""}}) is False
def test_false_when_mariadb_key_absent(self) -> None:
assert mariadb_is_available({}) is False
def test_false_when_mariadb_is_empty_dict(self) -> None:
assert mariadb_is_available({"mariadb": {}}) is False
def test_false_when_host_is_none(self) -> None:
assert mariadb_is_available({"mariadb": {"host": None, "database": "prod"}}) is False
class TestMariaDBExtractParams:
def test_all_fields_returned(self) -> None:
sources = {
"mariadb": {
"host": "h",
"database": "d",
"username": "u",
"password": "p",
"port": 3307,
"ssl": False,
}
}
params = mariadb_extract_params(sources)
assert params["host"] == "h"
assert params["database"] == "d"
assert params["username"] == "u"
assert params["password"] == "p"
assert params["port"] == 3307
assert params["ssl"] is False
def test_defaults_when_keys_missing(self) -> None:
params = mariadb_extract_params({"mariadb": {}})
assert params["host"] == ""
assert params["port"] == DEFAULT_MARIADB_PORT
assert params["ssl"] is True
def test_empty_sources_dict(self) -> None:
params = mariadb_extract_params({})
assert params["host"] == ""
assert params["port"] == DEFAULT_MARIADB_PORT
# ---------------------------------------------------------------------------
# Query functions
# ---------------------------------------------------------------------------
class TestGetProcessList:
def _config(self) -> MariaDBConfig:
return MariaDBConfig(host="host", database="db", username="user")
@patch("integrations.mariadb._get_connection")
def test_returns_process_list_on_success(self, mock_get_conn: MagicMock) -> None:
rows = [(1, "root", "localhost", "mydb", "Query", 5, "executing", "SELECT sleep(5)")]
mock_conn, _ = _make_mock_conn(fetchall_rows=rows)
mock_get_conn.return_value = mock_conn
result = get_process_list(self._config())
assert result["available"] is True
assert result["source"] == "mariadb"
assert result["total_processes"] == 1
proc = result["processes"][0]
assert proc["id"] == 1
assert proc["user"] == "root"
assert proc["command"] == "Query"
assert proc["time_secs"] == 5
assert proc["query"] == "SELECT sleep(5)"
mock_conn.close.assert_called_once()
@patch("integrations.mariadb._get_connection")
def test_query_text_is_truncated(self, mock_get_conn: MagicMock) -> None:
long_query = "SELECT " + "x" * 300
rows = [(1, "root", "localhost", "db", "Query", 1, "", long_query)]
mock_conn, _ = _make_mock_conn(fetchall_rows=rows)
mock_get_conn.return_value = mock_conn
result = get_process_list(self._config())
assert result["processes"][0]["query"].endswith("...")
assert len(result["processes"][0]["query"]) == 200
@patch("integrations.mariadb._get_connection")
def test_empty_process_list(self, mock_get_conn: MagicMock) -> None:
mock_conn, _ = _make_mock_conn(fetchall_rows=[])
mock_get_conn.return_value = mock_conn
result = get_process_list(self._config())
assert result["available"] is True
assert result["total_processes"] == 0
assert result["processes"] == []
@patch("integrations.mariadb._get_connection")
def test_max_results_override(self, mock_get_conn: MagicMock) -> None:
mock_conn, mock_cursor = _make_mock_conn(fetchall_rows=[])
mock_get_conn.return_value = mock_conn
get_process_list(self._config(), max_results=10)
sql_call = mock_cursor.execute.call_args
assert sql_call[0][1] == (10,)
@patch("integrations.mariadb._get_connection", side_effect=Exception("timeout"))
def test_exception_returns_error(self, _: MagicMock) -> None:
result = get_process_list(self._config())
assert result["available"] is False
assert "timeout" in result["error"]
def test_not_configured_returns_error(self) -> None:
result = get_process_list(MariaDBConfig())
assert result["available"] is False
assert "Not configured" in result["error"]
class TestGetGlobalStatus:
def _config(self) -> MariaDBConfig:
return MariaDBConfig(host="host", database="db", username="user")
@patch("integrations.mariadb._get_connection")
def test_returns_curated_metrics(self, mock_get_conn: MagicMock) -> None:
all_status = [
("Threads_connected", "5"),
("Threads_running", "2"),
("Uptime", "86400"),
("Slow_queries", "10"),
("SomeOtherVar", "ignored"),
]
mock_conn, _ = _make_mock_conn(fetchall_rows=all_status)
mock_get_conn.return_value = mock_conn
result = get_global_status(self._config())
assert result["available"] is True
assert result["source"] == "mariadb"
assert result["metrics"]["Threads_connected"] == "5"
assert "Uptime" in result["metrics"]
assert "SomeOtherVar" not in result["metrics"]
mock_conn.close.assert_called_once()
@patch("integrations.mariadb._get_connection")
def test_missing_keys_skipped_gracefully(self, mock_get_conn: MagicMock) -> None:
mock_conn, _ = _make_mock_conn(fetchall_rows=[("Uptime", "3600")])
mock_get_conn.return_value = mock_conn
result = get_global_status(self._config())
assert result["available"] is True
assert result["metrics"]["Uptime"] == "3600"
@patch("integrations.mariadb._get_connection", side_effect=Exception("access denied"))
def test_exception_returns_error(self, _: MagicMock) -> None:
result = get_global_status(self._config())
assert result["available"] is False
assert "access denied" in result["error"]
def test_not_configured_returns_error(self) -> None:
result = get_global_status(MariaDBConfig())
assert result["available"] is False
class TestGetInnoDBStatus:
def _config(self) -> MariaDBConfig:
return MariaDBConfig(host="host", database="db", username="user")
@patch("integrations.mariadb._get_connection")
def test_returns_innodb_status_text(self, mock_get_conn: MagicMock) -> None:
status_text = "================\nBUFFER POOL AND MEMORY\n================"
mock_conn, _ = _make_mock_conn(fetchone_row=("InnoDB", "", status_text))
mock_get_conn.return_value = mock_conn
result = get_innodb_status(self._config())
assert result["available"] is True
assert result["source"] == "mariadb"
assert "BUFFER POOL" in result["innodb_status"]
mock_conn.close.assert_called_once()
@patch("integrations.mariadb._get_connection")
def test_long_status_text_truncated(self, mock_get_conn: MagicMock) -> None:
long_text = "x" * 5000
mock_conn, _ = _make_mock_conn(fetchone_row=("InnoDB", "", long_text))
mock_get_conn.return_value = mock_conn
result = get_innodb_status(self._config())
assert result["available"] is True
assert result["innodb_status"].endswith("(truncated)")
assert len(result["innodb_status"]) < 5000
@patch("integrations.mariadb._get_connection")
def test_none_row_returns_empty_string(self, mock_get_conn: MagicMock) -> None:
mock_conn, _ = _make_mock_conn(fetchone_row=None)
mock_get_conn.return_value = mock_conn
result = get_innodb_status(self._config())
assert result["available"] is True
assert result["innodb_status"] == ""
@patch("integrations.mariadb._get_connection", side_effect=Exception("no privilege"))
def test_exception_returns_error(self, _: MagicMock) -> None:
result = get_innodb_status(self._config())
assert result["available"] is False
assert "no privilege" in result["error"]
def test_not_configured_returns_error(self) -> None:
result = get_innodb_status(MariaDBConfig())
assert result["available"] is False
class TestGetSlowQueries:
def _config(self) -> MariaDBConfig:
return MariaDBConfig(host="host", database="mydb", username="user")
@patch("integrations.mariadb._get_connection")
def test_returns_slow_queries_when_perf_schema_enabled(self, mock_get_conn: MagicMock) -> None:
mock_conn = MagicMock()
mock_cursor = MagicMock()
query_rows = [("SELECT * FROM users", 100, 50.5, 5050.0, 1000, 100)]
mock_cursor.fetchone.side_effect = [(1,)]
mock_cursor.fetchall.return_value = query_rows
mock_conn.cursor.return_value.__enter__ = lambda _self: mock_cursor
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
mock_get_conn.return_value = mock_conn
result = get_slow_queries(self._config())
assert result["available"] is True
assert result["source"] == "mariadb"
assert result["total_queries"] == 1
q = result["queries"][0]
assert q["digest_text"] == "SELECT * FROM users"
assert q["count"] == 100
assert q["avg_time_ms"] == pytest.approx(50.5)
assert q["rows_examined"] == 1000
mock_conn.close.assert_called_once()
@patch("integrations.mariadb._get_connection")
def test_returns_note_when_perf_schema_disabled(self, mock_get_conn: MagicMock) -> None:
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = (0,)
mock_conn.cursor.return_value.__enter__ = lambda _self: mock_cursor
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
mock_get_conn.return_value = mock_conn
result = get_slow_queries(self._config())
assert result["available"] is True
assert "note" in result
assert "performance_schema" in result["note"]
assert result["queries"] == []
@patch("integrations.mariadb._get_connection", side_effect=Exception("query error"))
def test_exception_returns_error(self, _: MagicMock) -> None:
result = get_slow_queries(self._config())
assert result["available"] is False
assert "query error" in result["error"]
def test_not_configured_returns_error(self) -> None:
result = get_slow_queries(MariaDBConfig())
assert result["available"] is False
class TestGetReplicationStatus:
def _config(self) -> MariaDBConfig:
return MariaDBConfig(host="host", database="db", username="user")
@patch("integrations.mariadb._get_connection")
def test_returns_channels_on_success(self, mock_get_conn: MagicMock) -> None:
columns = [
"Slave_IO_Running",
"Slave_SQL_Running",
"Seconds_Behind_Master",
"Last_Error",
"Last_Errno",
"Master_Host",
"Master_Port",
"Master_Log_File",
"Relay_Log_Space",
"Exec_Master_Log_Pos",
"Connection_name",
]
row = ("Yes", "Yes", 0, "", 0, "primary.db", 3306, "binlog.000001", 1024, 512, "")
mock_conn, _ = _make_mock_conn(
fetchall_rows=[row],
description=[(col,) for col in columns],
)
mock_get_conn.return_value = mock_conn
result = get_replication_status(self._config())
assert result["available"] is True
assert result["source"] == "mariadb"
assert len(result["channels"]) == 1
ch = result["channels"][0]
assert ch["Slave_IO_Running"] == "Yes"
assert ch["Master_Host"] == "primary.db"
mock_conn.close.assert_called_once()
@patch("integrations.mariadb._get_connection")
def test_not_replica_returns_note(self, mock_get_conn: MagicMock) -> None:
mock_conn, _ = _make_mock_conn(fetchall_rows=[], description=None)
mock_get_conn.return_value = mock_conn
result = get_replication_status(self._config())
assert result["available"] is True
assert "not configured as a replica" in result["note"]
assert result["channels"] == []
@patch("integrations.mariadb._get_connection")
def test_fallback_to_show_slave_status(self, mock_get_conn: MagicMock) -> None:
# Stub pymysql so this test does not require PyMySQL at import time (matches the
# dynamic `import pymysql` inside get_replication_status error handling).
class _ProgrammingError(Exception):
pass
_fake_err = types.ModuleType("pymysql.err")
_fake_err.ProgrammingError = _ProgrammingError
_fake_pymysql = types.ModuleType("pymysql")
_fake_pymysql.err = _fake_err
_prior = sys.modules.get("pymysql")
sys.modules["pymysql"] = _fake_pymysql
try:
mock_conn = MagicMock()
mock_cursor = MagicMock()
columns = ["Slave_IO_Running", "Slave_SQL_Running"]
row = ("Yes", "Yes")
execute_call_count = 0
def execute_side_effect(stmt: str, *args: object, **kwargs: object) -> None:
nonlocal execute_call_count
execute_call_count += 1
if "ALL SLAVES" in stmt:
raise _ProgrammingError(1064, "syntax error")
mock_cursor.execute.side_effect = execute_side_effect
mock_cursor.fetchall.return_value = [row]
mock_cursor.description = [(col,) for col in columns]
mock_conn.cursor.return_value.__enter__ = lambda _self: mock_cursor
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
mock_get_conn.return_value = mock_conn
result = get_replication_status(self._config())
assert result["available"] is True
assert len(result["channels"]) == 1
assert execute_call_count == 2
finally:
if _prior is not None:
sys.modules["pymysql"] = _prior
else:
sys.modules.pop("pymysql", None)
@patch("integrations.mariadb._get_connection")
def test_partial_columns_handled(self, mock_get_conn: MagicMock) -> None:
columns = ["Slave_IO_Running", "Slave_SQL_Running"]
mock_conn, _ = _make_mock_conn(
fetchall_rows=[("Yes", "Yes")],
description=[(col,) for col in columns],
)
mock_get_conn.return_value = mock_conn
result = get_replication_status(self._config())
assert result["available"] is True
ch = result["channels"][0]
assert ch["Slave_IO_Running"] == "Yes"
assert "Master_Host" not in ch
@patch("integrations.mariadb._get_connection", side_effect=Exception("network error"))
def test_exception_returns_error(self, _: MagicMock) -> None:
result = get_replication_status(self._config())
assert result["available"] is False
assert "network error" in result["error"]
def test_not_configured_returns_error(self) -> None:
result = get_replication_status(MariaDBConfig())
assert result["available"] is False