wip: snapshot full local workspace state
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
This commit is contained in:
@@ -1 +1 @@
|
||||
# Tests package
|
||||
# Tests package
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
"""认证相关测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.security import create_access_token, decode_access_token
|
||||
|
||||
|
||||
class TestJWT:
|
||||
"""JWT token 测试。"""
|
||||
|
||||
def test_create_and_decode_token(self):
|
||||
"""测试 token 创建和解码。"""
|
||||
payload = {"sub": "github:12345"}
|
||||
token = create_access_token(payload)
|
||||
decoded = decode_access_token(token)
|
||||
assert decoded is not None
|
||||
assert decoded["sub"] == "github:12345"
|
||||
|
||||
def test_decode_invalid_token(self):
|
||||
"""测试无效 token 解码。"""
|
||||
result = decode_access_token("invalid-token")
|
||||
assert result is None
|
||||
|
||||
def test_decode_empty_token(self):
|
||||
"""测试空 token 解码。"""
|
||||
result = decode_access_token("")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestSession:
|
||||
"""Session 端点测试。"""
|
||||
|
||||
def test_session_without_auth(self, client: TestClient):
|
||||
"""未登录时获取 session。"""
|
||||
response = client.get("/auth/session")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] is None
|
||||
|
||||
def test_session_with_auth(self, auth_client: TestClient, test_user):
|
||||
"""已登录时获取 session。"""
|
||||
response = auth_client.get("/auth/session")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] is not None
|
||||
assert data["user"]["id"] == test_user.id
|
||||
assert data["user"]["name"] == test_user.name
|
||||
|
||||
def test_session_with_invalid_token(self, client: TestClient):
|
||||
"""无效 token 获取 session。"""
|
||||
client.cookies.set("access_token", "invalid-token")
|
||||
response = client.get("/auth/session")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] is None
|
||||
|
||||
|
||||
class TestSignout:
|
||||
"""登出测试。"""
|
||||
|
||||
def test_signout(self, auth_client: TestClient):
|
||||
"""测试登出。"""
|
||||
response = auth_client.post("/auth/signout", follow_redirects=False)
|
||||
assert response.status_code == 302
|
||||
"""认证相关测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.security import create_access_token, decode_access_token
|
||||
|
||||
|
||||
class TestJWT:
|
||||
"""JWT token 测试。"""
|
||||
|
||||
def test_create_and_decode_token(self):
|
||||
"""测试 token 创建和解码。"""
|
||||
payload = {"sub": "github:12345"}
|
||||
token = create_access_token(payload)
|
||||
decoded = decode_access_token(token)
|
||||
assert decoded is not None
|
||||
assert decoded["sub"] == "github:12345"
|
||||
|
||||
def test_decode_invalid_token(self):
|
||||
"""测试无效 token 解码。"""
|
||||
result = decode_access_token("invalid-token")
|
||||
assert result is None
|
||||
|
||||
def test_decode_empty_token(self):
|
||||
"""测试空 token 解码。"""
|
||||
result = decode_access_token("")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestSession:
|
||||
"""Session 端点测试。"""
|
||||
|
||||
def test_session_without_auth(self, client: TestClient):
|
||||
"""未登录时获取 session。"""
|
||||
response = client.get("/auth/session")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] is None
|
||||
|
||||
def test_session_with_auth(self, auth_client: TestClient, test_user):
|
||||
"""已登录时获取 session。"""
|
||||
response = auth_client.get("/auth/session")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] is not None
|
||||
assert data["user"]["id"] == test_user.id
|
||||
assert data["user"]["name"] == test_user.name
|
||||
|
||||
def test_session_with_invalid_token(self, client: TestClient):
|
||||
"""无效 token 获取 session。"""
|
||||
client.cookies.set("access_token", "invalid-token")
|
||||
response = client.get("/auth/session")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] is None
|
||||
|
||||
|
||||
class TestSignout:
|
||||
"""登出测试。"""
|
||||
|
||||
def test_signout(self, auth_client: TestClient):
|
||||
"""测试登出。"""
|
||||
response = auth_client.post("/auth/signout", follow_redirects=False)
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -1,195 +1,195 @@
|
||||
"""Provider router 测试 - failover 和配置加载。"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.adapters import AdapterConfig
|
||||
from app.services.adapters.text.models import StoryOutput
|
||||
|
||||
|
||||
class TestProviderFailover:
|
||||
"""Provider failover 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failover_to_second_provider(self):
|
||||
"""第一个 provider 失败时切换到第二个。"""
|
||||
from app.services import provider_router
|
||||
|
||||
# Mock 两个 provider - 使用 spec=False 并显式设置所有属性
|
||||
mock_provider_1 = MagicMock()
|
||||
mock_provider_1.configure_mock(
|
||||
id="provider-1",
|
||||
type="text",
|
||||
adapter="text_primary",
|
||||
api_key="key1",
|
||||
api_base=None,
|
||||
model=None,
|
||||
timeout_ms=60000,
|
||||
max_retries=3,
|
||||
config_ref=None,
|
||||
config_json={},
|
||||
priority=10,
|
||||
weight=1.0,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
mock_provider_2 = MagicMock()
|
||||
mock_provider_2.configure_mock(
|
||||
id="provider-2",
|
||||
type="text",
|
||||
adapter="text_primary",
|
||||
api_key="key2",
|
||||
api_base=None,
|
||||
model=None,
|
||||
timeout_ms=60000,
|
||||
max_retries=3,
|
||||
config_ref=None,
|
||||
config_json={},
|
||||
priority=5,
|
||||
weight=1.0,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
mock_providers = [mock_provider_1, mock_provider_2]
|
||||
|
||||
mock_result = StoryOutput(
|
||||
mode="generated",
|
||||
title="测试故事",
|
||||
story_text="内容",
|
||||
cover_prompt_suggestion="prompt",
|
||||
)
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def mock_execute(**kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise Exception("First provider failed")
|
||||
return mock_result
|
||||
|
||||
with patch.object(provider_router, "get_providers", return_value=mock_providers):
|
||||
with patch("app.services.adapters.AdapterRegistry.get") as mock_get:
|
||||
mock_adapter_class = MagicMock()
|
||||
mock_adapter_instance = MagicMock()
|
||||
mock_adapter_instance.execute = mock_execute
|
||||
mock_adapter_class.return_value = mock_adapter_instance
|
||||
mock_get.return_value = mock_adapter_class
|
||||
|
||||
result = await provider_router.generate_story_content(
|
||||
input_type="keywords",
|
||||
data="测试",
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
assert call_count == 2 # 第一个失败,第二个成功
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_providers_fail(self):
|
||||
"""所有 provider 都失败时抛出异常。"""
|
||||
from app.services import provider_router
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.configure_mock(
|
||||
id="provider-1",
|
||||
type="text",
|
||||
adapter="text_primary",
|
||||
api_key="key1",
|
||||
api_base=None,
|
||||
model=None,
|
||||
timeout_ms=60000,
|
||||
max_retries=3,
|
||||
config_ref=None,
|
||||
config_json={},
|
||||
priority=10,
|
||||
weight=1.0,
|
||||
enabled=True,
|
||||
)
|
||||
mock_providers = [mock_provider]
|
||||
|
||||
async def mock_execute(**kwargs):
|
||||
raise Exception("Provider failed")
|
||||
|
||||
with patch.object(provider_router, "get_providers", return_value=mock_providers):
|
||||
with patch("app.services.adapters.AdapterRegistry.get") as mock_get:
|
||||
mock_adapter_class = MagicMock()
|
||||
mock_adapter_instance = MagicMock()
|
||||
mock_adapter_instance.execute = mock_execute
|
||||
mock_adapter_class.return_value = mock_adapter_instance
|
||||
mock_get.return_value = mock_adapter_class
|
||||
|
||||
with pytest.raises(ValueError, match="No text provider succeeded"):
|
||||
await provider_router.generate_story_content(
|
||||
input_type="keywords",
|
||||
data="测试",
|
||||
)
|
||||
|
||||
|
||||
class TestProviderConfigFromDB:
|
||||
"""从 DB 加载 provider 配置测试。"""
|
||||
|
||||
def test_build_config_from_provider_with_api_key(self):
|
||||
"""Provider 有 api_key 时优先使用。"""
|
||||
from app.services.provider_router import _build_config_from_provider
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.adapter = "text_primary"
|
||||
mock_provider.api_key = "db-api-key"
|
||||
mock_provider.api_base = "https://custom.api.com"
|
||||
mock_provider.model = "custom-model"
|
||||
mock_provider.timeout_ms = 30000
|
||||
mock_provider.max_retries = 5
|
||||
mock_provider.config_ref = None
|
||||
mock_provider.config_json = {}
|
||||
|
||||
config = _build_config_from_provider(mock_provider)
|
||||
|
||||
assert config.api_key == "db-api-key"
|
||||
assert config.api_base == "https://custom.api.com"
|
||||
assert config.model == "custom-model"
|
||||
assert config.timeout_ms == 30000
|
||||
assert config.max_retries == 5
|
||||
|
||||
def test_build_config_fallback_to_settings(self):
|
||||
"""Provider 无 api_key 时回退到 settings。"""
|
||||
from app.services.provider_router import _build_config_from_provider
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.adapter = "text_primary"
|
||||
mock_provider.api_key = None
|
||||
mock_provider.api_base = None
|
||||
mock_provider.model = None
|
||||
mock_provider.timeout_ms = None
|
||||
mock_provider.max_retries = None
|
||||
mock_provider.config_ref = "text_api_key"
|
||||
mock_provider.config_json = {}
|
||||
|
||||
with patch("app.services.provider_router.settings") as mock_settings:
|
||||
mock_settings.text_api_key = "settings-api-key"
|
||||
mock_settings.text_model = "gemini-2.0-flash"
|
||||
|
||||
config = _build_config_from_provider(mock_provider)
|
||||
|
||||
assert config.api_key == "settings-api-key"
|
||||
|
||||
|
||||
class TestProviderCacheStartup:
|
||||
"""Provider cache 启动加载测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_loaded_on_startup(self):
|
||||
"""启动时加载 provider cache。"""
|
||||
from app.main import _load_provider_cache
|
||||
|
||||
with patch("app.db.database._get_session_factory") as mock_factory:
|
||||
mock_session = AsyncMock()
|
||||
mock_factory.return_value.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_factory.return_value.__aexit__ = AsyncMock()
|
||||
|
||||
with patch("app.services.provider_cache.reload_providers", new_callable=AsyncMock) as mock_reload:
|
||||
mock_reload.return_value = {"text": [], "image": [], "tts": []}
|
||||
|
||||
await _load_provider_cache()
|
||||
|
||||
mock_reload.assert_called_once()
|
||||
"""Provider router 测试 - failover 和配置加载。"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.adapters import AdapterConfig
|
||||
from app.services.adapters.text.models import StoryOutput
|
||||
|
||||
|
||||
class TestProviderFailover:
|
||||
"""Provider failover 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failover_to_second_provider(self):
|
||||
"""第一个 provider 失败时切换到第二个。"""
|
||||
from app.services import provider_router
|
||||
|
||||
# Mock 两个 provider - 使用 spec=False 并显式设置所有属性
|
||||
mock_provider_1 = MagicMock()
|
||||
mock_provider_1.configure_mock(
|
||||
id="provider-1",
|
||||
type="text",
|
||||
adapter="text_primary",
|
||||
api_key="key1",
|
||||
api_base=None,
|
||||
model=None,
|
||||
timeout_ms=60000,
|
||||
max_retries=3,
|
||||
config_ref=None,
|
||||
config_json={},
|
||||
priority=10,
|
||||
weight=1.0,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
mock_provider_2 = MagicMock()
|
||||
mock_provider_2.configure_mock(
|
||||
id="provider-2",
|
||||
type="text",
|
||||
adapter="text_primary",
|
||||
api_key="key2",
|
||||
api_base=None,
|
||||
model=None,
|
||||
timeout_ms=60000,
|
||||
max_retries=3,
|
||||
config_ref=None,
|
||||
config_json={},
|
||||
priority=5,
|
||||
weight=1.0,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
mock_providers = [mock_provider_1, mock_provider_2]
|
||||
|
||||
mock_result = StoryOutput(
|
||||
mode="generated",
|
||||
title="测试故事",
|
||||
story_text="内容",
|
||||
cover_prompt_suggestion="prompt",
|
||||
)
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def mock_execute(**kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise Exception("First provider failed")
|
||||
return mock_result
|
||||
|
||||
with patch.object(provider_router, "get_providers", return_value=mock_providers):
|
||||
with patch("app.services.adapters.AdapterRegistry.get") as mock_get:
|
||||
mock_adapter_class = MagicMock()
|
||||
mock_adapter_instance = MagicMock()
|
||||
mock_adapter_instance.execute = mock_execute
|
||||
mock_adapter_class.return_value = mock_adapter_instance
|
||||
mock_get.return_value = mock_adapter_class
|
||||
|
||||
result = await provider_router.generate_story_content(
|
||||
input_type="keywords",
|
||||
data="测试",
|
||||
)
|
||||
|
||||
assert result == mock_result
|
||||
assert call_count == 2 # 第一个失败,第二个成功
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_providers_fail(self):
|
||||
"""所有 provider 都失败时抛出异常。"""
|
||||
from app.services import provider_router
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.configure_mock(
|
||||
id="provider-1",
|
||||
type="text",
|
||||
adapter="text_primary",
|
||||
api_key="key1",
|
||||
api_base=None,
|
||||
model=None,
|
||||
timeout_ms=60000,
|
||||
max_retries=3,
|
||||
config_ref=None,
|
||||
config_json={},
|
||||
priority=10,
|
||||
weight=1.0,
|
||||
enabled=True,
|
||||
)
|
||||
mock_providers = [mock_provider]
|
||||
|
||||
async def mock_execute(**kwargs):
|
||||
raise Exception("Provider failed")
|
||||
|
||||
with patch.object(provider_router, "get_providers", return_value=mock_providers):
|
||||
with patch("app.services.adapters.AdapterRegistry.get") as mock_get:
|
||||
mock_adapter_class = MagicMock()
|
||||
mock_adapter_instance = MagicMock()
|
||||
mock_adapter_instance.execute = mock_execute
|
||||
mock_adapter_class.return_value = mock_adapter_instance
|
||||
mock_get.return_value = mock_adapter_class
|
||||
|
||||
with pytest.raises(ValueError, match="No text provider succeeded"):
|
||||
await provider_router.generate_story_content(
|
||||
input_type="keywords",
|
||||
data="测试",
|
||||
)
|
||||
|
||||
|
||||
class TestProviderConfigFromDB:
|
||||
"""从 DB 加载 provider 配置测试。"""
|
||||
|
||||
def test_build_config_from_provider_with_api_key(self):
|
||||
"""Provider 有 api_key 时优先使用。"""
|
||||
from app.services.provider_router import _build_config_from_provider
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.adapter = "text_primary"
|
||||
mock_provider.api_key = "db-api-key"
|
||||
mock_provider.api_base = "https://custom.api.com"
|
||||
mock_provider.model = "custom-model"
|
||||
mock_provider.timeout_ms = 30000
|
||||
mock_provider.max_retries = 5
|
||||
mock_provider.config_ref = None
|
||||
mock_provider.config_json = {}
|
||||
|
||||
config = _build_config_from_provider(mock_provider)
|
||||
|
||||
assert config.api_key == "db-api-key"
|
||||
assert config.api_base == "https://custom.api.com"
|
||||
assert config.model == "custom-model"
|
||||
assert config.timeout_ms == 30000
|
||||
assert config.max_retries == 5
|
||||
|
||||
def test_build_config_fallback_to_settings(self):
|
||||
"""Provider 无 api_key 时回退到 settings。"""
|
||||
from app.services.provider_router import _build_config_from_provider
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.adapter = "text_primary"
|
||||
mock_provider.api_key = None
|
||||
mock_provider.api_base = None
|
||||
mock_provider.model = None
|
||||
mock_provider.timeout_ms = None
|
||||
mock_provider.max_retries = None
|
||||
mock_provider.config_ref = "text_api_key"
|
||||
mock_provider.config_json = {}
|
||||
|
||||
with patch("app.services.provider_router.settings") as mock_settings:
|
||||
mock_settings.text_api_key = "settings-api-key"
|
||||
mock_settings.text_model = "gemini-2.0-flash"
|
||||
|
||||
config = _build_config_from_provider(mock_provider)
|
||||
|
||||
assert config.api_key == "settings-api-key"
|
||||
|
||||
|
||||
class TestProviderCacheStartup:
|
||||
"""Provider cache 启动加载测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_loaded_on_startup(self):
|
||||
"""启动时加载 provider cache。"""
|
||||
from app.main import _load_provider_cache
|
||||
|
||||
with patch("app.db.database._get_session_factory") as mock_factory:
|
||||
mock_session = AsyncMock()
|
||||
mock_factory.return_value.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_factory.return_value.__aexit__ = AsyncMock()
|
||||
|
||||
with patch("app.services.provider_cache.reload_providers", new_callable=AsyncMock) as mock_reload:
|
||||
mock_reload.return_value = {"text": [], "image": [], "tts": []}
|
||||
|
||||
await _load_provider_cache()
|
||||
|
||||
mock_reload.assert_called_once()
|
||||
|
||||
@@ -65,4 +65,4 @@ def test_add_achievement(auth_client):
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert {"type": "勇气", "description": "克服黑暗"} in data["achievements"]
|
||||
assert {"type": "勇气", "description": "克服黑暗"} in data["achievements"]
|
||||
|
||||
Reference in New Issue
Block a user