|
|
"""Analysis 存储迁移测试:Redis 缓存 + MySQL/SQLite 降级。"""
|
|
|
import json
|
|
|
from datetime import datetime
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
from app.storage_manager import StorageManager
|
|
|
|
|
|
|
|
|
class TestStorageManagerCache:
|
|
|
"""StorageManager 通用缓存方法测试。"""
|
|
|
|
|
|
def test_cache_get_returns_redis_value_on_hit(self):
|
|
|
"""Redis 命中时返回反序列化数据。"""
|
|
|
redis_client = MagicMock()
|
|
|
redis_client.get.return_value = json.dumps({"symbol": "AG2606", "score": 85})
|
|
|
manager = StorageManager(redis_client=redis_client, mysql_engine=MagicMock())
|
|
|
|
|
|
result = manager.cache_get("analysis:ai_analysis_cache:1")
|
|
|
|
|
|
assert result == {"symbol": "AG2606", "score": 85}
|
|
|
redis_client.get.assert_called_once_with("analysis:ai_analysis_cache:1")
|
|
|
|
|
|
def test_cache_get_returns_none_when_redis_unavailable(self):
|
|
|
"""Redis 不可用时返回 None。"""
|
|
|
manager = StorageManager(redis_client=None, mysql_engine=MagicMock())
|
|
|
|
|
|
result = manager.cache_get("analysis:ai_analysis_cache:1")
|
|
|
|
|
|
assert result is None
|
|
|
|
|
|
def test_cache_get_returns_none_on_miss(self):
|
|
|
"""Redis 未命中时返回 None。"""
|
|
|
redis_client = MagicMock()
|
|
|
redis_client.get.return_value = None
|
|
|
manager = StorageManager(redis_client=redis_client, mysql_engine=MagicMock())
|
|
|
|
|
|
result = manager.cache_get("analysis:ai_analysis_cache:1")
|
|
|
|
|
|
assert result is None
|
|
|
|
|
|
def test_cache_set_writes_to_redis_with_ttl(self):
|
|
|
"""cache_set 使用 TTL 写入 Redis。"""
|
|
|
redis_client = MagicMock()
|
|
|
manager = StorageManager(redis_client=redis_client, mysql_engine=MagicMock())
|
|
|
value = {"symbol": "AG2606", "score": 85}
|
|
|
|
|
|
manager.cache_set("analysis:ai_analysis_cache:1", value, ttl=300)
|
|
|
|
|
|
redis_client.setex.assert_called_once_with(
|
|
|
"analysis:ai_analysis_cache:1", 300, json.dumps(value, ensure_ascii=False)
|
|
|
)
|
|
|
|
|
|
def test_cache_set_returns_false_when_redis_unavailable(self):
|
|
|
"""Redis 不可用时 cache_set 返回 False。"""
|
|
|
manager = StorageManager(redis_client=None, mysql_engine=MagicMock())
|
|
|
|
|
|
result = manager.cache_set("analysis:ai_analysis_cache:1", {"score": 85}, ttl=300)
|
|
|
|
|
|
assert result is False
|
|
|
|
|
|
def test_cache_delete_removes_redis_key(self):
|
|
|
"""cache_delete 删除 Redis 键。"""
|
|
|
redis_client = MagicMock()
|
|
|
manager = StorageManager(redis_client=redis_client, mysql_engine=MagicMock())
|
|
|
|
|
|
manager.cache_delete("analysis:ai_analysis_cache:1")
|
|
|
|
|
|
redis_client.delete.assert_called_once_with("analysis:ai_analysis_cache:1")
|
|
|
|
|
|
|
|
|
class TestAnalysisDbSwitching:
|
|
|
"""analysis_db 动态 Session 切换测试。"""
|
|
|
|
|
|
def test_get_analysis_db_returns_mysql_session_when_mysql_available(self):
|
|
|
"""MySQL 可用时返回 MySQL Session。"""
|
|
|
from app.analysis_db import get_analysis_db
|
|
|
|
|
|
mysql_engine = MagicMock()
|
|
|
mock_session = MagicMock()
|
|
|
mock_sessionmaker = MagicMock(return_value=mock_session)
|
|
|
mock_session.__enter__ = MagicMock(return_value=mock_session)
|
|
|
mock_session.__exit__ = MagicMock(return_value=False)
|
|
|
|
|
|
with patch("app.analysis_db.MySQLSessionLocal", mock_sessionmaker):
|
|
|
with patch("app.analysis_db.get_storage_manager") as mock_sm:
|
|
|
sm = MagicMock()
|
|
|
sm.check_mysql.return_value = True
|
|
|
sm.mysql_engine = mysql_engine
|
|
|
mock_sm.return_value = sm
|
|
|
|
|
|
db = next(get_analysis_db())
|
|
|
|
|
|
assert db is mock_session
|
|
|
|
|
|
def test_get_analysis_db_returns_sqlite_session_when_mysql_unavailable(self):
|
|
|
"""MySQL 不可用时返回 SQLite Session。"""
|
|
|
from app.analysis_db import get_analysis_db
|
|
|
|
|
|
sqlite_engine = MagicMock()
|
|
|
mock_session = MagicMock()
|
|
|
mock_sessionmaker = MagicMock(return_value=mock_session)
|
|
|
mock_session.__enter__ = MagicMock(return_value=mock_session)
|
|
|
mock_session.__exit__ = MagicMock(return_value=False)
|
|
|
|
|
|
with patch("app.analysis_db.AnalysisSessionLocal", mock_sessionmaker):
|
|
|
with patch("app.analysis_db.get_storage_manager") as mock_sm:
|
|
|
sm = MagicMock()
|
|
|
sm.check_mysql.return_value = False
|
|
|
mock_sm.return_value = sm
|
|
|
|
|
|
db = next(get_analysis_db())
|
|
|
|
|
|
assert db is mock_session
|
|
|
|
|
|
|
|
|
class TestAnalysisMigration:
|
|
|
"""Analysis 数据迁移测试。"""
|
|
|
|
|
|
def test_migrate_skips_when_mysql_has_data(self):
|
|
|
"""MySQL 已有数据时跳过迁移。"""
|
|
|
from app.analysis_migration import migrate_analysis_sqlite_to_mysql
|
|
|
|
|
|
with patch("app.analysis_migration.mysql_engine", MagicMock()):
|
|
|
with patch("app.analysis_migration._count_mysql_analysis_records") as mock_count:
|
|
|
mock_count.return_value = 10
|
|
|
|
|
|
result = migrate_analysis_sqlite_to_mysql()
|
|
|
|
|
|
assert result is True
|
|
|
mock_count.assert_called_once()
|
|
|
|
|
|
def test_migrate_moves_data_from_sqlite_to_mysql(self):
|
|
|
"""SQLite 数据正确迁移到 MySQL。"""
|
|
|
from app.analysis_migration import migrate_analysis_sqlite_to_mysql
|
|
|
|
|
|
with patch("app.analysis_migration.mysql_engine", MagicMock()):
|
|
|
with patch("app.analysis_migration._count_mysql_analysis_records") as mock_count:
|
|
|
mock_count.return_value = 0
|
|
|
with patch("app.analysis_migration._read_sqlite_analysis_data") as mock_read:
|
|
|
mock_read.return_value = {
|
|
|
"ai_analysis_cache": [{"id": 1, "symbol": "AG2606"}]
|
|
|
}
|
|
|
with patch("app.analysis_migration._write_mysql_analysis_data") as mock_write:
|
|
|
mock_write.return_value = True
|
|
|
|
|
|
result = migrate_analysis_sqlite_to_mysql()
|
|
|
|
|
|
assert result is True
|
|
|
mock_write.assert_called_once_with({"ai_analysis_cache": [{"id": 1, "symbol": "AG2606"}]})
|