|
|
|
|
|
"""配置存储测试:MySQL + JSON fallback。"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
from app.config_store import ConfigStore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_session_maker(mock_db):
|
|
|
|
|
|
"""构造正确的 sessionmaker mock,使 with 语句返回 mock_db。"""
|
|
|
|
|
|
session_obj = MagicMock()
|
|
|
|
|
|
session_obj.__enter__ = MagicMock(return_value=mock_db)
|
|
|
|
|
|
session_obj.__exit__ = MagicMock(return_value=False)
|
|
|
|
|
|
|
|
|
|
|
|
session_maker = MagicMock(return_value=session_obj)
|
|
|
|
|
|
return session_maker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestConfigStoreGet:
|
|
|
|
|
|
"""ConfigStore.get_config 测试。"""
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_config_returns_mysql_value_on_hit(self, tmp_path):
|
|
|
|
|
|
"""MySQL 命中时返回数据库中的配置。"""
|
|
|
|
|
|
mock_config = MagicMock()
|
|
|
|
|
|
mock_config.config_value = {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
|
|
|
|
|
|
mock_db = MagicMock()
|
|
|
|
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_config
|
|
|
|
|
|
|
|
|
|
|
|
storage = MagicMock()
|
|
|
|
|
|
storage.check_mysql.return_value = True
|
|
|
|
|
|
|
|
|
|
|
|
store = ConfigStore(
|
|
|
|
|
|
storage_manager=storage,
|
|
|
|
|
|
config_dir=tmp_path,
|
|
|
|
|
|
session_maker=_make_session_maker(mock_db),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = store.get_config("symbols", {"futures": {}})
|
|
|
|
|
|
|
|
|
|
|
|
assert result == {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_config_reads_json_and_backfills_when_mysql_miss(self, tmp_path):
|
|
|
|
|
|
"""MySQL 未命中时读取 JSON 并回填数据库。"""
|
|
|
|
|
|
json_path = tmp_path / "symbols_config.json"
|
|
|
|
|
|
json_path.write_text(json.dumps({"futures": {"沪银": "AG2608"}}, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
mock_db = MagicMock()
|
|
|
|
|
|
mock_db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
|
|
|
|
|
|
|
|
storage = MagicMock()
|
|
|
|
|
|
storage.check_mysql.return_value = True
|
|
|
|
|
|
|
|
|
|
|
|
store = ConfigStore(
|
|
|
|
|
|
storage_manager=storage,
|
|
|
|
|
|
config_dir=tmp_path,
|
|
|
|
|
|
session_maker=_make_session_maker(mock_db),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = store.get_config("symbols", {"futures": {}})
|
|
|
|
|
|
|
|
|
|
|
|
assert result == {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
mock_db.add.assert_called_once()
|
|
|
|
|
|
mock_db.commit.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_config_falls_back_to_json_when_mysql_unavailable(self, tmp_path):
|
|
|
|
|
|
"""MySQL 不可用时直接读取 JSON。"""
|
|
|
|
|
|
json_path = tmp_path / "symbols_config.json"
|
|
|
|
|
|
json_path.write_text(json.dumps({"futures": {"沪银": "AG2608"}}, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
storage = MagicMock()
|
|
|
|
|
|
storage.check_mysql.return_value = False
|
|
|
|
|
|
|
|
|
|
|
|
store = ConfigStore(
|
|
|
|
|
|
storage_manager=storage,
|
|
|
|
|
|
config_dir=tmp_path,
|
|
|
|
|
|
session_maker=None,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = store.get_config("symbols", {"futures": {}})
|
|
|
|
|
|
|
|
|
|
|
|
assert result == {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_config_returns_fallback_when_no_mysql_and_no_json(self, tmp_path):
|
|
|
|
|
|
"""MySQL 和 JSON 都不存在时返回 fallback。"""
|
|
|
|
|
|
storage = MagicMock()
|
|
|
|
|
|
storage.check_mysql.return_value = False
|
|
|
|
|
|
|
|
|
|
|
|
store = ConfigStore(
|
|
|
|
|
|
storage_manager=storage,
|
|
|
|
|
|
config_dir=tmp_path,
|
|
|
|
|
|
session_maker=None,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = store.get_config("symbols", {"futures": {"默认": "DF2609"}})
|
|
|
|
|
|
|
|
|
|
|
|
assert result == {"futures": {"默认": "DF2609"}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestConfigStoreSet:
|
|
|
|
|
|
"""ConfigStore.set_config 测试。"""
|
|
|
|
|
|
|
|
|
|
|
|
def test_set_config_writes_to_mysql_and_json(self, tmp_path):
|
|
|
|
|
|
"""MySQL 可用时同时写入数据库和 JSON。"""
|
|
|
|
|
|
mock_existing = MagicMock()
|
|
|
|
|
|
|
|
|
|
|
|
mock_db = MagicMock()
|
|
|
|
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_existing
|
|
|
|
|
|
|
|
|
|
|
|
storage = MagicMock()
|
|
|
|
|
|
storage.check_mysql.return_value = True
|
|
|
|
|
|
|
|
|
|
|
|
store = ConfigStore(
|
|
|
|
|
|
storage_manager=storage,
|
|
|
|
|
|
config_dir=tmp_path,
|
|
|
|
|
|
session_maker=_make_session_maker(mock_db),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = store.set_config("symbols", {"futures": {"沪银": "AG2608"}})
|
|
|
|
|
|
|
|
|
|
|
|
assert result is True
|
|
|
|
|
|
assert mock_existing.config_value == {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
mock_db.commit.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
json_path = tmp_path / "symbols_config.json"
|
|
|
|
|
|
assert json_path.exists()
|
|
|
|
|
|
assert json.loads(json_path.read_text(encoding="utf-8")) == {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
|
|
|
|
|
|
def test_set_config_writes_only_json_when_mysql_unavailable(self, tmp_path):
|
|
|
|
|
|
"""MySQL 不可用时只写入 JSON。"""
|
|
|
|
|
|
storage = MagicMock()
|
|
|
|
|
|
storage.check_mysql.return_value = False
|
|
|
|
|
|
|
|
|
|
|
|
store = ConfigStore(
|
|
|
|
|
|
storage_manager=storage,
|
|
|
|
|
|
config_dir=tmp_path,
|
|
|
|
|
|
session_maker=None,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = store.set_config("symbols", {"futures": {"沪银": "AG2608"}})
|
|
|
|
|
|
|
|
|
|
|
|
assert result is True
|
|
|
|
|
|
|
|
|
|
|
|
json_path = tmp_path / "symbols_config.json"
|
|
|
|
|
|
assert json_path.exists()
|
|
|
|
|
|
assert json.loads(json_path.read_text(encoding="utf-8")) == {"futures": {"沪银": "AG2608"}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestConfigMigration:
|
|
|
|
|
|
"""配置迁移测试。"""
|
|
|
|
|
|
|
|
|
|
|
|
def test_migrate_skips_when_config_exists(self, tmp_path):
|
|
|
|
|
|
"""数据库已有配置时跳过迁移。"""
|
|
|
|
|
|
from app.config_migration import migrate_configs_to_mysql
|
|
|
|
|
|
|
|
|
|
|
|
with patch("app.config_migration.get_config_store") as mock_get_store:
|
|
|
|
|
|
store = MagicMock()
|
|
|
|
|
|
store._config_exists_in_mysql.return_value = True
|
|
|
|
|
|
store.storage_manager.check_mysql.return_value = True
|
|
|
|
|
|
store.session_maker = MagicMock()
|
|
|
|
|
|
mock_get_store.return_value = store
|
|
|
|
|
|
|
|
|
|
|
|
result = migrate_configs_to_mysql()
|
|
|
|
|
|
|
|
|
|
|
|
assert result is True
|
|
|
|
|
|
store._load_json.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
def test_migrate_loads_json_and_saves(self, tmp_path):
|
|
|
|
|
|
"""从 JSON 读取并保存到数据库。"""
|
|
|
|
|
|
from app.config_migration import migrate_configs_to_mysql
|
|
|
|
|
|
|
|
|
|
|
|
symbols_path = tmp_path / "symbols_config.json"
|
|
|
|
|
|
symbols_path.write_text(json.dumps({"futures": {"沪银": "AG2608"}}, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
ai_path = tmp_path / "ai_config.json"
|
|
|
|
|
|
ai_path.write_text(json.dumps({"models": []}, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
with patch("app.config_migration.get_config_store") as mock_get_store:
|
|
|
|
|
|
store = MagicMock()
|
|
|
|
|
|
store._config_exists_in_mysql.return_value = False
|
|
|
|
|
|
store.storage_manager.check_mysql.return_value = True
|
|
|
|
|
|
store.session_maker = MagicMock()
|
|
|
|
|
|
store.config_dir = tmp_path
|
|
|
|
|
|
store.config_files = {
|
|
|
|
|
|
"symbols": symbols_path,
|
|
|
|
|
|
"ai": ai_path,
|
|
|
|
|
|
}
|
|
|
|
|
|
store._load_json.side_effect = lambda key, default: json.loads(store.config_files[key].read_text(encoding="utf-8")) if store.config_files[key].exists() else default
|
|
|
|
|
|
store._save_to_mysql.return_value = True
|
|
|
|
|
|
mock_get_store.return_value = store
|
|
|
|
|
|
|
|
|
|
|
|
result = migrate_configs_to_mysql()
|
|
|
|
|
|
|
|
|
|
|
|
assert result is True
|
|
|
|
|
|
assert store._save_to_mysql.call_count == 2
|