Initial commit: clean project structure

- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+)
- Frontend: Vue 3 + TypeScript + Pinia + Tailwind
- Admin Frontend: separate Vue 3 app for management
- Docker Compose: 9 services orchestration
- Specs: design prototypes, memory system PRD, product roadmap

Cleanup performed:
- Removed temporary debug scripts from backend root
- Removed deprecated admin_app.py (embedded UI)
- Removed duplicate docs from admin-frontend
- Updated .gitignore for Vite cache and egg-info
This commit is contained in:
zhangtuo
2026-01-20 18:20:03 +08:00
commit e9d7f8832a
241 changed files with 33070 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
"""故事 API 测试。"""
import time
from unittest.mock import AsyncMock, patch
import pytest
from fastapi.testclient import TestClient
from app.api.stories import _request_log, RATE_LIMIT_REQUESTS
class TestStoryGenerate:
"""故事生成测试。"""
def test_generate_without_auth(self, client: TestClient):
"""未登录时生成故事。"""
response = client.post(
"/api/generate",
json={"type": "keywords", "data": "小兔子, 森林"},
)
assert response.status_code == 401
def test_generate_with_empty_data(self, auth_client: TestClient):
"""空数据生成故事。"""
response = auth_client.post(
"/api/generate",
json={"type": "keywords", "data": ""},
)
assert response.status_code == 422
def test_generate_with_invalid_type(self, auth_client: TestClient):
"""无效类型生成故事。"""
response = auth_client.post(
"/api/generate",
json={"type": "invalid", "data": "test"},
)
assert response.status_code == 422
def test_generate_story_success(self, auth_client: TestClient, mock_text_provider):
"""成功生成故事。"""
response = auth_client.post(
"/api/generate",
json={"type": "keywords", "data": "小兔子, 森林, 勇气"},
)
assert response.status_code == 200
data = response.json()
assert "id" in data
assert "title" in data
assert "story_text" in data
assert data["mode"] == "generated"
class TestStoryList:
"""故事列表测试。"""
def test_list_without_auth(self, client: TestClient):
"""未登录时获取列表。"""
response = client.get("/api/stories")
assert response.status_code == 401
def test_list_empty(self, auth_client: TestClient):
"""空列表。"""
response = auth_client.get("/api/stories")
assert response.status_code == 200
assert response.json() == []
def test_list_with_stories(self, auth_client: TestClient, test_story):
"""有故事时获取列表。"""
response = auth_client.get("/api/stories")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["id"] == test_story.id
assert data[0]["title"] == test_story.title
def test_list_pagination(self, auth_client: TestClient, test_story):
"""分页测试。"""
response = auth_client.get("/api/stories?limit=1&offset=0")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
response = auth_client.get("/api/stories?limit=1&offset=1")
assert response.status_code == 200
data = response.json()
assert len(data) == 0
class TestStoryDetail:
"""故事详情测试。"""
def test_get_story_without_auth(self, client: TestClient, test_story):
"""未登录时获取详情。"""
response = client.get(f"/api/stories/{test_story.id}")
assert response.status_code == 401
def test_get_story_not_found(self, auth_client: TestClient):
"""故事不存在。"""
response = auth_client.get("/api/stories/99999")
assert response.status_code == 404
def test_get_story_success(self, auth_client: TestClient, test_story):
"""成功获取详情。"""
response = auth_client.get(f"/api/stories/{test_story.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == test_story.id
assert data["title"] == test_story.title
assert data["story_text"] == test_story.story_text
class TestStoryDelete:
"""故事删除测试。"""
def test_delete_without_auth(self, client: TestClient, test_story):
"""未登录时删除。"""
response = client.delete(f"/api/stories/{test_story.id}")
assert response.status_code == 401
def test_delete_not_found(self, auth_client: TestClient):
"""删除不存在的故事。"""
response = auth_client.delete("/api/stories/99999")
assert response.status_code == 404
def test_delete_success(self, auth_client: TestClient, test_story):
"""成功删除故事。"""
response = auth_client.delete(f"/api/stories/{test_story.id}")
assert response.status_code == 200
assert response.json()["message"] == "Deleted"
response = auth_client.get(f"/api/stories/{test_story.id}")
assert response.status_code == 404
class TestRateLimit:
"""Rate limit 测试。"""
def setup_method(self):
"""每个测试前清理 rate limit 缓存。"""
_request_log.clear()
def test_rate_limit_allows_normal_requests(self, auth_client: TestClient, test_story):
"""正常请求不触发限流。"""
for _ in range(RATE_LIMIT_REQUESTS - 1):
response = auth_client.get(f"/api/stories/{test_story.id}")
assert response.status_code == 200
def test_rate_limit_blocks_excess_requests(self, auth_client: TestClient, test_story):
"""超限请求被阻止。"""
for _ in range(RATE_LIMIT_REQUESTS):
auth_client.get(f"/api/stories/{test_story.id}")
response = auth_client.get(f"/api/stories/{test_story.id}")
assert response.status_code == 429
assert "Too many requests" in response.json()["detail"]
class TestImageGenerate:
"""封面图片生成测试。"""
def test_generate_image_without_auth(self, client: TestClient, test_story):
"""未登录时生成图片。"""
response = client.post(f"/api/image/generate/{test_story.id}")
assert response.status_code == 401
def test_generate_image_not_found(self, auth_client: TestClient):
"""故事不存在。"""
response = auth_client.post("/api/image/generate/99999")
assert response.status_code == 404
class TestAudio:
"""语音朗读测试。"""
def test_get_audio_without_auth(self, client: TestClient, test_story):
"""未登录时获取音频。"""
response = client.get(f"/api/audio/{test_story.id}")
assert response.status_code == 401
def test_get_audio_not_found(self, auth_client: TestClient):
"""故事不存在。"""
response = auth_client.get("/api/audio/99999")
assert response.status_code == 404
def test_get_audio_success(self, auth_client: TestClient, test_story, mock_tts_provider):
"""成功获取音频。"""
response = auth_client.get(f"/api/audio/{test_story.id}")
assert response.status_code == 200
assert response.headers["content-type"] == "audio/mpeg"
class TestGenerateFull:
"""完整故事生成测试(/api/generate/full"""
def test_generate_full_without_auth(self, client: TestClient):
"""未登录时生成完整故事。"""
response = client.post(
"/api/generate/full",
json={"type": "keywords", "data": "小兔子, 森林"},
)
assert response.status_code == 401
def test_generate_full_success(self, auth_client: TestClient, mock_text_provider, mock_image_provider):
"""成功生成完整故事(含图片)。"""
response = auth_client.post(
"/api/generate/full",
json={"type": "keywords", "data": "小兔子, 森林, 勇气"},
)
assert response.status_code == 200
data = response.json()
assert "id" in data
assert "title" in data
assert "story_text" in data
assert data["mode"] == "generated"
assert data["image_url"] == "https://example.com/image.png"
assert data["audio_ready"] is False # 音频按需生成
assert data["errors"] == {}
def test_generate_full_image_failure(self, auth_client: TestClient, mock_text_provider):
"""图片生成失败时返回部分成功。"""
with patch("app.api.stories.generate_image", new_callable=AsyncMock) as mock_img:
mock_img.side_effect = Exception("Image API error")
response = auth_client.post(
"/api/generate/full",
json={"type": "keywords", "data": "小兔子, 森林"},
)
assert response.status_code == 200
data = response.json()
assert data["image_url"] is None
assert "image" in data["errors"]
assert "Image API error" in data["errors"]["image"]
def test_generate_full_with_education_theme(self, auth_client: TestClient, mock_text_provider, mock_image_provider):
"""带教育主题生成故事。"""
response = auth_client.post(
"/api/generate/full",
json={
"type": "keywords",
"data": "小兔子, 森林",
"education_theme": "勇气与友谊",
},
)
assert response.status_code == 200
mock_text_provider.assert_called_once()
call_kwargs = mock_text_provider.call_args.kwargs
assert call_kwargs["education_theme"] == "勇气与友谊"
class TestImageGenerateSuccess:
"""封面图片生成成功测试。"""
def test_generate_image_success(self, auth_client: TestClient, test_story, mock_image_provider):
"""成功生成图片。"""
response = auth_client.post(f"/api/image/generate/{test_story.id}")
assert response.status_code == 200
data = response.json()
assert data["image_url"] == "https://example.com/image.png"