在准备环境前提交次全部更改。
This commit is contained in:
94
apps/backend/tests/test_database.py
Normal file
94
apps/backend/tests/test_database.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
数据库连接模块单元测试。
|
||||
|
||||
覆盖:ETL 只读连接的创建、RLS site_id 设置、只读模式、异常处理。
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-unit-tests")
|
||||
|
||||
from app.database import get_etl_readonly_connection
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_etl_readonly_connection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetEtlReadonlyConnection:
|
||||
"""ETL 只读连接:验证连接参数、只读设置、RLS 隔离。"""
|
||||
|
||||
@patch("app.database.psycopg2.connect")
|
||||
def test_sets_readonly_and_site_id(self, mock_connect):
|
||||
"""连接后应依次执行 SET read_only 和 SET LOCAL site_id。"""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_conn.cursor.return_value.__enter__ = lambda s: mock_cursor
|
||||
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_connect.return_value = mock_conn
|
||||
|
||||
conn = get_etl_readonly_connection(site_id=42)
|
||||
|
||||
# 验证 autocommit 被关闭
|
||||
assert mock_conn.autocommit is False
|
||||
|
||||
# 验证执行了两条 SET 语句
|
||||
executed = [c.args[0] for c in mock_cursor.execute.call_args_list]
|
||||
assert "SET default_transaction_read_only = on" in executed[0]
|
||||
assert "SET LOCAL app.current_site_id" in executed[1]
|
||||
|
||||
# 验证 site_id 参数化传递(防 SQL 注入)
|
||||
site_id_call = mock_cursor.execute.call_args_list[1]
|
||||
assert site_id_call.args[1] == ("42",)
|
||||
|
||||
# 验证提交
|
||||
mock_conn.commit.assert_called_once()
|
||||
assert conn is mock_conn
|
||||
|
||||
@patch("app.database.psycopg2.connect")
|
||||
def test_accepts_string_site_id(self, mock_connect):
|
||||
"""site_id 为字符串时也应正常工作。"""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_conn.cursor.return_value.__enter__ = lambda s: mock_cursor
|
||||
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_connect.return_value = mock_conn
|
||||
|
||||
get_etl_readonly_connection(site_id="99")
|
||||
|
||||
site_id_call = mock_cursor.execute.call_args_list[1]
|
||||
assert site_id_call.args[1] == ("99",)
|
||||
|
||||
@patch("app.database.psycopg2.connect")
|
||||
def test_closes_connection_on_setup_error(self, mock_connect):
|
||||
"""SET 语句执行失败时应关闭连接并抛出异常。"""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.execute.side_effect = Exception("SET failed")
|
||||
mock_conn.cursor.return_value.__enter__ = lambda s: mock_cursor
|
||||
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_connect.return_value = mock_conn
|
||||
|
||||
with pytest.raises(Exception, match="SET failed"):
|
||||
get_etl_readonly_connection(site_id=1)
|
||||
|
||||
mock_conn.close.assert_called_once()
|
||||
|
||||
@patch("app.database.psycopg2.connect")
|
||||
def test_uses_etl_config_params(self, mock_connect):
|
||||
"""应使用 ETL_DB_* 配置项连接。"""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_conn.cursor.return_value.__enter__ = lambda s: mock_cursor
|
||||
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_connect.return_value = mock_conn
|
||||
|
||||
get_etl_readonly_connection(site_id=1)
|
||||
|
||||
connect_kwargs = mock_connect.call_args.kwargs
|
||||
# 验证使用了 ETL 数据库名(默认 etl_feiqiu)
|
||||
assert connect_kwargs["dbname"] == "etl_feiqiu"
|
||||
Reference in New Issue
Block a user